← Back
function getDte(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));
  return Math.max(0, Math.ceil((expDate - Date.now()) / 86400000));
}

function findUnusualVolume(optionsData, spotPrices, cfg = {}) {
  const { minVoiRatio = 3, minVolume = 1, limit = 30,
    underlying = null, minPremiumUsd = 5000 } = cfg;
  const results = [];
  for (const opt of optionsData) {
    if (underlying && !opt.symbol.startsWith(underlying))
      continue;
    const volume = parseFloat(opt.volume || 0);
    const oi = parseFloat(opt.openInterest || opt.oi || 0);
    if (volume < minVolume) continue;
    if (oi < 20) continue; // Liquidity gate: skip thin OI
    const voiRatio = oi > 0 ? volume / oi : 0;
    if (voiRatio < minVoiRatio) continue;
    const parts = opt.symbol.split('-');
    const asset = parts[0];
    const expiry = parts[1];

    // Phase 8: Strict DTE for Volume signals (avoid Theta burn, require 7-30 days)
    const dte = getDte(expiry);
    if (dte === null || dte < 7 || dte > 30) continue;

    const strike = parseFloat(parts[2] || opt.strikePrice);
    const spot = spotPrices[asset] || 0;
    const lastPrice = parseFloat(opt.lastPrice || opt.close || 0);
    const premiumUsd = lastPrice * volume;
    if (premiumUsd < minPremiumUsd) continue;

    const isCall = opt.side === 'CALL'
      || opt.symbol.includes('-C');
    const isItm = isCall ? spot > strike : spot < strike;
    const distPct = spot > 0 ? ((strike - spot) / spot) * 100 : 0;
    let severity = 'LOW';
    if (voiRatio > 10) severity = 'EXTREME';
    else if (voiRatio > 5) severity = 'HIGH';
    else if (voiRatio > 3) severity = 'MEDIUM';
    let direction = isCall ? 'BULLISH' : 'BEARISH';
    let confidence = 'MEDIUM';
    if (isItm) {
      confidence = 'HIGH';
      direction = isCall ? 'STRONG_BULLISH' : 'STRONG_BEARISH';
    }
    if (premiumUsd > 100000) confidence = 'HIGH';
    const typeLabel = isCall ? 'Call' : 'Put';
    const biasLabel = isCall ? 'рост 📈' : 'падение 📉';
    const descs = {
      EXTREME: `🐋 Экстремальная активность! Объём ${voiRatio.toFixed(0)}× от OI. Крупная ставка на ${biasLabel} ($${premiumUsd.toLocaleString('en-US', {maximumFractionDigits: 0})}).`,
      HIGH: `🐋 Высокая активность ${typeLabel}. V/OI ${voiRatio.toFixed(1)}×. Премия $${premiumUsd.toLocaleString('en-US', {maximumFractionDigits: 0})}. Ставка на ${biasLabel}.`,
      MEDIUM: `📊 Заметная активность ${typeLabel}. V/OI ${voiRatio.toFixed(1)}×. Кто-то набирает позицию.`,
      LOW: `📊 Умеренная активность ${typeLabel}. V/OI ${voiRatio.toFixed(1)}×.`,
    };
    results.push({
      symbol: opt.symbol, underlying: asset, strike,
      type: isCall ? 'CALL' : 'PUT', expiry: parts[1],
      volume, openInterest: oi,
      voiRatio: parseFloat(voiRatio.toFixed(2)),
      lastPrice,
      premiumUsd: parseFloat(premiumUsd.toFixed(2)),
      spotPrice: spot, moneyness: isItm ? 'ITM' : 'OTM',
      distancePercent: parseFloat(distPct.toFixed(2)),
      delta: opt.delta ? parseFloat(opt.delta) : null,
      iv: opt.markIV ? parseFloat(opt.markIV) : null,
      signal: {
        severity, direction, confidence,
        description: descs[severity]
      },
    });
  }
  results.sort((a, b) => b.voiRatio - a.voiRatio);
  return {
    count: Math.min(results.length, limit),
    totalFound: results.length,
    data: results.slice(0, limit)
  };
}
module.exports = { findUnusualVolume };

📜 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
e83dc9bfeat(options): Phase 8 - Implement Greek filters, Smart Sizing, and Weekend Traps4 months ago
64464bcfix(analysis): enforce absolute USD minimum premium to filter fake unusual volume spikes4 months ago
88df106fix(v2): fix breakeven math per coin and deflate unusual volume premium4 months ago
163bb5dfeat: migrate to options-screener-v2 folder to isolate deployment4 months ago
Show last diff
Loading...