β ΠΠ°Π·Π°Π΄function parseDte(expiryStr) {
if (!expiryStr || expiryStr.length !== 6) return null;
const year = 2000 + parseInt(expiryStr.substring(0, 2));
const month = parseInt(expiryStr.substring(2, 4)) - 1;
const day = parseInt(expiryStr.substring(4, 6));
const expDate = new Date(Date.UTC(year, month, day, 8, 0, 0));
const diffMs = expDate.getTime() - Date.now();
return Math.max(0,
parseFloat((diffMs / 86400000).toFixed(2)));
}
function findGammaPlays(optionsData, spotPrices, cfg = {}) {
const { maxDte = 3, maxDistancePercent = 5,
limit = 20 } = cfg;
const results = [];
for (const opt of optionsData) {
const parts = opt.symbol.split('-');
if (parts.length < 4) continue;
const underlying = parts[0];
const expiryStr = parts[1];
const strike = parseFloat(parts[2]);
const spot = spotPrices[underlying];
if (!spot) continue;
const dte = parseDte(expiryStr);
if (dte === null || dte > maxDte) continue;
const distPct = Math.abs(
((strike - spot) / spot) * 100);
if (distPct > maxDistancePercent) continue;
const gamma = parseFloat(opt.gamma || 0);
const delta = parseFloat(opt.delta || 0);
const theta = parseFloat(opt.theta || 0);
const lastPrice = parseFloat(
opt.lastPrice || opt.close || 0);
const isCall = opt.side === 'CALL'
|| opt.symbol.includes('-C');
const premiumMove = lastPrice > 0
? ((Math.abs(delta) * spot * 0.01) / lastPrice) * 100
: 0;
let strength = 'LOW';
const isMajor = underlying === 'BTC' || underlying === 'ETH';
const distLimit1 = isMajor ? 2 : 1;
const distLimit2 = isMajor ? 3 : 1.5;
const distLimit3 = isMajor ? 5 : 2.5;
// Liquidity gate: reject illiquid options (fake gamma spikes)
const openInterest = parseFloat(opt.openInterest || 0);
const volume = parseFloat(opt.volume || 0);
if (openInterest < 20) continue; // Need real OI
if (volume < 1 && openInterest < 50) continue; // Very thin market
// Cap premiumMove to prevent garbage from near-zero lastPrice
if (lastPrice < 0.01 && premiumMove > 500) continue;
if (dte <= 1 && distPct < distLimit1) strength = 'EXTREME';
else if (dte <= 2 && distPct < distLimit2) strength = 'HIGH';
else if (dte <= 3 && distPct < distLimit3) strength = 'MEDIUM';
const expiryLabel = dte <= 1 ? 'Π‘ΠΠΠΠΠΠ―/ΠΠΠΠ’Π Π' : `ΡΠ΅ΡΠ΅Π· ${Math.round(dte)} Π΄Π½.`;
const moveLabel = premiumMove > 0 ? ` ΠΠ²ΠΈΠΆΠ΅Π½ΠΈΠ΅ 1% ΡΠΏΠΎΡΠ° β ${premiumMove.toFixed(0)}% ΠΏΡΠ΅ΠΌΠΈΠΈ!` : '';
const typeEmoji = isCall ? 'π' : 'π';
const descs = {
EXTREME: `β‘ ΠΠΊΡΠΏΠΈΡΠ°ΡΠΈΡ ${expiryLabel}! Π¦Π΅Π½Π° Ρ ΡΡΡΠ°ΠΉΠΊΠ° (${distPct.toFixed(1)}%).${moveLabel}`,
HIGH: `${typeEmoji} ΠΠΊΡΠΏΠΈΡΠ°ΡΠΈΡ ${expiryLabel}. ΠΠ»ΠΈΠ·ΠΊΠΎ ΠΊ ΡΡΡΠ°ΠΉΠΊΡ (${distPct.toFixed(1)}%).${moveLabel}`,
MEDIUM: `${typeEmoji} ΠΠΊΡΠΏΠΈΡΠ°ΡΠΈΡ ${expiryLabel}. Π£ΠΌΠ΅ΡΠ΅Π½Π½Π°Ρ Π³Π°ΠΌΠΌΠ°. Π₯ΠΎΡΠΎΡΠΈΠΉ R:R Π΄Π»Ρ Π½Π°ΠΏΡΠ°Π²Π»Π΅Π½Π½ΠΎΠΉ ΡΡΠ°Π²ΠΊΠΈ.`,
LOW: 'Π‘Π»Π°Π±ΡΠΉ Π³Π°ΠΌΠΌΠ°-ΡΡΡΠ΅ΠΊΡ.',
};
results.push({
symbol: opt.symbol, underlying,
expiry: expiryStr, strike,
type: isCall ? 'CALL' : 'PUT',
dte, spotPrice: spot,
distancePercent: parseFloat(distPct.toFixed(2)),
moneyness: distPct < 1 ? 'ATM'
: (strike > spot ? 'OTM' : 'ITM'),
lastPrice, gamma, delta, theta,
iv: opt.markIV ? parseFloat(opt.markIV) : null,
volume: parseFloat(opt.volume || 0),
premiumMoveFor1Pct:
parseFloat(premiumMove.toFixed(1)),
signal: {
strength,
description: descs[strength]
},
});
}
results.sort((a, b) => b.gamma - a.gamma);
return results.slice(0, limit);
}
module.exports = { findGammaPlays, parseDte };