← Back
const { fetchOptions, fetchGreeks, fetchSpotPrices, fetchOpenInterest, fetchContractUnits } = require('./binance');
const { cache } = require('./cache');
const config = require('../config');
const { saveAtmIvSnapshots, buildIvContexts } = require('./ivHistory');
const { processSignals } = require('./alerts');
const { checkCustomAlerts } = require('./alertChecker');
const { logSignals } = require('./signalLogger');
const { trackOutcomes } = require('./outcomeTracker');
const { analyzeAllStrategies } = require('../analysis/strategies');
const { findUnusualVolume } = require('../analysis/unusualVolume');
const { findGammaPlays } = require('../analysis/gammaPlay');
const { analyzeSkew } = require('../analysis/ivSkew');
const { fetchSpotTrends, trendLabel } = require('../analysis/spotTrend');
const { mergeGreeksIntoOptions } = require('../utils/mergeGreeks');
const { monitorTrades, openTradeWithTP, getTrades, clampPrice, getMaxPrice } = require('./tradeManager');
const { getPositions } = require('./trading');
const prisma = require('./db');
const logger = require('../utils/logger');

let broadcastFn = null;
let recentSignalIds = new Set();
let fastTimer = null;
let slowTimer = null;

function setBroadcast(fn) {
  broadcastFn = fn;
}

// ─── FAST CYCLE (every 30s) ───────────────────────────────
// Fetches fresh prices + Greeks, updates cache, broadcasts to WS clients
// Lightweight — no DB writes, no heavy analysis
async function fastUpdate() {
  const start = Date.now();
  try {
    const [options, greeks, spotPrices] = await Promise.all([
      fetchOptions(),
      fetchGreeks(),
      fetchSpotPrices(),
    ]);

    // Fetch OI separately (needs options for expiry list)
    if (options) {
      try {
        const oiMap = await fetchOpenInterest(options);
        for (const opt of options) {
          if (oiMap[opt.symbol] !== undefined) {
            opt.openInterest = oiMap[opt.symbol];
          }
        }
      } catch (err) {
        logger.error(`OI merge error: ${err.message}`);
      }
    }

    if (options) cache.options = options;
    if (greeks) {
      cache.greeks = {};
      greeks.forEach(g => { cache.greeks[g.symbol] = g; });
    }
    if (spotPrices) cache.spotPrices = spotPrices;
    cache.lastUpdate = new Date().toISOString();

    if (broadcastFn) broadcastFn();

    // Monitor active trades (check fills → place TP)
    try {
      await monitorTrades();
    } catch (err) {
      logger.error(`Trade monitor error: ${err.message}`);
    }

    const ms = Date.now() - start;
    logger.info(`Fast update done (${ms}ms) — ${(options || []).length} options`);
  } catch (err) {
    logger.error(`Fast update error: ${err.message}`);
  }
}

// ─── SLOW CYCLE (every 5 min) ─────────────────────────────
// Heavy tasks: IV snapshots to DB, strategy analysis, signal alerts
// Runs after a fast update so data is fresh
async function slowUpdate() {
  if (!cache.options) return;

  const start = Date.now();
  let spotPrices;
  try {
    spotPrices = cache.spotPrices || await fetchSpotPrices();
  } catch (err) {
    logger.error(`slowUpdate fetchSpotPrices error: ${err.message}`);
    return;
  }

  const data = mergeGreeksIntoOptions(cache.options, cache.greeks);

  // Save ATM IV snapshots for real IV Rank history
  try {
    await saveAtmIvSnapshots(data, spotPrices);
  } catch (err) {
    logger.error(`IV snapshot error: ${err.message}`);
  }

  // Build real IV contexts (from DB history) and collect signals for alerts
  try {
    const [ivContexts, spotTrends] = await Promise.all([
      buildIvContexts(data, spotPrices),
      fetchSpotTrends(),
    ]);
    cache.spotTrends = spotTrends; // Cache for API access
    const strategySignals = analyzeAllStrategies(data, spotPrices, ivContexts, 'ALL', spotTrends);

    // Also collect unusual-volume and gamma-play signals
    const uvResult = findUnusualVolume(data, spotPrices, { minVoiRatio: 5, limit: 12 });
    const uvSignals = uvResult.data
      .filter(item => item.signal.severity === 'HIGH' || item.signal.severity === 'EXTREME')
      .map(item => {
        const isBull = item.signal.direction.includes('BULL');
        const biasEmoji = isBull ? '📈' : '📉';
        const biasLabel = isBull ? 'рост' : 'падение';
        const premiumFmt = item.premiumUsd >= 1000 ? `$${(item.premiumUsd / 1000).toFixed(0)}K` : `$${item.premiumUsd.toFixed(0)}`;
        const trend = spotTrends[item.underlying];
        const trendInfo = trend ? ` Спот ${trend.change4h >= 0 ? '+' : ''}${trend.change4h}% (4ч).` : '';
        return {
          id: `unusual_vol_${item.symbol.replace(/-/g, '_').toLowerCase()}`,
          strategy: 'Unusual Volume',
          signal: item.signal.direction,
          underlying: item.underlying,
          direction: isBull ? 'BULLISH' : 'BEARISH',
          confidence: item.signal.severity === 'EXTREME' ? 90 : 75,
          severity: item.signal.severity,
          parameters: {
            symbol: item.symbol,
            voiRatio: item.voiRatio,
            costUsd: item.lastPrice,
            delta: Math.abs(item.delta || 0.5).toFixed(3),
            totalWhalePremium: item.premiumUsd,
            spot: item.spotPrice,
            ...(trend ? { spotTrend: { bias: trend.bias, change1h: trend.change1h, change4h: trend.change4h } } : {}),
          },
          description: `🐋 ${item.underlying} — кит вложил ${premiumFmt} в ${item.type} $${item.strike} (объём ${item.voiRatio.toFixed(0)}× от OI).${trendInfo} Крупная ставка на ${biasLabel}. Подтверди спотом перед входом.`,
          rationale: `🐋 Кит влил ${premiumFmt} в ${item.type} ${item.underlying}.${trendInfo} V/OI ${item.voiRatio.toFixed(1)}×. Ставка на ${biasLabel}.`,
          tooltip: `🐋 КИТОВЫЙ СЛЕД: ${premiumFmt} влито в ${item.type} ${item.underlying} $${item.strike}.\nV/OI ${item.voiRatio.toFixed(1)}× — объём в ${item.voiRatio.toFixed(0)} раз превышает OI.${trendInfo}\n⚠️ Не копируй слепо — используй как подтверждение!`,
        };
      });

    const gammaPlays = findGammaPlays(data, spotPrices, { maxDte: 3, maxDistancePercent: 5, limit: 20 });
    // Dedup: keep only best gamma per underlying+expiry+strike (no CALL+PUT duplicates)
    const gammaDeduped = new Map();
    for (const g of gammaPlays) {
      if (g.signal.strength !== 'EXTREME' && g.signal.strength !== 'HIGH') continue;
      const key = `${g.underlying}_${g.expiry}_${g.strike}`;
      const existing = gammaDeduped.get(key);
      if (!existing || g.gamma > existing.gamma) {
        gammaDeduped.set(key, g);
      }
    }
    const gammaSignals = [...gammaDeduped.values()].slice(0, 5).map(g => {
        const expiryLabel = g.dte <= 1 ? 'СЕГОДНЯ' : `через ${Math.round(g.dte)} дн.`;
        const isCall = g.type === 'CALL';
        const strikeFmt = g.strike >= 100 ? `$${Math.round(g.strike).toLocaleString('en-US')}` : `$${g.strike}`;
        const moveLabel = g.premiumMoveFor1Pct > 0 ? ` При движении 1% спота премия +${Math.round(g.premiumMoveFor1Pct)}%.` : '';
        const trend = spotTrends[g.underlying];
        const trendInfo = trend ? ` Спот ${trend.change1h >= 0 ? '+' : ''}${trend.change1h}%/1ч, EMA ${trend.emaDirection === 'UP' ? '↑' : trend.emaDirection === 'DOWN' ? '↓' : '→'}.` : '';

        // Trend filter: penalize if spot goes against direction
        let conf = g.signal.strength === 'EXTREME' ? 90 : 75;
        let trendWarn = '';
        if (trend) {
          const up = trend.emaDirection === 'UP' || trend.change1h > 0.3;
          const down = trend.emaDirection === 'DOWN' || trend.change1h < -0.3;
          if (isCall && down) { conf -= 15; trendWarn = ' ⚠️ Спот падает — CALL рискован!'; }
          else if (!isCall && up) { conf -= 15; trendWarn = ' ⚠️ Спот растёт — PUT рискован!'; }
          else if ((isCall && up) || (!isCall && down)) { conf += 5; }
        }

        const thetaVal = g.theta ? Math.abs(g.theta) : 0;
        const thetaWarn = thetaVal > 0
          ? `⚠️ Theta -$${thetaVal.toFixed(4)}/день`
          : `⚠️ Theta сжигает`;

        return {
          id: `gamma_play_${g.underlying.toLowerCase()}_${g.expiry}_${g.strike}`,
          strategy: 'Gamma Play',
          signal: `GAMMA_${g.signal.strength}`,
          underlying: g.underlying,
          direction: isCall ? 'BULLISH' : 'BEARISH',
          confidence: conf,
          severity: g.signal.strength,
          parameters: {
            symbol: g.symbol, dte: g.dte, gamma: g.gamma, theta: g.theta,
            delta: Math.abs(g.delta || 0.5).toFixed(3), costUsd: g.lastPrice,
            distancePercent: g.distancePercent,
            ...(trend ? { spotTrend: { bias: trend.bias, change1h: trend.change1h, change4h: trend.change4h } } : {}),
          },
          description: `⚡ ${g.underlying} ${g.type} ${strikeFmt} — экспирация ${expiryLabel}, цена ${g.distancePercent.toFixed(1)}% от страйка.${moveLabel}${trendInfo}${trendWarn} ${thetaWarn}`,
          rationale: `⚡ ${g.underlying} ${g.type} ${strikeFmt} — экспирация ${expiryLabel}! Цена ${g.distancePercent.toFixed(1)}% от страйка.${moveLabel}${trendInfo} Гамма взрыв возможен — закрывай быстро!`,
        };
      }).filter(s => s.confidence >= 70);

    // IV Skew signals (was missing from scheduler — only in dashboard.js)
    const sevToConf = { EXTREME: 90, HIGH: 75, MEDIUM: 58, LOW: 40 };
    const ivSkewSignals = [];
    for (const [asset, spot] of Object.entries(spotPrices)) {
      const skewResults = analyzeSkew(data, asset, spot);
      for (const s of skewResults) {
        if (s.signal !== 'BULLISH' && s.signal !== 'STRONG_BEARISH') continue;
        const isBullSkew = s.signal === 'BULLISH';
        const skewDir = isBullSkew ? 'Коллы дороже Путов — рынок ждёт роста' : 'Путы дороже Коллов — хеджирование/страх';
        const skewAction = isBullSkew ? 'Рассмотри покупку Call' : 'Рассмотри покупку Put или хеджирование';
        const spotFmt = spot >= 100 ? `$${Math.round(spot).toLocaleString('en-US')}` : `$${spot.toFixed(4)}`;
        const skTrend = spotTrends[asset];
        const skTrendInfo = skTrend ? ` Спот ${skTrend.change4h >= 0 ? '+' : ''}${skTrend.change4h}%/4ч, EMA ${skTrend.emaDirection === 'UP' ? '↑' : skTrend.emaDirection === 'DOWN' ? '↓' : '→'}.` : '';
        ivSkewSignals.push({
          id: `iv_skew_${asset.toLowerCase()}_${s.expiry}`,
          strategy: 'IV Skew',
          signal: s.signal,
          underlying: asset,
          direction: isBullSkew ? 'BULLISH' : 'BEARISH',
          confidence: sevToConf[s.severity] || 50,
          severity: s.severity,
          parameters: {
            skew25d: s.skew25d, putIv: s.putIv, callIv: s.callIv,
            putStrike: s.putStrike, callStrike: s.callStrike, expiry: s.expiry, spot,
            ...(skTrend ? { spotTrend: { bias: skTrend.bias, change1h: skTrend.change1h, change4h: skTrend.change4h } } : {}),
          },
          description: `📊 ${asset} ${spotFmt} — ${skewDir} (skew ${s.skew25d.toFixed(3)}).${skTrendInfo} ${skewAction}.`,
          rationale: s.description,
        });
      }
    }

    // ─── Auto-trade Gamma Plays before merging ─────────
    try {
      await autoTradeGammaPlay(gammaSignals, spotPrices);
    } catch (err) {
      logger.error(`Auto-trade gamma error: ${err.message}`);
    }

    const allSignals = [...strategySignals, ...uvSignals, ...gammaSignals, ...ivSkewSignals]
      .sort((a, b) => b.confidence - a.confidence);

    // Push notification for strictly new EXTREME signals
    const newSignals = allSignals.filter(s => !recentSignalIds.has(s.id) && s.confidence >= 90);
    if (newSignals.length > 0 && recentSignalIds.size > 0) {
      const best = newSignals[0];
      const { broadcastPushNotification } = require('../routes/notifications');

      // Build push notification: title = action, body = context + key numbers
      const trend = spotTrends[best.underlying];
      const spotNow = trend?.spot || best.parameters?.spot;
      const spotFmt = spotNow >= 100 ? `$${Math.round(spotNow).toLocaleString('en-US')}` : `$${spotNow?.toFixed(2) || '?'}`;
      const trendBit = trend ? `${trend.change4h >= 0 ? '+' : ''}${trend.change4h}%/4ч` : '';

      let pushTitle, pushBody;

      if (best.trade && (best.strategy === 'Buy Call' || best.strategy === 'Buy Put')) {
        const dir = best.strategy === 'Buy Call' ? '📈' : '📉';
        const strikeFmt = best.trade.strike >= 100 ? `$${Math.round(best.trade.strike).toLocaleString('en-US')}` : `$${best.trade.strike}`;
        pushTitle = `${dir} ${best.underlying} — ${best.trade.action} ${strikeFmt}`;
        pushBody = `Спот ${spotFmt} (${trendBit}). DTE ${best.trade.dte}д, TP +${best.trade.target.returnPct}%, SL -${best.trade.stopLoss.lossPct}%`;
      } else if (best.strategy === 'Gamma Play') {
        const strikePart = best.parameters?.symbol?.split('-')[2] || '';
        const dte = best.parameters.dte <= 1 ? 'СЕГОДНЯ' : `${Math.round(best.parameters.dte)}дн`;
        pushTitle = `⚡ ${best.underlying} — гамма скальп $${strikePart}`;
        pushBody = `Экспирация ${dte}, ${best.parameters.distancePercent?.toFixed(1) || '?'}% от страйка. Быстрый вход/выход, theta жжёт!`;
      } else if (best.strategy === 'Unusual Volume') {
        const premFmt = best.parameters?.totalWhalePremium >= 1000 ? `$${(best.parameters.totalWhalePremium / 1000).toFixed(0)}K` : `$${best.parameters?.totalWhalePremium?.toFixed(0) || '?'}`;
        pushTitle = `🐋 ${best.underlying} — кит ${premFmt}`;
        pushBody = `Спот ${spotFmt} (${trendBit}). V/OI ${best.parameters?.voiRatio?.toFixed(0) || '?'}×. Подтверди спотом перед входом`;
      } else if (best.strategy.includes('Straddle') || best.strategy.includes('Strangle')) {
        pushTitle = `🧊 ${best.underlying} — ${best.strategy}`;
        pushBody = `IV на дне (Rank ${best.parameters?.ivRank || '?'}). Опционы дёшевы — ставка на движение в любую сторону`;
      } else if (best.strategy === 'Weekend Trap') {
        pushTitle = `🌙 ${best.underlying} — Weekend Trap`;
        pushBody = `IV занижена (Rank ${best.parameters?.ivRank || '?'}). Покупка стрэддла — в понедельник IV расширится`;
      } else {
        pushTitle = `🔥 ${best.underlying} — ${best.strategy}`;
        pushBody = best.description?.substring(0, 120) || 'Новый сигнал — открой дашборд';
      }

      await broadcastPushNotification({
        title: pushTitle,
        body: pushBody,
        icon: '/pwa-192x192.png',
        data: { url: `/?signalId=${best.id}` }
      });
    }
    recentSignalIds = new Set(allSignals.map(s => s.id));

    await processSignals(allSignals);

    // ─── Log signals for backtest tracking ───────────
    try {
      await logSignals(allSignals, spotPrices);
    } catch (err) {
      logger.error(`Signal logging error: ${err.message}`);
    }

    // ─── Track outcomes of past signals ──────────────
    try {
      await trackOutcomes(spotPrices);
    } catch (err) {
      logger.error(`Outcome tracking error: ${err.message}`);
    }

    // ─── Custom Alerts check ─────────────────────────
    try {
      await checkCustomAlerts(spotPrices);
    } catch (err) {
      logger.error(`Custom alerts check error: ${err.message}`);
    }
  } catch (err) {
    logger.error(`Slow update error: ${err.message}`);
  }

  const ms = Date.now() - start;
  logger.info(`Slow update done (${ms}ms) — signals processed`);
}

// ─── AUTO-TRADE: Gamma Play ──────────────────────────────
// Backtest-validated filters (57 signals, 32W/24L = 57% WR):
//   conf>=90, DTE<=1d, delta 0.3-0.7, dist<1%, cost $0.50-$3/$5, no counter-trend
// Uses existing tradeManager for TP + monitoring

// Persistent dedup: check DB for recent trades on same underlying+strike+expiry
async function getRecentTradeSymbols() {
  const symbols = new Set();
  try {
    // In-memory active trades
    const memTrades = getTrades();
    for (const t of memTrades) {
      if (t.status !== 'CLOSED' && t.status !== 'ERROR') {
        symbols.add(t.symbol);
      }
    }
    // DB trades from last 24h (covers PM2 restarts)
    const cutoff = new Date(Date.now() - 24 * 3600 * 1000);
    const dbTrades = await prisma.tradeLog.findMany({
      where: { createdAt: { gte: cutoff }, source: 'auto_gamma' },
      select: { symbol: true },
    });
    for (const t of dbTrades) symbols.add(t.symbol);
  } catch (err) {
    logger.error(`[AUTO_GAMMA] Dedup query error: ${err.message}`);
  }
  return symbols;
}

async function autoTradeGammaPlay(gammaSignals, spotPrices) {
  const cfg = config.trading.autoGamma;
  if (!cfg.enabled || !config.trading.enabled) return;

  // Count open positions (Binance + in-memory pending)
  let openCount = 0;
  try {
    const positions = await getPositions();
    openCount = (positions || []).filter(p => parseFloat(p.positionAmt) > 0).length;
  } catch (err) {
    const active = getTrades().filter(t => t.status !== 'CLOSED' && t.status !== 'ERROR');
    openCount = active.length;
  }

  if (openCount >= cfg.maxPositions) {
    logger.info(`[AUTO_GAMMA] Skip — ${openCount}/${cfg.maxPositions} positions open`);
    return;
  }

  // Get already-traded symbols (in-memory + DB last 24h) for dedup
  const tradedSymbols = await getRecentTradeSymbols();

  // Filter signals by backtest-validated criteria
  const candidates = gammaSignals.filter(s => {
    if (s.confidence < cfg.minConfidence) return false;

    const p = s.parameters || {};
    const dte = parseFloat(p.dte) || 99;
    const delta = parseFloat(p.delta) || 0;
    const cost = parseFloat(p.costUsd) || 0;
    const distPct = parseFloat(p.distancePercent) || 99;
    const trend = p.spotTrend || {};
    const isCall = s.direction === 'BULLISH';
    const isMajor = cfg.majorAssets.includes(s.underlying);
    const maxCost = isMajor ? cfg.maxCostMajor : cfg.maxCostDefault;

    // Asset filter
    if (!cfg.assets.includes(s.underlying)) return false;

    // DTE: <= 1 day (24h). Above = 20% WR in backtest
    if (dte > cfg.maxDte) return false;

    // Delta 0.3-0.7: below = 45% WR, above = 50% WR. Sweet spot 56-66%
    if (delta < cfg.minDelta || delta > cfg.maxDelta) return false;

    // Distance from strike: > 1% = 0% WR in backtest
    if (distPct > cfg.maxDistancePct) return false;

    // Cost: < $0.50 = 45% WR (dust), > $25 = 0% WR. Per-asset cap.
    if (cost < cfg.minCostUsd || cost > maxCost) return false;

    // Counter-trend: 0% WR in backtest
    if (cfg.noCounterTrend && trend.bias) {
      const trendUp = trend.bias === 'BULLISH' || trend.bias === 'STRONG_BULLISH';
      const trendDown = trend.bias === 'BEARISH' || trend.bias === 'STRONG_BEARISH';
      if (isCall && trendDown) return false;
      if (!isCall && trendUp) return false;
    }

    // Dedup: don't re-enter same symbol (underlying+strike+expiry) within 24h
    const symbol = p.symbol;
    if (symbol && tradedSymbols.has(symbol)) {
      logger.info(`[AUTO_GAMMA] ⏭️ Dedup skip: ${symbol} — already traded in last 24h`);
      return false;
    }

    return true;
  });

  if (candidates.length === 0) return;

  // Sort by confidence desc, take what fits in position limit
  candidates.sort((a, b) => b.confidence - a.confidence);
  const slotsAvailable = cfg.maxPositions - openCount;

  for (let i = 0; i < Math.min(candidates.length, slotsAvailable); i++) {
    const sig = candidates[i];
    const p = sig.parameters || {};
    const symbol = p.symbol;
    const premium = parseFloat(p.costUsd) || 0;
    const isMajor = cfg.majorAssets.includes(sig.underlying);
    const maxCost = isMajor ? cfg.maxCostMajor : cfg.maxCostDefault;

    if (!symbol || premium <= 0) continue;

    // Clamp entry price to exchange maxPrice (lastPrice can exceed it)
    const entryPrice = clampPrice(symbol, premium);
    if (entryPrice <= 0) continue;

    // TP room check: skip if maxPrice doesn't allow at least 50% gain
    const maxPrice = getMaxPrice(symbol);
    if (maxPrice) {
      const maxGainPct = ((maxPrice / entryPrice) - 1) * 100;
      if (maxGainPct < 50) {
        logger.info(`[AUTO_GAMMA] ⏭️ Skip ${symbol}: maxPrice=$${maxPrice} only allows +${maxGainPct.toFixed(0)}% (need ≥50%)`);
        continue;
      }
    }

    // Calculate quantity: max budget / price, min 0.01
    let qty = Math.floor((maxCost / entryPrice) * 100) / 100;
    if (qty < 0.01) qty = 0.01;

    // Final cost check
    const totalCost = qty * entryPrice;
    if (totalCost > maxCost * 1.1) {
      qty = Math.floor((maxCost / entryPrice) * 100) / 100;
    }

    try {
      logger.info(`[AUTO_GAMMA] 🎯 Entering: ${symbol} qty=${qty} price=$${entryPrice} cost=$${(qty * entryPrice).toFixed(2)} conf=${sig.confidence} DTE=${p.dte} Δ=${p.delta} dist=${p.distancePercent}%`);

      const result = await openTradeWithTP({
        symbol,
        quantity: qty,
        price: entryPrice,
        tpPct: cfg.tpPct,
        source: 'auto_gamma',
      });

      tradedSymbols.add(symbol); // prevent duplicates within same scan cycle
      logger.info(`[AUTO_GAMMA] ✅ Order placed: orderId=${result.orderId} TP=$${result.tpPrice} (+${cfg.tpPct}%)`);

      openCount++;
    } catch (err) {
      const msg = err.response?.data?.msg || err.message;
      logger.error(`[AUTO_GAMMA] ❌ Failed: ${symbol} — ${msg}`);
    }
  }
}

// ─── LEGACY COMPAT ────────────────────────────────────────
// updateCache + updateAndBroadcast kept for any code that calls them directly
async function updateCache() {
  await fastUpdate();
}

async function updateAndBroadcast() {
  await fastUpdate();
  slowUpdate().catch(err => logger.error(`slowUpdate error: ${err.message}`));
}

// ─── START ────────────────────────────────────────────────
function start() {
  const fast = config.cache.fastInterval;
  const slow = config.cache.slowInterval;

  logger.info(`Scheduler starting — fast: ${fast / 1000}s, slow: ${slow / 1000}s`);

  // Load contract units (XRP=100, DOGE=1000) before first data fetch
  fetchContractUnits().then(units => {
    cache.contractUnits = units;
    logger.info(`[SCHEDULER] Contract units cached: ${JSON.stringify(units)}`);
  }).catch(err => {
    logger.error(`[SCHEDULER] Contract units fallback: ${err.message}`);
    cache.contractUnits = { BTC: 1, ETH: 1, SOL: 1, BNB: 1, XRP: 100, DOGE: 1000 };
  });

  // Initial run: fast + slow together
  fastUpdate().then(() => {
    slowUpdate().catch(err => logger.error(`Initial slowUpdate error: ${err.message}`));
  });

  // Fast cycle — prices always fresh
  fastTimer = setInterval(fastUpdate, fast);

  // Slow cycle — heavy analysis
  slowTimer = setInterval(() => {
    slowUpdate().catch(err => logger.error(`slowUpdate error: ${err.message}`));
  }, slow);
}

function stop() {
  if (fastTimer) clearInterval(fastTimer);
  if (slowTimer) clearInterval(slowTimer);
}

module.exports = { updateCache, updateAndBroadcast, start, stop, setBroadcast };

📜 Git History

7cbeeb2fix: auto-gamma dedup — persistent DB check + disable auto-trading9 weeks ago
e78fc62feat: auto-trading for Gamma Play + fix fake 100% WR bug10 weeks ago
36ce19afix: normalize option premium by contract unit (XRP=100, DOGE=1000)3 months ago
80bf7b6fix: add IV Skew signals to scheduler + align UV limit with dashboard3 months ago
e2929fcfeat: signal quality overhaul — descriptions, spot analysis, gamma filter, push notifications3 months ago
612398cfeat: signal reliability overhaul — OI data, liquidity gate, realistic PnL tracking3 months ago
7bd5a13feat: auto-TP trade manager + $5 position cap3 months ago
e12be00feat: Historical Signals Backtest tracking (Task 2.4)3 months ago
f8910e1feat: Custom Alerts system (Task 2.3)3 months ago
04f5515feat: split scheduler into fast (30s) and slow (5min) cycles3 months ago
Show last diff
Loading...