Variational PnL

Adds Total PnL to the Variational Portfolio page & header

// ==UserScript==
// @name         Variational PnL
// @namespace    variational-tools
// @version      0.1
// @description  Adds Total PnL to the Variational Portfolio page & header
// @author       inco
// @match        https://omni.variational.io/*
// @grant        none
// @license MIT 
// ==/UserScript==

(function() {

    const dataUpdateInterval = 30000; // How often to retrieve new data
    const portfolioPageCheckInterval = 1000; // How often to check whether we're on the portfolio page

    // ---------- HTML elements ----------
    const PnLBox = document.createElement('div');
    // flex flex-col sm:flex-col gap-0.5
    PnLBox.classList.add('flex', 'flex-col', 'sm:flex-col', 'gap-0.5')
    PnLBox.innerHTML = `
    <span class="text-blackwhite/50 truncate">Total PnL</span>
    <div class="flex items-center gap-1">
        <span class="inline-block tabular-nums text-left text-red transition ease-in-out duration-300" id="pnl-value-header">N/A</span>
    </div>
    `;

    const statsBar = document.createElement('div');
    statsBar.classList.add('relative', 'flex', 'items-center', 'justify-around', 'px-8', 'gap-3', 'h-16', 'bg-darkblue-400', 'rounded-sm', 'm-[2px]');

    statsBar.innerHTML = `
    <!--Num Transfers-->
    <div class="flex flex-col items-start text-xs text-center gap-0.5 whitespace-nowrap" style="width:100px">
        <div role="button" tabindex="0" class="text-blackwhite/50" style="color: #f7f5efc2">Num Orders</div>
        <div slot="value" class="transition ease-in-out duration-300">
            <span class="" id="num-transfers">N/A</span>
        </div>
    </div>

    <!--PnL-->
    <div class="flex flex-col items-start text-xs text-center gap-0.5 whitespace-nowrap" style="width:100px">
        <div role="button" tabindex="0" class="text-blackwhite/50" style="color: #f7f5efc2">Total Realized PnL</div>
        <div slot="value" class="transition ease-in-out duration-300">
            <span class="text-red" id="pnl-value">N/A</span>
        </div>
    </div>

    <!--Funding-->
    <div class="flex flex-col items-start text-xs text-center gap-0.5 whitespace-nowrap" style="width:100px">
        <div role="button" tabindex="0" class="text-blackwhite/50" style="color: #f7f5efc2">Total Funding</div>
        <div slot="value" class="transition ease-in-out duration-300">
            <span class="" id="funding-value">N/A</span>
        </div>
    </div>

    <!--Refunds-->
    <div class="flex flex-col items-start text-xs text-center gap-0.5 whitespace-nowrap" style="width:100px">
        <div role="button" tabindex="0" class="text-blackwhite/50" style="color: #f7f5efc2">Num Refunds</div>
        <div slot="value" class="transition ease-in-out duration-300">
            <span class="" id="num-refunds">N/A</span>
        </div>
    </div>

    <!--Refund Value-->
    <div class="flex flex-col items-start text-xs text-center gap-0.5 whitespace-nowrap" style="width:100px">
        <div role="button" tabindex="0" class="text-blackwhite/50" style="color: #f7f5efc2">Total Refund Value</div>
        <div slot="value" class="transition ease-in-out duration-300">
            <span class="" id="refund-value">N/A</span>
        </div>
    </div>
    `;

    const checkInterval = setInterval(() => {

        
        const pnlBoxInjectionTarget = document.querySelector('[data-testid="portfolio-summary"]');
        if (pnlBoxInjectionTarget) {
            pnlBoxInjectionTarget.prepend(PnLBox);
            console.log('PnL box added.');
            clearInterval(checkInterval); // Stop the interval once the element is found
        }
    }, 500); // Check every 500 milliseconds until found

    let statsBarAdded = false;

    setInterval(() => {
        if( !statsBarAdded && window.location.pathname === "/portfolio" ) {
            const statsBarInjectionTarget = document.querySelector('.relative.flex.flex-col.w-full.px-2.my-12');
            if (statsBarInjectionTarget) {
                //Select second child
                statsBarInjectionTarget.insertBefore(statsBar, statsBarInjectionTarget.children[2]);
                console.log('Stats bar added.');
                statsBarAdded = true;
            }
        } else if (statsBarAdded && window.location.pathname !== "/portfolio") {
            statsBarAdded = false;
        }
    }, portfolioPageCheckInterval); // Check every 1 second

    const $ = (sel) => statsBar.querySelector(sel);

    const el = {
        numTransfers: $('#num-transfers'),
        pnlValue: $('#pnl-value'),
        fundingValue: $('#funding-value'),
        numRefunds: $('#num-refunds'),
        refundValue: $('#refund-value'),
    }
    
    const pnlValueHeader = PnLBox.querySelector('#pnl-value-header');
    

    async function getTransfers(offset) {
    return await fetch(`https://omni.variational.io/api/transfers?limit=100&offset=${offset}&order_by=created_at&order=desc`, {
        headers: {
        "accept": "*/*",
        "accept-language": "en-US,en;q=0.6",
        "content-type": "application/json",
        },
        method: "GET",
        mode: "cors",


        credentials: "include"
    })
        .then(r => r.json())
        .then(r => {
            return r.result;
        })
    }

    // Queries paginated API endpoint, calculates PnL
    async function getPnL() {
        let offset = 0;
        let count = 0;
        let PnL = 0;
        let funding = 0;
        let refund = 0;
        let refundCount = 0;
        // Get pnl
        while (true) {
            const transactions = await getTransfers(offset);
            //console.log(transactions)
            if (transactions.length === 0) break;
            
            transactions.forEach(t => {
                if (t.transfer_type === "realized_pnl") {
                    PnL += parseFloat(t.qty);
                }
                if (t.transfer_type === "funding") {
                    funding += parseFloat(t.qty);
                }
                if (t.transfer_type === "reward") {
                    refund += parseFloat(t.qty);
                    refundCount += 1;
                }
            });
            offset += 100;
            count += transactions.length;
        }


        el.numTransfers.textContent = `${count}`;
        //console.log(`Total PnL over ${count} transfers: \$${PnL}`);
        el.pnlValue.textContent = `$${PnL.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
        el.fundingValue.textContent = `$${funding.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
        el.numRefunds.textContent = `${refundCount}`;
        el.refundValue.textContent = `$${refund.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;

        pnlValueHeader.textContent = `$${PnL.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;

        if( PnL > 0 ) {
            el.pnlValue.classList.replace('text-red', 'text-green');
            pnlValueHeader.classList.replace('text-red', 'text-green');
        } else {
            el.pnlValue.classList.replace('text-green', 'text-red');
            pnlValueHeader.classList.replace('text-green', 'text-red');
        }
        if( funding > 0 ) {
            el.fundingValue.classList.replace('text-red', 'text-green');
        } else {
            el.fundingValue.classList.replace('text-green', 'text-red');
        }
    }

    getPnL();
    // Update data every 30 seconds
    setInterval(getPnL, dataUpdateInterval);

})();