Injects Options Greeks into Polymarket using the Gamma API for exact expirations and the NY Fed API for live SOFR interest rates.
// ==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();
})();