Class Map
ArbitrageBot ← main orchestrator | flags: paused_today, _day_closed
├── TelegramCommandHandler ← long-polls getUpdates; syncs Telegram app commands to default + configured chat scopes; dispatches /help /status /safety /positions /risk /explain_signal /audit_positions /confirm_recon /trades /journal /note /pnl /balance /report /logs /ai_market /rerun_ai_market /close_smart /remove_trade /stop_today /resume /kill /override_limit /reset_override plus /golive_status /confidence_report /shadow /promote /revert; backs off 60s on 409 conflict
│ ├── start() ← starts command-menu sync and spawns daemon poll thread on bot startup
│ ├── stop() ← signals poll loop to exit on bot shutdown
│ └── _dispatch() ← routes command string → handler; ignores non-CHAT_ID senders
├── TokenManager ← SQLite-backed Dhan access token lifecycle
│ ├── refresh_if_due() ← token_refresher.py renews near-expiry tokens via /v2/RenewToken or PIN+TOTP fallback
│ └── reload_from_store() ← bot reloads externally refreshed tokens from SQLite before SDK refresh
├── AutonomousMarketAnalyst ← scheduled 09:05 premarket thesis, 09:20 live confirmation, 15:25 scoring, and OTP-confirmed manual reruns; stores stage-linked rows, token cost, and optional dry-run trade
│ ├── run_analysis(manual_rerun=False) ← premarket thesis; skips when disabled, missing OPENAI_API_KEY, non-trading day, or same-day premarket row already exists
│ ├── run_confirmation(manual_rerun=False) ← live spot/option-chain confirmation; can create the dry-run trade from the confirmed setup
│ └── score_today_forecast() ← closes the daily loop with directional accuracy, Brier score, and excursion stats from intraday snapshots
├── TradeDebriefGenerator ← low-cost OpenAI post-trade debrief for closed trade journal rows; summarizes trade outcome, rejected-strategy delta, AI verdict outcome, and event risk
│ └── latest_direction_hint() ← returns same-day BULLISH/BEARISH verdict for DDC weak-trend tie break
├── AIRunner ← OpenAI Responses wrapper; loads autonomous-market and confirmation prompts, requests strict JSON, records input/output tokens and estimated USD/INR cost
├── NSEHolidayCalendar ← cached NSE holiday helper; trading-day gate for AI analysis
├── DhanClient ← SDK wrapper + rate limiting
│ ├── get_option_chain() ← respects OPTION_CHAIN_COOLDOWN (3.5 s)
│ ├── get_ltp() ← batch LTP via ticker_data
│ ├── place_order() ← unwraps v2.0.2 resp["data"], supports validity=IOC
│ └── get_order_status() ← handles dict AND list from v2 API
├── BoxSpreadScanner ← finds profitable box opportunities
│ └── scan(underlying) ← returns list sorted by net profit desc
├── CalendarSpreadScanner ← tracks near/far NIFTY futures spread
│ ├── _resolve_futures_ids() ← downloads scrip CSV, caches to disk
│ ├── scan() ← returns signal dict or None
│ └── get_current_z_score() ← used by ExecutionEngine for exit checks
├── DoubleCalendarScanner ← scans for ATM straddle double calendar entry (NIFTY weekly/monthly); IV must be 15–25%
│ ├── get_expiries() ← returns (near_expiry, far_expiry) tuple when DTE 3–7; None otherwise
│ ├── _next_tuesday() ← nearest Tuesday strictly after today (NIFTY weekly expiry)
│ ├── _last_tuesday_of_month() ← monthly expiry; rolls to next month if within 7d of near
│ └── scan(open_trades) ← fetches both chains, checks IV + premium filters → signal dict or None
├── StretchedDoubleCalendarScanner ← extends DoubleCalendarScanner; 8-leg DCS (ATM DC + OTM wings)
│ ├── get_expiries() ← overrides DC: DTE 7–10 window; looks ahead one week if nearest Tuesday is too close
│ ├── _calc_wing() ← wing = round(spot × IV/100 × √(DTE/365) × 0.52 / 50) × 50
│ └── scan(open_trades) ← 3-layer guard (count cap ≤ 3, one-per-day, ATM+expiry dedup); IV/straddle filters; → 8-leg signal or None
├── DirectionalDiagonalScanner ← extends DoubleCalendarScanner helpers; 2-leg directional diagonal calendar
│ ├── get_expiries() ← near weekly DTE 5–9 plus monthly far expiry
│ ├── _capture_day_open() ← stores first valid intraday spot reading as trend anchor
│ └── scan(open_trades, ai_direction_hint=None) ← trend threshold, optional AI weak-trend hint, IV, DTE, and OTM short-strike filters → DDC signal or None
├── IronCondorScanner ← extends DoubleCalendarScanner helpers; fallback 4-leg NIFTY weekly credit strategy
│ ├── get_expiry() ← near Tuesday only when DTE is in IC_DTE_MIN..IC_DTE_MAX
│ ├── scan(open_trades, realized_range) ← blocks when any primary options trade is open, including
MDC_*; checks IV, range, IVP-adjusted credit, strikes
│ └── _iv_bound() ← normalizes fractional IC config IV to percent convention
├── MasterDCScanner ← meta-strategy orchestrator; classifies regime each cycle and routes to the best DC-family scanner; tags trades MDC_* for persistence, exits, and reporting
│ ├── _classify() ← trending / skewed / wide / calm / IC-fallback regime selection
│ └── scan(open_trades) ← enforces MDC concurrency, fetches near-week chain, delegates, and prefixes strategy with MDC_
├── strategies/ ← strategy framework (arb_bot/strategies/) — W5 fully live; all monitor/close/P&L/leg-ID/explain/icon dispatch routes through the registry for every live strategy
│ ├── base.py ← Strategy interface: ScannerProtocol, TradeLifecycle, RiskProfile, StrategySpec
│ ├── registry.py ← central validated registry; resolve() handles MDC_* prefixes; GREEK_STRATEGIES/OPTION_RISK_STRATEGIES/icon maps auto-derived from registered specs
│ ├── dc.py ← DC spec (W3 pilot)
│ ├── dcs.py ← DCS + DCS_SKEW specs sharing one 8-leg lifecycle (W4)
│ ├── ddc.py ← DDC spec; ddc_pnl is already net → closing_cost 0 (W4)
│ ├── ic.py ← IC spec; exit rule converts net P&L ↔ credit value (W4)
│ ├── mdc.py ← MDC meta-spec: scan-entry dispatch only; trades resolve to sub-strategy specs; routes to sub-strategies via registry.get(sub) over the eligible set (any registered strategy with a scanner factory)
│ └── ai_market.py ← AI_MARKET spec (W5); AIMarketLifecycle + AIMARKET_SPEC; dry-run single-leg NIFTY option entry and exit migrated onto the registry
├── IVSampler ← records one IV surface snapshot per scan cycle: near/mid/far term structure, bounded smile with raw-or-estimated IV, ATM IV, skew, straddles, DTEs, and realized range
├── RiskEnvelope ← pre-scanner gate (Phase 1 autonomous roadmap); enforces six guards before any new trade scan: daily kill switch, weekly/monthly/lifetime DD caps, loss-streak pause, and lot-size lock for first 30 live days
│ ├── check_envelope() ← returns {status: HEALTHY|PAUSED|HALTED, reason, until}; called at start of run_scan_cycle()
│ ├── update_after_close(pnl) ← increments daily/weekly/monthly/lifetime counters and loss-streak after each trade close
│ ├── apply_pause() ← writes pause_until + pause_reason to risk_envelope_state; used by PAUSED triggers
│ ├── resume() ← clears pause and loss streak; called by /resume_envelope confirm Telegram command
│ └── lot_size_lock() ← returns 1 when <30 live days or lifetime P&L < LOT_UNLOCK_PROFIT; returns 0 to use strategy default
├── ExecutionEngine
│ ├── preflight() ← checks balance; triggers reconcile once
│ ├── GoLiveManager ← owns DRY_RUN / SHADOW / LIVE state, confidence gate, and 10% margin buffer checks
│ ├── reconcile_positions() ← compares open_trades vs Dhan; auto-adopts orphan box spreads; alerts remaining unmatched legs
│ ├── can_trade() ← kill switch + balance + daily limit + positions
│ ├── _place_legs_ioc() ← anchor to scanner bid/ask + 0.2% drift buffer; retry once at LTP+0.8%
│ ├── execute_double_calendar()← reads go-live state; dry-run simulates, shadow/live place real orders with margin check
│ ├── execute_stretched_double_calendar()← reads DCS/SDCS state; shadow records SHADOW_DR_* close snapshot
│ ├── execute_diagonal_calendar()← reads DDC state; dry-run simulates, shadow/live place far buy + near sell
│ ├── execute_iron_condor()← reads IC state; dry-run simulates, shadow/live place hedge buys before short sells
│ ├── execute_ai_market()← phase-1 AI_MARKET path; dry-run single NIFTY option trade only, mirrored in ai_market_trades
│ ├── _close_double_calendar() ← closes 4 DC legs; dry-run logs + records net P&L
│ ├── _close_stretched_double_calendar() ← closes 8 DCS legs; dry-run logs + records net P&L
│ ├── _close_diagonal_calendar() ← closes DDC near short and far long; dry-run logs LTPs
│ ├── _close_iron_condor() ← closes 4 IC legs; dry-run logs LTPs and records credit-spread P&L
│ ├── _close_ai_market() ← closes dry-run AI_MARKET option, updates open_trades and ai_market_trades
│ ├── monitor_open_positions() ← exit checks every 30s: BOX + CAL + DC + DCS + DCS_SKEW + DDC + IC + AI_MARKET; same-cycle re-entry blocked by bot.py. All active strategies (DC, DCS, DCS_SKEW, DDC, IC, AI_MARKET, incl. MDC_* tags) dispatch monitor/close/P&L/leg-IDs entirely through the strategy registry (arb_bot/strategies/); the legacy BOX/CALENDAR/RECOVERY/AI_MARKET elif chains were deleted in W5
│ ├── _get_nifty_spot() ← fetches NIFTY 50 IDX_I LTP; handles flat and segment-keyed responses
│ ├── check_kill_switch() ← halts if daily P&L ≤ KILL_SWITCH_LOSS
│ └── _save_trades() / _load_trades() ← compatibility wrappers around TradeStore
├── execution.persistence.TradeStore ← SQLite-backed open_trades load/save helper
├── execution.reconciliation.ReconciliationService ← scan-cycle auto-healer; removes confirmed-flat trades after 2 cycles, adopts known orphan strategy shapes, blocks strategies with pending qty mismatches, and writes reconciliation_audit rows
├── GoLiveManager ← SQLite-backed go_live_state load/save helper; confidence uses closed_trades table
├── execution.pnl ← pure P&L formulas for BOX, CALENDAR, RECOVERY, DC, DCS, DDC, IC, and AI_MARKET single-leg trades; active strategies (DC family + AI_MARKET) invoke P&L via spec.lifecycle through the registry
├── execution.double_calendar ← DC/DCS trade-record builders and close-leg specifications
├── execution.position_monitor ← DC/DCS/DDC/IC close-decision helpers with tested stop/target ordering
├── PartialFillRecovery ← triggered when _place_legs_ioc() aborts mid-spread
├── execute() ← classify, keep aligned legs, close opposing, arm trailing stop
├── predict_direction() ← two-gate: P2P ≥0.15% (Gate 1) + ≥60% tick consistency (Gate 2) → UP/DOWN/FLAT
├── _delta_sign() ← BUY CE=UP, SELL CE=DOWN, BUY PE=DOWN, SELL PE=UP
└── _fresh_atm_order() ← open ATM option in trend direction if no legs kept
├── greeks.py ← pure-Python Black-Scholes Greeks (no scipy); uses math.erf for normal CDF
│ ├── net_greeks_dc() ← Δ/Γ/Θ/ν for 4-leg DC at entry; stored as entry_greeks in trade dict
│ ├── net_greeks_dcs() ← Δ/Γ/Θ/ν for 8-leg DCS at entry; stored as entry_greeks in trade dict
│ └── _compute_live_greeks() ← live Greeks re-computed from current spot + IV for DC, DCS, and DCS_SKEW; shown in /positions command
└── MetricsCollector ← collects intraday data + safety events; shared via .metrics on all subsystems
├── record_box_scan() ← funnel: raw pairs → filtered → executed
├── record_cal_scan() ← funnel: signals → filtered → executed
├── record_execution() ← per-trade outcome: filled/aborted, legs filled, expected profit
├── record_close() ← expected vs actual P&L, holding hours, close reason
├── record_safety_event() ← low balance / drift / stale / duplicate / orphan counters
├── save_json() ← writes daily metrics, executions, and closed trades to SQLite
└── format_telegram() ← generates EOD Telegram report section
├── market/indices.py ← Performance Dashboard "Market Indices" tile grid (live NSE index LTP + day change% + 5-min sparkline) pushed over /ws/live on a decoupled timer
│ ├── MarketIndexService ← batches one Dhan ticker_data per refresh; paces uncached intraday calls, caches candles server-side (MARKET_INDEX_INTRADAY_CACHE_SEC), and emits the index frame independently of trade-monitoring ticks
│ └── IndexConfigStore ← persists the enabled-index list as JSON in the Postgres bot_settings table; read/written via GET/PUT /api/market/indices/config
Research subsystem (isolated)
arb_bot/research/strategies/ ← backtestable strategies; each
ResearchStrategy spec exposes scan(market_data) → signal and lifecycle for entry/exit/P&L
├── momentum_quality ← Blends 12M−1M price momentum z-score with quality fundamentals z-score (ROE gate, D/E gate); balances trending strength with financial health
├── quality_alpha ← Screens on ROE/ROCE/D/E/margin/FCF thresholds, ranks by composite quality z-score, confirms uptrend with 200-SMA filter
├── value_trend ← Value stocks (E/P, B/P, S/P, dividend yield ≥ universe median; min 2 of 4 metrics) confirmed by 200-SMA price trend
├── pair_statarb ← Market-neutral pairs engine (PairBacktestEngine); trades same-sector cointegrated spreads via z-score entry/exit, dollar-neutral sizing, daily short-borrow cost, and daily rebalance cadence
├── options_vol_premium ← Strategy #14 — sells defined-risk iron condors when IV-rank is elevated; uses OptionsVolEngine with EOD bhavcopy pricing and Black-Scholes fallback (arb_bot/research/strategies/options_vol_premium.py)
├── CombinedStrategy ← (arb_bot/research/strategies/combined.py) Blends N member strategies by z-scoring each member's composite score cross-sectionally and weighting the blend; no new factor logic — composes existing strategies for diversified factor exposure (Mode A composite)
├── earnings_pead ← Strategy #8 — enters on positive earnings surprise (surprise_pct ≥ RESEARCH_PEAD_MIN_SURPRISE); holds up to RESEARCH_PEAD_HOLDING_DAYS via carry-forward signals; exits via engine's "sell anything not targeted" path when aged out (arb_bot/research/strategies/earnings_pead.py)
└── event_driven ← Strategy #12 — enters on enabled corporate events (buyback, index_inclusion, pledge_reduction, block_deal); score = type_weight × value_num; holds up to RESEARCH_EVENT_HOLDING_DAYS; depends on manual event ingestion for non-earnings types (arb_bot/research/strategies/event_driven.py)
arb_bot/research/engine/options_vol_engine.py: OptionsVolEngine ← Options vol-premium backtest engine — iron condor entry/exit, IV-rank signal, EOD bhavcopy pricing; isolated from live trading; lazy-imports arb_bot.backtest.simulation.option_pricer only for Black-Scholes fallback
arb_bot/research/providers/ ← read-only data providers; never import arb_bot.client or place orders (enforced by tests/research/test_import_isolation.py + test_provider_safety.py)
├── dhan_data.py ← read-only Dhan historical-data accessor + instrument master for RESEARCH_DATA_PROVIDER=dhan; resolves symbols to security_ids and fetches OHLCV with no order surface
│ ├── DhanHistoricalData ← wraps the dhanhq SDK exposing ONLY read methods; builds its own SDK client from TokenManager credentials when none is injected
│ │ ├── historical_daily() ← Dhan /charts/historical daily OHLCV → date-indexed DataFrame (open/high/low/close/volume); provider callers normalize dates to YYYY-MM-DD before SDK calls
│ │ ├── intraday_minute() ← intraday minute OHLCV (default 5-min interval)
│ │ ├── ticker_data() ← batch last-traded-price read; grouped one call per segment; backs DhanProvider.get_ltp()
│ │ └── ohlc_data() ← snapshot OHLC read
│ └── InstrumentMaster ← lazily fetches Dhan's public compact security-list CSV; resolve(symbol) → (security_id, segment, instrument); accepts compact equity segment E while returning SDK segment NSE_EQ; benchmark aliases (^NSEI / ^NSEBANK / ^NSMIDCAP / ^CNXSML) map to IDX_I security_ids
├── dhan_provider.py: DhanProvider ← MarketDataProvider impl registered as "dhan"; resolves symbols via InstrumentMaster, fetches OHLCV via DhanHistoricalData, reuses the shared DB-backed cache (key includes the adjusted flag), and applies the AdjustmentProvider seam when adjusted=True
├── events_store.py ← upsert_events(rows) writes to research_events via ON CONFLICT … DO UPDATE; get_window(symbols, as_of_date, lookback_days) returns a dict of per-symbol event lists with no-lookahead gate (announce_date ≤ as_of_date)
└── adjustments.py ← corporate-action adjustment seam for raw Dhan OHLC; default NoOpAdjustmentProvider returns raw-equivalent prices until a real NSE corp-action source is wired
├── AdjustmentProvider ← ABC: get_adjustments(symbol, start, end) → list[Adjustment]
├── NoOpAdjustmentProvider ← default; returns [] (raw passthrough)
└── apply_adjustments(df, adjustments) ← back-adjusts pre-ex-date OHLC by each ratio (split+bonus combined); volume inverse-scaled
Interactive Architecture Explorer
Click any class to inspect its key methods. Press Trace Scan Cycle to animate the execution path.