Known Gotchas

Known Gotchas

DhanHQ v2 API quirks

QuirkFix in code
place_orderorderId is nested under resp["data"], not top-level DhanClient.place_order() unwraps resp["data"]
get_order_by_iddata can be a dict or list get_order_status() handles both shapes, logs WARNING if neither
availabelBalance — typo in Dhan's fund limits response Hard-coded key "availabelBalance" (with typo) in preflight()
Option chain — double-nested: resp["data"]["data"]["oc"] get_option_chain() unwraps both layers
Ticker data — response shape varies: flat resp["data"]["NSE_FNO"] or double-nested resp["data"]["data"]["NSE_FNO"] get_ltp() tries both shapes and returns flat {str(sec_id): price} dict regardless; also handles last_price vs ltp key names
Telegram getUpdates returns 409 Conflict when two bot instances are polling simultaneously (e.g. after market-close restart) TelegramCommandHandler backs off 60 s on 409 instead of logging an error every second — prevents log spam during dual-instance window

NSE Exchange Errors

ErrorCauseFix
EXCH:16283 Price not a multiple of tick size (₹0.05) tick_round() — BUYs round up, SELLs round down, then round(,2) clears float artifacts
EXCH:17070 Price outside LPP (circuit) range Fresh LTP fetch in _place_legs_ioc() — prevents stale price + slippage overshooting the band

Other edge cases

PART_TRADED_AND_CANCELLED NSE filled part of the IOC order. _poll_fill() treats this as "filled" — a position exists and must be tracked. If multiple legs hit this state, the box spread aborts and PartialFillRecovery runs: it classifies the filled legs by delta direction, keeps legs aligned with the market trend, and closes opposing legs at MARKET.
open_trades is in-memory + disk If the open_trades table in the SQLite database is cleared, the bot starts with no tracked positions. reconcile_positions() runs automatically at startup and alerts on any orphaned legs.
Config dry-run flags are not runtime switches anymore The *_DRY_RUN values in config.py only seed the SQLite records when they are missing. After first startup, use /shadow, /promote, and /revert. Editing config alone will not change a strategy already present in the state file.
Shadow mode is real-order mode SHADOW places broker orders just like LIVE. The difference is observability: the bot also writes a SHADOW_DR_* metrics snapshot for comparison. If margin is below strategy max × 1.10, the entry is blocked before orders are sent.
AI_MARKET is intentionally dry-run only The AI path stores market analysis and simulated option trades for review. Invalid JSON or an NSE holiday results in a skipped/error analysis row and never blocks DC/DCS/DDC/IC scanning. Do not treat an AI_MARKET verdict as live-order authorization.
Codex auth — local login required once CodexRunner shells out to codex exec. The Codex CLI must be authenticated before the bot starts (codex auth login once per machine). Set CODEX_HOME in .env if Codex is installed in a non-default location. The bot does not store or rotate an OpenAI API key — auth is entirely managed by the Codex CLI.
AI prompt data shape AI_MARKET, market-view, and breakout-risk prompts use web_search=False; they only analyze the JSON supplied by the bot. MarketDataBundle.to_prompt_dict() must keep emitting the normalized prompt blocks (gift_nifty, us_markets, technicals, options_data, and related macro groups). If that serializer falls back to raw bundle keys such as spot, pcr, or nested macro, Codex will treat critical fields as unavailable and the data-quality gate will force NO_TRADE.
NSE holiday cache fallback NSEHolidayCalendar caches holidays in SQLite. Weekends are always rejected; if a holiday list is unavailable, the AI job can still run on weekdays unless the cached table marks that date as closed.
DC / DCS same-cycle re-entry monitor_open_positions() removes a trade from open_trades in-memory. If the scanner ran in the same cycle it would see an empty list and open a fresh position immediately. Fixed by snapshotting counts before monitoring and skipping the scanner if its count dropped this cycle for a strategy that currently places real orders.