Scan Cycle

Full Scan Cycle

Runs every SCAN_INTERVAL_SEC = 30 s during market hours (9:16 – 15:20 IST, weekdays only). is_market_hours() returns False on Saturdays and Sundays — no scans, hourly Telegram reports, or strategy actions run on weekends.

1. preflight() └── fetch fund limits · store available_balance for go-live margin checks · if first cycle with good balance → reconcile_positions() 2. check_kill_switch() └── sum realised + unrealised P&L · if ≤ −₹10,000 → halt all new trades 3. monitor_open_positions() └── BOX: expired? near-expiry? profit target hit? → close └── CAL: |z-score| ≤ 0.5 → close └── DC: 3-trigger stop · 32% debit profit target · near-expiry 15:00 IST time stop → close └── DCS: same triggers · 42% debit profit target · near-expiry 15:00 IST time stop → close └── AI_MARKET: target · stop · EOD close for the dry-run option leg → close 4. Operator safety audit + reconciliation auto-healer └── audit_positions() compares tracked trades with broker positions └── ReconciliationService removes confirmed-flat trades after 2 cycles, adopts safe orphan shapes, and blocks strategies with pending quantity mismatches 5. Broker health guard └── BrokerHealthMonitor samples API health; DOWN skips new trade scans after monitor/reconciliation, DEGRADED/UNSTABLE force dry-run entries 6. IVSampler.sample() └── records one IV surface snapshot: near/mid/far ATM term structure, bounded smile, call/put IV, skew, straddles, DTEs, and realized range └── estimates smile IV from option LTP when Dhan omits per-leg IV, while keeping raw IV when provided └── runs after broker health and before paused_today so IV history continues while trading is paused; skipped when broker health is DOWN 7. RiskEnvelope.check_envelope() └── after exits/reconciliation/health/IV sampling, block new-entry scans unless the envelope is HEALTHY └── enforces daily kill, weekly/monthly/lifetime drawdown caps, loss-streak pause, and logs [ENVELOPE] each cycle 8. AI market jobs [09:05 / 09:20 / 15:25 IST] └── premarket thesis skips when disabled, missing OpenAI key, NSE holiday/weekend, or same-day premarket row already exists └── live confirmation reuses the premarket row, verifies live spot/option-chain context, and can create the dry-run AI_MARKET trade └── scoring uses the day’s IV snapshots to write directional precision, Brier score, and excursion stats back to SQLite 9. Pause + total open-trade cap └── /stop_today skips new entries after monitoring; MAX_CONCURRENT_OPEN_TRADES skips the rest of the scan when the book is full 10. AI new-entry gate └── if enabled, block new option entries when today's verdict is missing, NO_TRADE, or BEARISH 11. Strategy slot guard └── ExecutionEngine allows at most 2 dc_family positions, blocks exact duplicate legs, and persists runtime_state.strategy_slots; MDC's second slot also requires controlled scale-in gates 12. MasterDCScanner.scan(open_trades) └── EventCalendar and reconciliation guards run before the MDC scanner └── classifies regime each cycle, delegates to DC / DCS / DCS_SKEW / DDC / IC via registry.get(sub) over the eligible set, and stores the trade as MDC_* 13. IronCondorScanner.scan(open_trades, realized_range) └── EventCalendar and reconciliation guards run before the IC fallback scanner └── fallback only if no primary options trade is open, including MDC_*; range-bound credit strategy; low IV percentile raises the minimum credit floor 14. Log summary Orders=N Open=M P&L=₹X
Registry-driven dispatch (W5: all live strategies) For DC, DCS, DCS_SKEW, DDC, IC, and AI_MARKET (including MDC_*-tagged trades), monitor/close/P&L/leg-ID/explain/icon dispatch runs entirely through StrategySpec.lifecycle — leg IDs, mark-to-market, close costs, and the exit decision come from arb_bot/strategies/<name>.py. The legacy BOX/CALENDAR/RECOVERY/AI_MARKET elif chains in engine.py were deleted in W5, completing the WP2 strangler migration. MDC routes to its sub-strategies via registry.get(sub) over the eligible set (any registered strategy with a scanner factory). Parity is locked by tests/execution/test_dc_monitor_parity.py, test_dcs_monitor_parity.py, test_ddc_monitor_parity.py, test_ic_monitor_parity.py, and test_ai_market_monitor_parity.py. One known cosmetic delta: MDC_IC close notifications are now labelled MDC_IC (previously IC); routing, P&L, and metrics keys are unchanged.
Same-cycle re-entry guard run_scan_cycle() snapshots DC and DCS trade counts before calling monitor_open_positions(). If a real-order strategy trade was closed this cycle, the corresponding scanner is skipped — preventing an immediate re-entry in the same 30-second window. Dry-run strategies may re-enter because no broker capital was used.
Go-live state gate The old *_DR_RUN config flags seed the SQLite records only on first run. After that, scan-cycle execution uses GoLiveManager.get_state(). SHADOW and LIVE both place real orders; SHADOW additionally records a SHADOW_DR_* metrics snapshot.
Scheduled jobs outside scan cycle AI market analysis (09:05 IST), SQLite token reload (5m), hourly Telegram P&L report (9–15 IST), EOD report (15:35 IST), and the operator-run make fetch-events market-event refresh all live outside the 30-second scan loop. Token renewal itself runs in the separate token_refresher.py service. The hourly report skips silently on weekends. Metrics checkpoints and token reloads run unconditionally.
Historical replay preserves cycle order The backtesting engine (arb_bot/backtest/simulation/engine.py) reproduces this same monitor-before-scan ordering for every historical trading day. Each day it patches now_ist() and the validated Config overrides to the simulated instant, first evaluates exits on open simulated trades (calendar state updated before exit checks), then runs the real scanner against the surviving open trades. Any trade still open after the last replayed day is force-closed with reason END_OF_RANGE. The scanner instance is created once and reused across all days, exactly mirroring the live bot.