Variational PnL

Adds Total PnL to the Variational Portfolio page & header

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();