Variational PnL

Adds Total PnL to the Variational Portfolio page & header

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

})();