BetterPolymarket

Injects Options Greeks into Polymarket using the Gamma API for exact expirations and the NY Fed API for live SOFR interest rates.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         BetterPolymarket
// @namespace    http://sidewaysturtle.com/
// @version      0.1.2
// @description  Injects Options Greeks into Polymarket using the Gamma API for exact expirations and the NY Fed API for live SOFR interest rates.
// @author       SanoKei
// @match        https://polymarket.com/event/*
// @grant        GM_xmlhttpRequest
// @license      GPL-3.0-or-later
// @connect      gamma-api.polymarket.com
// @connect      markets.newyorkfed.org
// ==/UserScript==

(function() {
    'use strict';

    // Global State
    let currentSlug = "";
    let riskFreeRate = 0.05; // Fallback
    let eventData = null;

    const CONFIG = {
        impliedVol: 0.50,   // 50% IV proxy for Delta/Gamma calculations
        proxyUnderlying: 100 // Abstract underlying asset value
    };

    // --- Math Helpers ---
    function normPDF(x) {
        return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
    }

    function invNormCDF(p) {
        if (p <= 0 || p >= 1) return p <= 0 ? -99 : 99;
        const c = [2.515517, 0.802853, 0.010328];
        const d = [1.432788, 0.189269, 0.001308];
        const t = Math.sqrt(-2.0 * Math.log(p < 0.5 ? p : 1 - p));
        const num = c[0] + t * (c[1] + t * c[2]);
        const den = 1.0 + t * (d[0] + t * (d[1] + t * d[2]));
        const x = t - num / den;
        return p < 0.5 ? -x : x;
    }

    function calculateGreeks(price, daysToExpiry) {
        if (price <= 0 || price >= 1 || daysToExpiry <= 0) return null;

        const T = daysToExpiry / 365.0;
        const r = riskFreeRate;
        const sigma = CONFIG.impliedVol;
        const S = CONFIG.proxyUnderlying;

        // Implied Nd2 accounting for interest discount rate
        let Nd2 = price * Math.exp(r * T);
        Nd2 = Math.min(Math.max(Nd2, 0.0001), 0.9999);
        const d2 = invNormCDF(Nd2);
        const d1 = d2 + sigma * Math.sqrt(T);
        const pdf_d2 = normPDF(d2);

        const delta = (Math.exp(-r * T) * pdf_d2) / (S * sigma * Math.sqrt(T));
        const gamma = -(Math.exp(-r * T) * pdf_d2 * d1) / (Math.pow(S, 2) * Math.pow(sigma, 2) * T);
        const vega = -Math.exp(-r * T) * pdf_d2 * (d1 / sigma);
        const rho = -T * Math.exp(-r * T) * Nd2;
        const theta = (r * Math.exp(-r * T) * Nd2) + (Math.exp(-r * T) * pdf_d2 * ((d1 / (2 * T)) - (r / (sigma * Math.sqrt(T)))));

        return {
            delta: delta.toFixed(4),
            gamma: gamma.toFixed(4),
            theta: (theta / 365).toFixed(4),
            vega: (vega / 100).toFixed(4),
            rho: (rho / 100).toFixed(4)
        };
    }

    // --- API Fetchers ---
    function fetchRiskFreeRate() {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: "https://markets.newyorkfed.org/api/rates/secured/sofr/last/1.json",
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data && data.refRates && data.refRates.length > 0) {
                            const rate = parseFloat(data.refRates[0].percentRate) / 100;
                            console.log(`[Greeks] Fetched Live SOFR: ${(rate * 100).toFixed(2)}%`);
                            resolve(rate);
                            return;
                        }
                    } catch(e) {
                        console.warn("[Greeks] Failed to parse NY Fed API, using 5% fallback.");
                    }
                    resolve(0.05);
                },
                onerror: () => resolve(0.05)
            });
        });
    }

    function fetchGammaEventData(slug) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://gamma-api.polymarket.com/events/slug/${slug}`,
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(JSON.parse(response.responseText));
                    } else {
                        resolve(null);
                    }
                },
                onerror: () => resolve(null)
            });
        });
    }

    // --- UI Injection ---
    function injectGreeksUI(greeks, expiryDateStr) {
        let container = document.getElementById('pm-greeks-container');
        if (!container) {
            container = document.createElement('div');
            container.id = 'pm-greeks-container';
            container.style.cssText = `
                margin-top: 16px;
                padding: 16px;
                background-color: var(--colors-bg-secondary, #f7f8fa);
                border-radius: 8px;
                border: 1px solid var(--colors-border, #e2e8f0);
                font-family: inherit;
                color: var(--colors-text-primary, #111827);
            `;

            const sidebar = document.querySelector('[data-testid="sidebar"]') || document.querySelector('aside') || document.querySelector('.flex.flex-col.w-full.max-w-md');
            if (sidebar) {
                sidebar.appendChild(container);
            } else {
                return;
            }
        }

        const expiryDate = new Date(expiryDateStr);

        container.innerHTML = `
            <h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between;">
                <span>Options Greeks</span>
                <span style="font-size: 11px; font-weight: 400; color: #6b7280;">r = ${(riskFreeRate * 100).toFixed(2)}% (SOFR)</span>
            </h3>
            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 13px;">
                <div style="display: flex; justify-content: space-between;"><span>Δ Delta:</span> <strong>${greeks.delta}</strong></div>
                <div style="display: flex; justify-content: space-between;"><span>Γ Gamma:</span> <strong>${greeks.gamma}</strong></div>
                <div style="display: flex; justify-content: space-between;"><span>Θ Theta (1D):</span> <strong>${greeks.theta}</strong></div>
                <div style="display: flex; justify-content: space-between;"><span>ν Vega:</span> <strong>${greeks.vega}</strong></div>
                <div style="display: flex; justify-content: space-between;"><span>ρ Rho:</span> <strong>${greeks.rho}</strong></div>
            </div>
            <div style="margin-top: 8px; font-size: 10px; color: #9ca3af; text-align: center;">
                Exp: ${expiryDate.toLocaleDateString()}
            </div>
        `;
    }

    async function updateGreeks() {
        // 1. Check if URL changed to trigger new API fetch
        const match = window.location.pathname.match(/\/event\/([^/]+)/);
        if (!match) return;
        const urlSlug = match[1];

        if (urlSlug !== currentSlug) {
            currentSlug = urlSlug;
            eventData = await fetchGammaEventData(currentSlug);
        }

        if (!eventData || !eventData.markets || eventData.markets.length === 0) return;

        // 2. Parse exact expiration from Gamma API
        const market = eventData.markets[0];
        const endDateStr = market.endDate;
        const expiryDate = new Date(endDateStr);

        const diffTime = expiryDate - new Date();
        const daysToExpiry = Math.max(0.01, diffTime / (1000 * 60 * 60 * 24)); // Prevent negative/zero

        // 3. Extract real-time price from the DOM for low-latency updates
        const priceElement = document.querySelector('[data-testid="buy-button-yes"] .font-semibold') || document.querySelector('.text-green-500.font-semibold');
        if (!priceElement) return;

        let priceText = priceElement.innerText.replace(/[^0-9.]/g, '');
        let price = parseFloat(priceText);
        if (price > 1) price = price / 100;

        // 4. Calculate & Inject
        const greeks = calculateGreeks(price, daysToExpiry);
        if (greeks) {
            injectGreeksUI(greeks, endDateStr);
        }
    }

    // --- Initialization & Observers ---
    async function init() {
        riskFreeRate = await fetchRiskFreeRate();

        const observer = new MutationObserver(() => {
            clearTimeout(window.greeksTimeout);
            window.greeksTimeout = setTimeout(updateGreeks, 500);
        });

        observer.observe(document.body, { childList: true, subtree: true });
        updateGreeks();
    }

    init();
})();