N MarketGenie Knowledge Base

MarketGenie

AI-assisted intraday trading for Indian index options — a news-first edge, strict multi-level risk control, and a paper-gated path to live capital.

● M0–M8 complete · campaign-ready 541 tests passing Python ≥3.12 Upstox API shadow · paper · sandbox · live
73
modules mapped
541
tests passing
M0–M8
milestones done
6
strategies
2
Grafana dashboards

No sections match — try another term.

01Overview

Build a sound system first. The returns target is derived from measured paper-trading expectancy, never assumed. Success is defined by gates, not income.

MarketGenie runs a full Indian-market day unattended (except one daily token-approval tap), trading Nifty / Bank Nifty / Sensex options on the Upstox API. It pairs an LLM-driven news edge with a deterministic risk engine and an execution router that swaps between a paper fill-simulator, the Upstox sandbox, and live order placement by config alone.

Goal frame

Positive expectancy after costs over ≥80 trades / ≥6 weeks, then live capital starting at 20%.

Edge

News-first: announcements scored by a two-tier LLM, stress-tested by a bull/bear/judge debate before money moves.

Safety

Three-level kill switch, fractional Kelly sizing, an always-on hedge overlay, and a hard 15:12 square-off.

Four success gates (design objective)

  1. System runs a full market day unattended (except the daily token tap).
  2. Paper trading shows positive expectancy after costs over ≥80 trades / ≥6 weeks.
  3. Risk engine demonstrably halts trading — in drill and in anger.
  4. Only then: live capital, starting at 20%.

02Architecture

Three temporal phases, one execution router with four backends. Data flows top-to-bottom; the router is the single seam between strategy and broker. Gate ladder: shadow (record only, no orders) → paper (fill simulator) → sandbox (Upstox sandbox API, no fills) → live (real orders; requires NT_ALLOW_LIVE=1 env — never settable from YAML, so the self-reflection loop cannot enable live trading).

PRE-MARKET · 07:00–09:14 IST Pollers (asyncio) GIFT · NSE/BSE news FII/DII · VIX · global option chain · fault-isolated News Agent2-tier LLM → schema JSON Debate Layerbull ⇄ bear → judge Pre-market mapranked · persisted LIVE MARKET · 09:15–15:30 IST Feed V3 (websocket)Upstox → Fyers failover Technical AgentVWAP · RSI · OI-shift Signal Combinerweighted · gate ≥65% · VIX Risk EngineKelly sizer · H1 overlaykill L1 / L2 / L3 Execution Router paper (fill sim) · sandbox (validate) · live (V3, static IP) Position Monitortrail · target · 15:12 POST-MARKET · 15:30–17:00 IST SQLite Journal (WAL)signals · fills · costs Refine Agenthit-rate → weights Telegram ReportEOD + alerts
risk / router (critical seam) component cross-phase / async
Figure 1 — Three-phase architecture. Same code path feeds shadow, paper, sandbox, and live; only the router backend differs.

Cross-cutting design rules

1 · Paper is first-class

Identical code path to live up to the router. Fill simulation is pessimistic — crosses the spread, adds slippage.

2 · Runtime market metadata

Lot sizes, expiries, freeze qtys, margins read from the instrument master at startup. Zero hardcoded contract math.

3 · Fault isolation

Any poller can die without downing the system; the combiner treats a missing source as reduced confidence, not failure.

4 · Token-absent degradation

No token → full data + signal + paper pipeline still runs. Only live orders are unavailable.

5 · Provider-abstracted LLM

No component touches a vendor SDK. Anthropic→OpenAI→Gemini→Ollama is a config change.

6 · Every live order alerted

Telegram notification on every order in live mode.

The shared execution seam

trade_signal() is the single entry-to-flat path for any signal: kill-check → risk plan → entry → monitor → exit. Two callers reuse it, so the gap strategy and announcement strategy share one tested core.

run_day()          ──┐  derives S2 gap signal, then delegates
IntradaySession()  ──┼─►  trade_signal(cfg, sig, spot=…, sources=…, …)
                      │       kill-check → RiskEngine.plan → execute → PositionMonitor → exit
replay (SimClock)  ──┘  same loop, archived store instead of live feed

03Data flow

Two flows matter most: how a single announcement becomes (or doesn't become) a trade, and how the daemon orchestrates a whole day.

Signal pipeline — announcement → trade

Announce NSE/BSE poll Evidence +gap/FII/VIX News Agent LLM · 2-tier Combiner gate ≥65% Debate bull/bear/judge Risk + Exec size → fill Journal costed P&L SKIP ↓ drops here < gate ↑ rejected VETO ↓ killed
Figure 2 — A signal must survive SKIP, the ≥65% gate, and a possible VETO. Debate-revised confidence replaces raw confidence at the combiner. News→order p95 budget <20s.

Daemon day-cycle

non-trading day  → sleep to next 08:30
trading day:
  08:45  start capture  ─┐
  09:10  start trade ────┼─ run concurrently (asyncio.gather), each fault-isolated
         (gap + intraday)│   capture fills the store the IntradaySession polls
  14:30  last new intraday entry (ops.intraday_until)
  15:12  hard square-off
  15:35  capture ends
  15:45  EOD → render report + refine proposal → Telegram
  every loop: touch var/heartbeat   (healthcheck input; stale >5min = unhealthy)

The same IntradaySession loop serves live and replay. In replay, RecordedSources + SimClock serve an archived day deterministically — clock-bounded so no future data leaks into a decision. This symmetry is the M6 integration-validation guarantee.

04Decision flow

How a signal becomes a trade — or gets blocked. Two views: follow one signal through every gate, or watch a whole trading day on the clock. Pick a scenario to see the accept and block branches the code actually takes.

Pollersnews/FII/VIX/gap News agentfast → deep Debatebull/bear/judge Combiner≥0.65 · VIX Risk engineKelly · hedge · kill Executionpaper/sandbox/live BLOCKED — logged to journal AUDIT → decisions table (rationale_json)
Figure 3 — One signal's path. Nodes light as it advances; at the deciding node it either reaches Execution (green) or diverts to BLOCKED (red) with the reason code the orchestrator records. Every terminal branch — traded or blocked — is also written to the decisions audit table with its full rationale (M8b).
Press Play to trace the selected scenario.
07:00 → 17:00 IST
PRE-MARKET LIVE MARKET POST-MARKET 08:45capture 09:10trade start 14:30last entry 15:12square-off 15:45EOD report
Figure 4 — The daemon's day. Capture and trade run concurrently from 08:45/09:10; no new entries after 14:30; hard square-off 15:12; EOD report at 15:45.
Press Play to scrub the trading day.

05Strategy portfolio

IDStrategyStatusMechanism
S2GIFT-gap momentumLiveGIFT Nifty gap >0.6% vs prior NSE close → directional ATM option buy at open
S1News-catalyst optionsLiveAnnouncement → LLM → debate → plan; pre-open reference, spread filter, limit orders
S30DTE expiry momentumLive (paper)NIFTY weekly-expiry day (Tue) + time window + technical_signal → ATM today-expiry call/put; one entry/day; target/stop; 15:00 square-off
S4VIX-spike momentumLive (paper)India VIX ≥ spike_pct% above prior close → BUY_PE; latched, re-arm, daily entry cap
S5FII mega-flowBias filterT-1 EOD data → combiner directional weight, not a standalone trigger
H1Hedge overlayAlways-onRisk-layer enforced by the engine — see below

S3 and S4 are enabled by default in config/default.yaml (paper mode — no live orders); toggle s3.enabled / s4.enabled. See Execution modes for how mode gates order placement, and Configure for every knob.

H1 — Hedge overlay (enforced, not optional)
  1. Defined-risk by default — entries above ₹15K premium are debit spreads (buy ATM, sell 1–2 OTM), not naked longs.
  2. Portfolio delta cap — net delta bounded ±2 lot-equivalents; breach forces opposite-delta or auto-hedge.
  3. Event-day protection — RBI/budget/Fed days: protective leg or size halves.
  4. L2 hedge-then-halt — on L2 trigger, convert positions to defined-risk before halting new entries.
  5. Loss-narrowing adjustment — down 20% but thesis intact → roll the short leg, journal-logged.

Overlay cost/benefit is measured, not assumed: the journal carries a pnl_unhedged_counterfactual column (what the same-size naked long would have realized) on every spread close.

06Risk engine & kill switches

Fractional Kelly sizerkelly_fraction (0.25) × edge, capped at risk_per_trade_pct (1.5% of capital). Live win-rate/payoff from a rolling 30-day journal window; priors until 30 trades exist. Lot size and margin are runtime lookups.

LevelTriggerAction
L1 · tradeStop-loss hitClose the single position
L2 · sessionDaily drawdown ≥2.5% or 3 consecutive lossesHedge open positions → no new entries (latches for the day)
L3 · emergency/halt sentinel or API-error storm (5/60s)Cancel all → flatten → stop

The monitor polls the halt sentinel on a 1s tick, so /halt flattens paper positions in <5s (drill-measured 0.51s). /resume clears the file, but L2/L3 latches stay latched for the session.

07Execution modes

One pipeline, four interchangeable execution backends. trading.mode (or run --mode) picks the backend, and orchestrator.run_cli resolves it through make_router() before any signal is evaluated — so an unknown or unsafe mode is rejected at the entry gate, never mid-trade.

shadow record only · no orders paper fill simulator sandbox payload validation live real orders · NT_ALLOW_LIVE=1 SAFEST · zero execution risk REAL CAPITAL
Figure — the same decide path feeds every backend; only the final router differs. You climb the ladder only after the gates below it hold.
ModeWhat it doesOrdersRequires
shadowFull pipeline → decision → trade recorded tagged shadow. Validates news / data-source / decision quality on live data with zero execution.None, anywhere
paperPessimistic fill simulator vs live bid/ask (crosses the spread, adds slippage). The campaign mode.Simulated
sandboxRoutes real V3 order payloads to api-sandbox.upstox.com for shape validation; no fills exist there. CI / pre-live check.Validated, no fillsUPSTOX_SANDBOX_TOKEN
livePlaces REAL orders via Place Order V3. Every order is Telegram-alerted.Realtrading.mode=live + NT_ALLOW_LIVE=1 + usable live token
NT_ALLOW_LIVE is an environment-only gate — never settable from config/default.yaml. The self-reflection loop can edit YAML and code via PR but cannot set an env var, so no automated change can turn live trading on. Live needs all three — config mode live, the env flag, and a usable token; miss any one and make_router degrades to a non-placing mode.

make_router(mode, …) is the single factory; an unrecognised mode resolves to UNKNOWN_MODE and degrades safely (no orders placed). Live fill reconciliation and position monitoring against real fills is M7 — not yet wired; see Roadmap.

08Install

Requirements: Python ≥3.12, git. Docker only for containerized deploy.

git clone <repo-url> marketgenie && cd neurotrade
python3.12 -m venv .venv
.venv/bin/pip install -e ".[dev,fyers]"
aiohttp pin trap (read this)

fyers-apiv3 pins aiohttp==3.9.3, which breaks LiteLLM (ConnectionTimeoutError). After any pip install into the venv, re-pin — the Dockerfile does this automatically:

.venv/bin/pip install 'aiohttp>=3.10'

Verify:

.venv/bin/python -m pytest tests/ -q      # 541 passed
.venv/bin/ruff check src tests            # clean
.venv/bin/python -m marketgenie token-status

09Deploy

SEBI algo rules (live since 1-Apr-2026): order APIs work only from a registered static IP. A Mumbai VPS is mandatory for live trading; a laptop can read data and paper-trade but cannot place live orders.

Primary target. Two services share one var/ volume: daemon (capture ∥ trade → EOD, heartbeat healthcheck) and bot (Telegram long-poll). restart: unless-stopped survives reboot into data-only mode.

cp .env.example .env       # fill secrets
docker compose up -d --build

Also starts Prometheus (:9090) and Grafana (:3000, anonymous viewer) — see Monitoring.

One Deployment, two containers sharing one RWO PVC at /app/var. Daemon exposes containerPort: 9100 (metrics); pod has prometheus.io/scrape: "true" annotation.

helm install nt deploy/helm/neurotrade \
  --set secret.data.UPSTOX_API_KEY=... \
  --set secret.data.TELEGRAM_BOT_TOKEN=...

Fresh Debian/Ubuntu VPS → running system.

cd deploy/ansible
cp inventory.example.ini inventory.ini   # fill VPS host
cp vars.example.yml vars.yml             # fill secrets + repo url
ansible-playbook -i inventory.ini provision.yml -e @vars.yml   # provision
ansible-playbook -i inventory.ini deploy.yml  -e @vars.yml     # update

The Upstox access token expires 03:30 IST daily with no headless renewal. Each morning, run the OAuth login through an SSH port-forward so the browser stays on your laptop:

scripts/token_ritual.sh user@vps-host

The token lands in the VPS var/ volume. Without it, the system runs data-only by design.

10Use — CLI

python -m marketgenie <command>
CommandPurpose
loginInteractive Upstox OAuth; saves the daily token
token-statusPrint token usability
instrumentsRefresh the instrument-master cache
run [--mode]Run one trading day (gap + intraday); mode shadow·paper·sandbox·live (see §07)
daemonDay-cycle supervisor — the production process
capture [--until]Data-layer session capture only
report [--date]EOD report from the journal
botTelegram command bot (long-poll)
replay [--date --mock --limit]Replay a day through the M3 signal layer
replay-day [--date --mock]Full-day replay through every layer
drill --scenario spread|l2|l3Kill-switch / hedge drills (paper, no network)
sandbox-checkValidate V3 order payloads against the sandbox
db upgradeApply Alembic migrations (Postgres / fresh DB) — see Storage
reflect [--dry-run]EOD self-reflection agent → change-set PR (opt-in) — see §13
campaign --since [--until --audit-fills]Graduation-gate metrics + fill spot-audit
analyze <ticker>Analyze a stock using news, technicals, and analyst consensus framework

Telegram bot (configured chat only): /halt /resume /status /pnl /signals [ticker] (shows signals or analyzes ticker), /weights.

11Monitoring

docker compose up starts Prometheus (:9090) and Grafana (:3000, anonymous viewer) with two auto-provisioned dashboards. No extra config needed.

Metric scrape endpoints

/metrics is exposed in-process via prometheus_client. Both processes are scraped:

daemon · :9100

Trade path — signals, executions, kill-switch states, position counts, pipeline latency, feed heartbeat. Helm containerPort: 9100 + prometheus.io/scrape: "true".

bot · :9101

Telegram command bot — command counts, latency. Lightweight; same scrape interval.

Three metric families

Performance

Timing and throughput — p50/p95 latency, feed health, order round-trip.

nt_signal_pipeline_seconds nt_order_rtt_seconds nt_feed_heartbeat_age_seconds nt_capture_events_total
Functional

Business events — signals, trades, blocks. Labels carry strategy, direction, reason.

nt_signals_total{strategy,direction,outcome} nt_blocked_total{reason} nt_trades_total nt_positions_open
Decision thresholds

Live config values as gauges — plot actual vs limit side-by-side in Grafana.

nt_threshold_combiner_gate nt_threshold_delta_cap_lots nt_threshold_vix_calm nt_threshold_daily_drawdown_pct

Two Grafana dashboards

Functional dashboard

Trade/signal counts, blocked-entry reason breakdown, kill-switch state, position lifecycle, LLM latency and spend, VIX regime.

Performance dashboard

Signal-pipeline p50/p95 latency, feed heartbeat age, capture throughput, order round-trip time.

Grafana annotations from the journal

Each journal write triggers a Grafana annotation via the Annotations API:

trade executed

Green annotation — signal passed all gates, order placed (paper or live), journal row written.

blocked entries > 0

Red annotation — one or more signals cleared the combiner gate but were stopped by the risk engine.

blocked_entries — "could-have-traded" concept

The journal column blocked_entries counts signals that passed the ≥65% combiner gate but were blocked downstream by the risk engine or a kill switch. This is the "could-have-traded" signal: a non-zero value doesn't mean bad performance — it means the guard rails were engaged. Tracking it lets you distinguish a quiet market (no signals) from a gated market (signals, but blocked).

Gate reason codes recorded in nt_blocked_total{reason}:

KILL_L2 KILL_L3 SIZE_BELOW_MIN DELTA_CAP MAX_POSITIONS MODE_NOT_LIVE TOKEN_ABSENT LEG_FAILURE

Observability data-flow

daemon :9100 /metrics bot · :9101 /metrics Prometheus :9090 15s scrape Grafana :3000 Functional + Performance dashboards SQLite Journal blocked annotation (red) trade annotation (green)
Figure 3 — Prometheus scrapes both /metrics endpoints every 15s. Journal writes drive green (trade) and red (blocked) Grafana annotations directly via the Annotations API.

12Admin console & decision audit

A read-mostly operator console (app/admin_api.py, Material 3) over the journal, plus a fault-isolated decision-audit trail. Every signal — traded or blocked — is written to the decisions table with its full rationale, so nothing the system decides is invisible.

Console (M8c)

Overview

Live kill level, day PnL, open positions and consecutive losses — fed by app/status.py writing var/status.json.

Audit · trades · signals

Browse the decisions, trades and signals tables with reason codes and the LLM rationale behind each call.

Controls

halt · resume · clear-latch — the same kill-switch surface as the Telegram bot, behind NT_ADMIN_TOKEN.

Decision audit (M8b)

journal/audit.py safe_log_decision() never raises — an audit-write failure cannot break the trade path. A passing signal records entered; an entry that unwinds mid-flight records abort; a rejected one records why:

below_gate vetoed_debate no_gap_signal size_delta_gate kill_l2 kill_l3 leg_failure abort

size_delta_gate covers the size / delta-cap / max-positions rejections. Mode & token gating from the execution router surfaces as an upper-case block reason — LIVE_NOT_ENABLED, UNKNOWN_MODE, TOKEN_ABSENT, SANDBOX_TOKEN_ABSENT — on the same nt_blocked_total{reason} metric the dashboards aggregate.

13Self-reflection loop

Opt-in (reflect.enabled). The system proposes its own improvements — but a human and CI are always between a proposal and production.

At EOD the reflect agent (app/reflect.py) reviews the decision audit and the day's trades, then emits a validated change-set — small, evidence-backed edits to config knobs, code, or docs. Deterministic code (app/reflect_apply.py) applies it to a reflect/<date> branch and opens a Pull Request.

EOD audit + trades reflect agent change-set →reflect/<date> PR CI + human review⚠ banner on risk paths merge → deploy
Nothing merges without green CI and your approval — the merge then rides the normal release → deploy pipeline.

Market-grounded feedback

app/trade_review.py scores closed trades against what the market actually did afterwards and feeds that review back into the reflection prompt — so proposals are grounded in realised outcomes, not the model's priors alone.

Guard rails

  • Never auto-merges — requires green CI and your approval.
  • Protected paths (.github/, tests/, risk & execution code) get a ⚠ HIGH-RISK banner on the PR.
  • Cannot enable live tradingNT_ALLOW_LIVE is env-only (see Execution modes), outside the agent's reach.
  • Requires the REFLECT_PAT secret. Dry run: docker compose run --rm daemon reflect --dry-run.

14Guides

Ten task-focused walkthroughs — four lifecycle (setup → live), six per-strategy playbooks. Each follows the same shape: what the app does → the knobs that steer it → run / observe → verify. The system trades autonomously; these show how to tune and watch it, not how to hand-place trades.

Run your first paper-trade campaign

The default, capital-free path. Everything runs; orders fill against a pessimistic simulator instead of the exchange.

Prereqs
  • Docker + Docker Compose (or Python ≥3.12 for the non-compose path).
  • The repo cloned. An Upstox account (free), an Anthropic API account, a Telegram account.
Gather config
SecretWhere to get itGoes in
UPSTOX_API_KEY
UPSTOX_API_SECRET
Upstox developer console → create an app → set redirect URI to http://localhost:8721/callback. Copy the API key + secret..env
ANTHROPIC_API_KEYconsole.anthropic.com → API keys → create key. (Swap to OpenAI/Gemini/Ollama later by editing llm.providers — no key change needed if self-hosted.).env
TELEGRAM_BOT_TOKENTelegram → message @BotFather/newbot → copy the token..env
TELEGRAM_CHAT_IDMessage your new bot once, then open https://api.telegram.org/bot<TOKEN>/getUpdates and read chat.id (or use @userinfobot). The bot serves only this chat..env
Run it
cp .env.example .env            # paste the four secrets above
docker compose up -d --build    # daemon + bot + Prometheus + Grafana

# one-time + each trading morning: approve the Upstox token
docker compose run --rm --service-ports daemon login
#   (on a remote VPS, use scripts/token_ritual.sh user@vps-host instead — it
#    SSH-forwards localhost:8721 so the OAuth browser stays on your laptop)
docker compose run --rm daemon token-status     # expect: usable
docker compose run --rm daemon instruments        # cache the instrument master

# the daemon already runs the day cycle; to trigger a single day manually:
docker compose run --rm daemon run --mode paper

# after a few trading days, score the campaign:
docker compose run --rm daemon campaign --since 2026-06-14
Verify
  • token-status prints usable.
  • Paper fills appear in the SQLite journal; the Telegram EOD report arrives ~15:45 IST.
  • campaign prints expectancy + graduation-gate status. Grafana on :3000 shows live panels (Monitoring).

Promote a paper campaign to live capital

Real money. Only flip to live after a paper campaign has passed its graduation gates. Confirm the risk config (kelly_fraction 0.25, risk_per_trade_pct 1.5, daily_drawdown_pct 2.5) and that kill switches arm in a drill first.
Prereqs
  • A graduated paper campaign. A live-enabled Upstox trading account.
  • A VPS with a registered static IP — SEBI algo rules require order APIs to call from an allowlisted IP. A laptop can read data + paper-trade but cannot place live orders.
Gather config
SettingWhere to get itGoes in
Static IP allowlistYour VPS's fixed outbound IP → register it in the Upstox app/developer settings.broker portal
X-Algo-NameThe algo identity required on live Place Order V3 — set per your broker's algo registration.execution config
Live mode enabledUpstox app switched from sandbox to live trading.broker portal

Live trading reuses the same .env secrets as paper — no new keys.

Run it
# 1. validate V3 order payload shapes against the sandbox first
docker compose run --rm daemon sandbox-check

# 2. arm + prove the kill switches in paper before risking capital
docker compose run --rm daemon drill --scenario l2
docker compose run --rm daemon drill --scenario l3

# 3. set trading.mode: live in config/default.yaml, redeploy, then:
docker compose run --rm daemon run --mode live
Verify
  • sandbox-check passes (payloads valid).
  • Drills show L2/L3 halting new entries. Live order rate stays ≤ 10 orders/s.
  • Hard square-off fires at 15:12 IST; no entries after 14:30.

Run in data-only / research mode

Gather and replay market data without placing any orders — useful before you have a usable token, or for offline signal research. This is the system's default degraded mode (no usable Upstox token ⇒ data-only).

Prereqs
  • Nothing required to capture. To replay with the real LLM, set ANTHROPIC_API_KEY; otherwise use --mock (canned LLM, fully offline).
Gather config
SettingWhere to get itGoes in
UPSTOX_SANDBOX_TOKEN (optional)Upstox portal → a separate 30-day sandbox token, for sandbox-check payload validation. Not needed for capture/replay..env
Run it
# capture a live session's news/FII/VIX/chain to the archive
docker compose run --rm daemon capture --until 15:35

# replay a captured day through the M3 signal layer (offline, no orders)
docker compose run --rm daemon replay --date 2026-06-14 --mock --limit 25

# or replay a full day through every layer end-to-end
docker compose run --rm daemon replay-day --date 2026-06-14 --mock
Verify
  • Capture writes session files to the archive DB.
  • Replay prints per-announcement signal decisions (SKIP / gate / VETO outcomes) and places no orders.

Daily operation & tuning

The one daily human task, reading results, and adjusting source weights.

Prereqs
  • A deployed instance (paper or live). SSH access to the VPS (user@vps-host) for the remote token ritual.
Gather config

No new tokens — reuse the deployed .env. You need only SSH access to the host running the daemon.

Run it
# every morning — the Upstox token expires 03:30 IST with no headless renewal.
# this forwards localhost:8721 over SSH so the OAuth browser stays on your laptop:
scripts/token_ritual.sh user@vps-host

# read the day's results (also auto-sent to Telegram at 15:45):
docker compose run --rm daemon report --date 2026-06-14

# tune: the refine agent proposes source-weight changes post-market (never auto-applied).
# review the proposal, then edit the individual weights under signals: in
# config/default.yaml — weight_news / weight_technical / weight_fii — and redeploy.
Verify
  • After the ritual, token-status is usable for the day.
  • Telegram bot answers /status, /pnl, /weights. Grafana panels (daemon :9100, bot :9101) stay live.
  • Applied weight changes show up in the next day's combiner output.

Operate the gap-momentum strategy (S2 · live)

S2 reads the overnight GIFT Nifty gap versus the prior NSE close. If the absolute gap clears the threshold, the app buys a directional ATM option at the open and manages it to a fixed target/stop. You don't place it — you set the threshold and exits, then watch.

Knobs (in config/default.yaml under trading:)
KnobDefaultEffect / how to dial
gap_threshold_pct0.6Minimum |GIFT gap| to fire. Lower → more trades, more noise; raise → fewer, higher-conviction opens.
target_pct0.25Exit at +25% premium. Raise for fatter winners (lower hit-rate), lower to bank quicker.
stop_pct0.30Exit at −30% premium. Tighten to cut losers faster; widen to ride volatility.
Run / observe
# dry-run a paper day so you can see the gap decision
docker compose run --rm daemon run --mode paper

# read the day's outcome (also sent to Telegram ~15:45 IST)
docker compose run --rm daemon report --date 2026-06-14
# or ask the bot intraday:  /signals   /pnl   /status
Verify (audit log / admin console)
  • On a day with no qualifying gap: a decision with reason_code = no_gap_signal — the threshold filtered it out.
  • On a fired day: a decision with reason_code = entered at the open, then an EXIT decision (target/stop) later.
  • Tune one knob, re-run the same day, and watch the decision flip — that's the feedback loop.
Live = real money. Validate any threshold change across a paper campaign before promoting (see the Go live tab).

Operate the news-catalyst strategy (S1 · live)

S1 reacts to NSE/BSE announcements: an LLM analyses the news, a debate layer cross-checks it, and a combiner blends news + technical + FII signals into one confidence score. Above the gate it trades a plan (with a debit-spread filter and limit orders). You steer it by weighting the inputs and the gate.

Knobs (under signals:, and llm:, in config/default.yaml)
KnobDefaultEffect / how to dial
combiner_gate0.65Min blended confidence to trade. Raise to be more selective; lower to act on weaker catalysts.
weight_news / weight_technical / weight_fii0.55 / 0.30 / 0.15Blend weights (should sum to 1.0). Tilt toward weight_news for a stronger headline edge; toward technical for chart-led days.
immediate_skip_confidence0.80An IMMEDIATE-tier catalyst above this skips the debate step for speed.
fii_bias_threshold_cr500|Net FII flow| (₹cr) beyond which the FII input takes a direction.
llm.providers.news_fast / news_deephaiku-4-5 / sonnet-4-6Fast triage vs deep analysis models. Swap providers (OpenAI/Gemini/Ollama) here without touching code.
Run / observe
# replay a captured announcement day through the signal layer (offline, no orders)
docker compose run --rm daemon replay --date 2026-06-14 --mock --limit 25

# the refine agent proposes weight changes post-market (never auto-applied):
docker compose run --rm daemon report --date 2026-06-14   #   /weights on the bot
Verify (audit log)
  • A catalyst below the gate → decision with reason_code = below_gate.
  • A catalyst the debate layer rejects → reason_code = vetoed_debate (with the verdict recorded).
  • A trade → reason_code = entered; the decision row carries the news tier + blended confidence.
Weights are applied by you, not the agent. Review the refine proposal, edit the weights, redeploy — changes show in the next day's combiner output.

Operate the hedge & spread overlay (H1 · always-on)

H1 is a risk overlay, not a standalone strategy: it automatically converts an expensive naked option into a defined-risk debit spread, caps net portfolio delta, and halves size on event days. It applies to whatever S1/S2 trades.

Knobs (under risk: in config/default.yaml)
KnobDefaultEffect / how to dial
premium_threshold_inr15000Above this entry premium, buy a debit spread (buy ATM, sell 1–2 strikes OTM) instead of a naked option. Lower → spreads kick in sooner (cheaper, capped upside).
delta_cap_lots2.0Max net directional exposure in lot-equivalents. Lower for a more market-neutral book.
event_size_factor0.5Size multiplier on known event days (results/policy). 0.5 = half size. Lower to de-risk events further.
Run / observe
# prove the hedge/spread + kill behaviour in a paper drill (no capital)
docker compose run --rm daemon drill --scenario l2

# watch a real day choose naked vs spread by premium
docker compose run --rm daemon run --mode paper
docker compose run --rm daemon report --date 2026-06-14
Verify (audit log)
  • High-premium entry → the decision's plan shows kind = SPREAD (vs NAKED).
  • An entry blocked for breaching the delta cap → reason_code = size_delta_gate.
  • On an event day, the logged lot size is the halved figure.

Tune position sizing — VIX regime & Kelly (risk engine)

Sizing is deterministic: a fractional-Kelly sizer scales each trade by measured edge and capital-at-risk, the VIX regime bands adjust posture, and three hard limits trip the L2 session kill. This is how you set the overall aggression of the book.

Knobs (under risk: and signals: in config/default.yaml)
KnobDefaultEffect / how to dial
kelly_fraction0.25Fraction of full Kelly used. Raise for bigger size (more variance); lower to be conservative. Quarter-Kelly is the cautious default.
risk_per_trade_pct1.5Max capital risked per trade (% of capital_inr). The sizer never exceeds this.
daily_drawdown_pct2.5Realised daily loss that latches the L2 kill (blocks new entries). Lower for a tighter daily stop.
max_consecutive_losses3Losing streak that latches L2. Lower to stop sooner on a bad run.
max_open_positions3Concurrent open positions cap.
vix_calm / vix_elevated / vix_panic13 / 17 / 22India-VIX band edges that set the volatility regime feeding sizing/posture.
Run / observe
# prove the L2 / L3 kill switches latch in paper before trusting the limits
docker compose run --rm daemon drill --scenario l2
docker compose run --rm daemon drill --scenario l3
#   bot: /status shows kill level + halt;  admin console shows it live
Verify (audit log / admin console)
  • Hitting the drawdown or loss-streak limit → decisions with reason_code = kill_l2 (and kill_l3 on emergency/halt).
  • The admin console Overview shows the current kill level, day PnL, open positions, and consecutive losses — the live view of these limits.
Raising kelly_fraction or risk_per_trade_pct increases real drawdown. Re-run a paper campaign and confirm expectancy holds before going live.

Operate the 0DTE expiry scalp (S3 · live in paper)

On a NIFTY weekly-expiry day, inside the configured window, S3 reads technical_signal and buys an ATM today-expiry call or put in the signal's direction — a momentum scalp into the theta-rich close. One entry per expiry day (latched), squared off before settlement. Enabled by default in paper (s3.enabled: true).

Knobs (under s3: in config/default.yaml)
KnobDefaultEffect
enabledtrueToggle S3. Paper-only until live graduation.
window_start / window_end09:45 / 13:00Intraday window in which S3 may fire on an expiry day.
target_pct / stop_pct0.40 / 0.30Take-profit / stop on premium; fall back to global trading.* when unset.
exit_by15:00Hard square-off before ~15:30 settlement.
Run / observe
# on a NIFTY expiry day, run a paper day and watch for the S3 entry
docker compose run --rm daemon run --mode paper
docker compose run --rm daemon report --date 2026-06-16
Verify (audit log)
  • Non-expiry day → S3 never fires (idle).
  • Expiry day, inside the window, with a confirming technical signal → one entered decision tagged S3, then an EXIT by exit_by.
  • A second qualifying signal the same day is suppressed (once-per-day latch).
0DTE is fast. Validate the window + target/stop across a paper campaign before any live promotion. See Execution modes for how mode gates order placement.

Operate the VIX-spike momentum strategy (S4 · live in paper)

S4 watches India VIX intraday. When VIX jumps spike_pct% or more above the prior close, it reads the move as a volatility / down-momentum impulse and buys a put (BUY_PE). The trigger latches so one spike fires once; it re-arms when VIX settles back, bounded by a daily entry cap. Enabled by default in paper (s4.enabled: true).

Knobs (under s4: in config/default.yaml)
KnobDefaultEffect
enabledtrueToggle S4. Paper-only until live graduation.
spike_pct10.0% jump in India VIX above prior close that arms a BUY_PE entry. Raise to react only to sharper spikes.
max_entries2Daily cap on S4 entries.
target_pct / stop_pct / exit_byfallbackUnset by default → inherit the global trading exits.
Run / observe
docker compose run --rm daemon run --mode paper
docker compose run --rm daemon report --date 2026-06-16
#   bot: /signals shows the live VIX-spike trigger state
Verify (audit log)
  • Calm VIX day → S4 idle.
  • VIX crosses the spike threshold → one entered decision tagged S4 (BUY_PE); the trigger latches.
  • Further spikes the same day are bounded by max_entries; the latch re-arms only after VIX settles.

15Configure

Configuration is config/default.yaml → pydantic Settings. Secrets always come from environment variables, never the file.

Secrets (env / .env)

UPSTOX_API_KEY=...        UPSTOX_API_SECRET=...
FYERS_CLIENT_ID=...       FYERS_SECRET_KEY=...    FYERS_PIN=...
ANTHROPIC_API_KEY=...     TAVILY_API_KEY=...      # Tavily = web-research (opt-in)
TELEGRAM_BOT_TOKEN=...    TELEGRAM_CHAT_ID=...    # bot serves ONLY this chat
UPSTOX_SANDBOX_TOKEN=...  # separate 30-day portal token
NT_ADMIN_TOKEN=...        REFLECT_PAT=...         # admin console · self-reflection PRs
NT_BASE_DOMAIN=...        CLOUDFLARE_API_TOKEN=...  # Caddy reverse proxy (subdomain TLS) — opt-in
# NT_ALLOW_LIVE=1   → env-only live-trading gate (NEVER in YAML — see §07)

Where to get each value — provider setup

Every value below is obtained manually from the provider, then pasted into .env. The When needed column tells you which are mandatory vs opt-in.

Provider setup
Provider · value(s)Where to get itWhen needed
Upstox
UPSTOX_API_KEY
UPSTOX_API_SECRET
Upstox developer console → create an app → set redirect URI http://localhost:8721/callback → copy the API key + API secret. The OAuth access token is minted at runtime by docker compose run --rm --service-ports daemon login (not stored in .env).Always (data + paper/live)
Upstox sandbox
UPSTOX_SANDBOX_TOKEN
Upstox portal → generate a separate 30-day sandbox token.Only for sandbox-check
Fyers
FYERS_CLIENT_ID
FYERS_SECRET_KEY
FYERS_PIN
myapi.fyers.in → Dashboard → Create App (App name + Redirect URL) → copy App ID (→ FYERS_CLIENT_ID, e.g. L9NY305RTW-100) + Secret ID (→ FYERS_SECRET_KEY). FYERS_PIN is your Fyers trading/login PIN.If using the Fyers feed/capture
Telegram
TELEGRAM_BOT_TOKEN
TELEGRAM_CHAT_ID
Message @BotFather/newbot → copy the token. Then message your bot once and read chat.id from https://api.telegram.org/bot<TOKEN>/getUpdates (or use @userinfobot). The bot serves only this chat.Bot reports / EOD
Anthropic (default LLM)
ANTHROPIC_API_KEY
console.anthropic.com → Settings → API keys → Create key. Prefix sk-ant-. (Comma-separate multiple keys via MARKETGENIE_LLM_KEYS_ANTHROPIC for rotation.)Real-LLM runs (else --mock)
OpenAI (alt LLM)
MARKETGENIE_LLM_KEYS_OPENAI
platform.openai.com → API keys → Create new secret key. Prefix sk-.Only if a role routes to openai
Tavily (web research)
TAVILY_API_KEY
app.tavily.com → sign in → copy an API key from the dashboard. Prefix tvly-; 1,000 free credits/month, no card.Only if research.enabled=true
GitHub (self-reflection PRs)
REFLECT_PAT
github.com → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate → scope to this repo → Repository permissions: Contents: Read and write + Pull requests: Read and write. Prefix github_pat_.Only if reflect.enabled
Cloudflare (Caddy proxy)
CLOUDFLARE_API_TOKEN
NT_BASE_DOMAIN (user-defined)
dash.cloudflare.com → My Profile → API Tokens → Create Token → permissions Zone → DNS → Edit + Zone → Zone → Read, scoped to your zone (e.g. abysmallab.in). Set NT_BASE_DOMAIN to the base domain. Also create DNS A records grafana + admin → your VPS IP.Only for the reverse proxy
Admin console (self-generated)
NT_ADMIN_TOKEN
No provider — generate one: openssl rand -hex 32. The admin API refuses to start if admin.enabled and this is empty.If admin API enabled

Sections & defaults

SectionKey defaults
tradingmode: paper · capital_inr: 1500000 · gap_threshold_pct: 0.6
llmper-role {provider, model} · daily_budget_inr: 100
signalscombiner_gate: 0.65 · top_k_premarket: 5 · immediate_skip_confidence: 0.80 · weights news .55 / tech .30 / fii .15
riskkelly_fraction: 0.25 · risk_per_trade_pct: 1.5 · daily_drawdown_pct: 2.5 · premium_threshold_inr: 15000 · history_days: 30
executiontrail_activate_pct: 0.10 · trail_pct: 0.15 · squareoff: 15:12 · orders_per_sec: 10
opscapture_start: 08:45 · trade_start: 09:10 · intraday_until: 14:30 · eod_at: 15:45
obsenabled: true · daemon_port: 9100 · bot_port: 9101
s3 · s40DTE + VIX-spike strategies — enabled: true (paper). Keys below.
news_feed · research · reflectAll enabled: false by default — see Intel & Self-reflection.
journal · marketdatadb_path + optional url (Postgres) — see Storage

LLM provider routing

llm:
  providers:
    news_fast:    {provider: anthropic, model: claude-haiku-4-5}
    news_deep:    {provider: anthropic, model: claude-sonnet-4-6}
    debate_bull:  {provider: anthropic, model: claude-haiku-4-5}
    debate_bear:  {provider: anthropic, model: claude-haiku-4-5}
    debate_judge: {provider: anthropic, model: claude-sonnet-4-6}
    refine:       {provider: anthropic, model: claude-haiku-4-5}
    reflect:      {provider: anthropic, model: claude-sonnet-4-6}
    # research:   {provider: anthropic, model: claude-sonnet-4-6}   # add when enabling research
  daily_budget_inr: 100

Swap any role to OpenAI / Gemini / Ollama by changing provider + model — no code change. Override any of this from the environment without editing the repo — see Intel → LLM env routing.

S3 / S4 strategy knobs

s3:                    # 0DTE expiry-day momentum (NIFTY weekly expiry only)
  enabled: true
  window_start: "09:45"
  window_end:   "13:00"
  target_pct: 0.40
  stop_pct:   0.30
  exit_by:    "15:00"  # hard square-off before ~15:30 settlement
s4:                    # VIX-spike momentum → BUY_PE
  enabled: true
  spike_pct: 10.0      # % jump in India VIX vs prior close that arms an entry
  max_entries: 2       # daily cap (latches per spike, re-arms when VIX settles)

S4's target_pct/stop_pct/exit_by are unset by default and fall back to the global trading exits; each s3.* exit key likewise falls back when unset.

16Intel — news, research & LLM routing

Three opt-in-OFF units that widen and harden the information edge. All default disabled; enable per unit in config/env.

News feed

news_feed — RSS poller. Items normalise to NewsItem and are scored by the existing two-tier NewsAgent. Enable: news_feed.enabled: true + feeds: [...].

Web research

research — pre-open web-search grounding (Tavily). Runs once pre-market; the pipeline reads cached ResearchFindings via the evidence bundle — no live intraday search. Budget-gated + fault-isolated. max_searches: 3; needs TAVILY_API_KEY.

LLM env routing

Env-only overrides + a credential pool — change models or keys without editing the repo. Precedence: per-role env > global env > YAML.

LLM env overrides & credential pool

The CredentialPool rotates comma-separated keys round-robin and fails over to the next provider when a provider's keys are exhausted; on full exhaustion the pipeline degrades to raw confidence rather than failing.

# Global kill-switch (all roles → one model):
MARKETGENIE_LLM_PROVIDER=anthropic
MARKETGENIE_LLM_MODEL=claude-haiku-4-5

# Per-role escape (role uppercased, hyphens → underscores):
MARKETGENIE_LLM_NEWS_DEEP_PROVIDER=anthropic
MARKETGENIE_LLM_NEWS_DEEP_MODEL=claude-opus-4-8

# Credential pool — comma-separated keys; round-robin, fail over on exhaustion:
MARKETGENIE_LLM_KEYS_ANTHROPIC=key1,key2
MARKETGENIE_LLM_KEYS_OPENAI=key1
# Cross-provider fallback order when all keys for a provider are exhausted:
MARKETGENIE_LLM_FALLBACK=anthropic,openai

Spend stays bounded by llm.daily_budget_inr. The YAML route blocks live in Configure.

17Storage

Two stores — the journal (signals, trades, positions, debates, decisions) and marketdata (ticks, candles, snapshots, events). Both run on SQLAlchemy Core; the dialect is chosen by URL, so the same code serves SQLite and Postgres.

StoreDefaultPostgres
journalSQLite (WAL) at var/journal.dbset journal.url or MARKETGENIE_JOURNAL_DB_URL
marketdataSQLite (WAL) at var/marketdata.dbset marketdata.url or MARKETGENIE_MARKETDATA_DB_URL
# point at Postgres, then create the schema:
export MARKETGENIE_JOURNAL_DB_URL=postgresql+psycopg://user:pass@host/db
python -m marketgenie db upgrade        # Alembic migrations on a fresh database

storage/factory.py picks the store from config; storage/engine.py builds a dialect-correct engine (SQLite WAL vs a Postgres pool); storage/schema.py defines the Core tables. SQLite needs zero setup and is the default for paper campaigns; Postgres is for durable, multi-process or cloud deploys.

18Module explorer

A curated map of the system's Python modules across 12 architectural layers — app, data, signals, risk, execution, llm, broker, market, journal, storage, research and obs. Filter by layer or search above.

19Tune

All knobs live in config/default.yaml — edit + restart, no redeploy logic.

GoalKnobEffect
More / fewer signalssignals.combiner_gateLower → more trades, lower avg confidence
Reweight sourcesweight_news / weight_technical / weight_fiiSet from measured hit-rates (refine agent)
Position sizerisk.kelly_fraction · risk_per_trade_pctLower = more conservative
Naked vs spreadrisk.premium_threshold_inrAbove → debit spread (H1-1)
Drawdown haltdaily_drawdown_pct · max_consecutive_lossesL2 trip thresholds
Trailing exittrail_activate_pct · trail_pctArm point and trail width
Entry/exit timesexecution.squareoff · ops.intraday_untilLast exit / last entry
LLM costllm.daily_budget_inrExhausted → fall back to raw confidence

Weight tuning is evidence-driven: the refine agent scores each source's hit-rate from the journal and proposes new weights after each session. Proposals are never auto-applied — review via /weights or the EOD report, then edit config manually.

20Testing & validation

.venv/bin/python -m pytest tests/ -q              # 541 passed, 2 skipped, 6 deselected (live)
.venv/bin/python -m pytest tests/integration/ -q  # cross-component + full-day E2E
.venv/bin/python -m pytest -m live                # network tests (needs token)
.venv/bin/ruff check src tests
  • TDD throughout — every feature shipped failing-test-first.
  • tests/integration/ is the M6 entry-condition gate: a synthetic full session through pollers → agents → debate → combiner → risk → paper fills → journal, output asserted.
  • replay-day --mock runs the same path on canned LLM output, gating on journal consistency (all positions closed, no orphaned legs, failures surfaced).
  • Drills exercise the kill switches in isolation, no network.
  • Fault-isolation testtest_server_bind_failure_does_not_raise: verifies that a second metrics server on an occupied port logs and never raises, keeping the trade path safe.

21Roadmap & milestones

M0 · Foundations

Repo, config, LLM abstraction, Upstox OAuth, instrument master, calendars.

M1 · Vertical slice

S2 gap signal → paper fill → journal → EOD report.

M2 · Data layer

MarketFeed + failover, pollers, candle archive, capture.

M3 · Signal layer

Technical + news agents, debate layer, combiner, VIX filter.

M4 · Risk + execution

Kelly sizer, H1 overlay, kill switches, order manager, monitor.

M5 · Ops + deployment

Telegram, daemon, Dockerfile, Compose, Helm, Ansible.

M6 · Validation (entry complete)

Integration E2E + campaign tooling. Daemon trades S1 + S2 live.

Observability · metrics + dashboards

prometheus_client, three metric families, two Grafana dashboards, journal annotations, Helm scrape port, fault-isolation test.

Execution modes

shadow / paper / sandbox / live router via make_router; env-only live gate (NT_ALLOW_LIVE).

Intel · news + research + LLM env

RSS news feed, pre-open web-search grounding, env LLM overrides + credential-pool rotation/failover.

S3 + S4 strategies

0DTE expiry momentum + VIX-spike momentum; opt-in, enabled in paper.

M8a · Storage abstraction

SQLAlchemy Core journal + marketdata; SQLite WAL default, Postgres via URL + Alembic.

M8b · Decision audit

Fault-isolated decisions table — every trade and block logged with reason code + rationale.

M8c · Admin console

Material 3 operator console: status, audit/trades/signals, halt/resume/clear-latch.

M8d · Self-reflection loop

EOD agent proposes config/code/doc changes via PR; CI + human gated; market-grounded.

M7 · Live graduation

20% capital, every order alerted, 2-week manual review — gated on prerequisites.

M6 → M7 graduation gates (measured by campaign)

  • Positive expectancy after costs over ≥80 trades / ≥6 weeks
  • Max daily drawdown ≤2.5% · zero L3 events from system defects
  • News→order p95 <20s · paper fills spot-audited against market prints
  • AlgoTest backtests don't contradict paper
Campaign blocked on human prerequisites: Upstox + Fyers live accounts · LLM API keys · UPSTOX_SANDBOX_TOKEN · Telegram tokens · Mumbai VPS with registered static IP · verified 2026 NSE holiday list.

22Verified market facts

Every load-bearing market/API claim was re-verified 2026-06-10 against primary sources (SEBI/NSE circulars, live API probes, instrument masters).

Lot sizes

Nifty 65, Bank Nifty 30 (changed 3× since Nov-2024). Always a runtime lookup, never hardcoded.

Weekly expiries

Only two exist: Nifty (NSE, Tuesday), Sensex (BSE, Thursday). NSE↔BSE swapped 1-Sep-2025.

Margins

Short ATM Nifty ≈ ₹1.74L; +2% ELM on expiry-day shorts. ₹15–25L → ~5–8 short lots → selling defaults to spreads.

SEBI algo rules (1-Apr-2026)

Registered static IP, X-Algo-Name header, ≤10 orders/sec. Live orders require a Mumbai VPS.

Sandbox ≠ paper

Only 7 order-payload APIs; no market data, no fills. Paper trading is our own fill simulator vs live quotes.

Token lifecycle

Expires 03:30 IST daily; no headless renewal → one human approval tap each morning.