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 };
📜 Git History
e841599feat: expired options tracking + exercise history + exit reason fix3 months ago
612398cfeat: signal reliability overhaul — OI data, liquidity gate, realistic PnL tracking3 months ago
06783dafix: options-screener-v2 — 9 bug fixes, 5 strategy improvements, 2 infra enhancements3 months ago
7c09bbefeat(ui): implement deep linking to scroll and highlight specific signal from push notification payload4 months ago
163bb5dfeat: migrate to options-screener-v2 folder to isolate deployment4 months ago
Show last diff
Loading...