Known Gotchas
DhanHQ v2 API quirks
| Quirk | Fix in code |
|---|---|
place_order — orderId is nested under resp["data"], not top-level |
DhanClient.place_order() unwraps resp["data"] |
get_order_by_id — data 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
| Error | Cause | Fix |
|---|---|---|
| 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.