← Назад
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 };