Skip to content

ThetaPrime v0.35.7

Choose a tag to compare

@dsharma01 dsharma01 released this 24 Aug 12:31
· 15 commits to main since this release

Download ThetaPrime-0.35.7-setup.exe

Changelog

All notable changes to ThetaPrime are documented here.
Format follows Keep a Changelog.
Versioning follows Semantic Versioning.

Unreleased


[0.35.7] — 2026-08-24

Fixed

  • A virtually-untraded deep ITM leg's best bid/ask could itself be mispriced — confirmed live 2026-08-24: NIFTY26AUG25950PE (1 DTE) had its MARKET close rejected ("no trades in this instrument") and the depth-based LIMIT fallback crossed into a thin, mispriced bid ~80pts below the leg's own intrinsic value. _marketable_limit_price now clamps toward intrinsic (a SELL never goes below it, a BUY never above), threaded through place_order/convert_to_limit across Kite/Breeze/Dhan adapters and strategies/engine.py's close/retry path. Unconfirmed closes now stash the full leg so the retry can re-place it the same way a fresh close would.

[0.35.6] — 2026-08-24

Fixed

  • Desktop shell (installer/shell.py) opened its WebView2 window in private mode by default, so broker OAuth login never saved passwords/autofill/cookies across launches. Now uses a persistent per-user profile under %LOCALAPPDATA%.

[0.35.5] — 2026-08-23

Fixed

  • setup.iss never declared 64-bit install mode, so Inno Setup defaulted to 32-bit and installed into Program Files (x86) despite ThetaPrime being a 64-bit app. Added ArchitecturesAllowed/ArchitecturesInstallIn64BitMode x64, plus detection and a migration prompt for a pre-fix (x86) install.

Changed

  • Install docs (docs/installation.md, docs/dev/installation.md) now describe broker-agnostic prerequisites (Kite/Breeze/Dhan) instead of Kite-only, and point at the correct app log path (C:\ProgramData\ThetaPrime\logs\thetaprime.log).
  • Dashboard's no-token banner now reads "Broker session expired" instead of Kite-specific wording.

[0.35.4] — 2026-08-23

Changed

  • Install docs (docs/installation.md, docs/setup.md, docs/security.md, docs/dev/installation.md) rewritten for the pywebview app-window/tray-icon flow, replacing stale browser-tab instructions; documents the license request flow and the NSSM service-registration failure mode.
  • tools/create_coupon.py, tools/generate_license.py, tools/process_license_requests.py now share a tools/_wrangler.py d1_execute() helper instead of each calling subprocess.run() directly.

Added

  • installer/build_installation_pdf.py regenerates the installation guide PDF.

[0.35.3] — 2026-08-22

Fixed

  • Installer upgrades could fail with DeleteFile failed; code 5 (Access is denied) on a bundled .pyd file — ThetaPrimeShell.exe hides to the tray on window close rather than exiting, so it's almost always still running (with its own DLLs loaded) during an upgrade, and the installer only ever stopped the NSSM service. setup.iss now also taskkill /F /IM ThetaPrimeShell.exe (best-effort) before file replacement.

[0.35.2] — 2026-08-22

Fixed

  • Installer's icon-cache refresh used ie4uinit.exe -ClearIconCache, which isn't reliably present on all Windows builds (CreateProcess failed; code 2 on a real install). Replaced with a direct SHChangeNotify(SHCNE_ASSOCCHANGED, ...) call to shell32.dll in setup.iss's [Code] section — no external binary to go missing.

[0.35.1] — 2026-08-22

Fixed

  • Desktop shell (ThetaPrimeShell.exe) showed a stale/incorrect icon after install — icon is now embedded into the exe at Nuitka compile time instead of referenced separately by the shortcut, and the installer clears Explorer's icon cache on upgrade so a changed embedded icon isn't hidden behind a stale cached one.
  • shell.py now passes the window icon to pywebview explicitly instead of relying on it being extracted from the running exe.

[0.35.0] — 2026-08-22

Added

  • Desktop shell (installer/shell.py): pywebview native window wrapping the dashboard, pystray tray icon, and win11toast toast notifications, replacing the old "open dashboard in browser" flow.
  • notifier.py desktop channel: queues alerts for the shell to poll via GET /api/desktop_alerts, since the notifier can run inside the Session-0 NSSM service where UI can't be shown directly.
  • Installer now silently installs the WebView2 runtime if missing, and launches ThetaPrimeShell.exe from the Start Menu / post-install instead of open_dashboard.bat.

[0.34.2] — 2026-08-22

Added

  • docs/option-scanner.md — scan modes, results columns, Flow (OI Analysis) buildup categorization, and the Ask AI handoff, cross-linked from docs/strategy-usage.md.

Fixed

  • Option Scanner page description said "Instruments & spot always from Kite" — stale since the broker-parity work made every field (instruments/spot/OI/LTPs) come from whichever broker is selected.
  • 4 markdown tables in docs/strategy-composer.md's regime-skip filter section were rendering as raw pipe-delimited text instead of tables (missing blank line before the table, same bug class as the list-gluing fix in v0.32.13).

Changed

  • marketing/features.md's Option Scanner entry updated for Flow (OI Analysis), multi-broker support, and Ask AI — was still describing only the original premium%/IV/delta scan.
  • README.md rewritten — was still describing the pre-multi-broker prototype (wrong product name, in-memory token storage, a PyInstaller build script that no longer exists).

[0.34.1] — 2026-08-22

Added

  • tests/test_broker_conformance.py — structural conformance suite that checks every broker adapter (Kite, Breeze, Dhan) implements the full BrokerClient contract and constants.
  • docs/brokers/adding-a-new-broker.md — checklist for wiring in a 4th broker adapter.

Changed

  • Broker adapters (Kite/Breeze/Dhan) and scanner.py/app.py brought into parity per the new conformance checklist.

[0.34.0] — 2026-08-21

Added

  • Option Scanner: OI-buildup / unusual-volume / PCR flow analysis (Long/Short Buildup, Short Covering, Long Unwinding) per strike, plus an OI column on the plain scan. Previous-day OI baseline is fetched once per symbol per day and cached to oi_baseline.json.
  • Option Scanner results have an "Ask AI" handoff — reviews the scan CSV for notable strikes, mispricings, or unusual activity via the configured AI provider.
  • option_scanner feature flag now defaults on.
  • New strategies/reliance_cash_secured_put strategy (spec + generated .py).

Changed

  • AI block-catalog prompt trimmed to type/param essentials and compacted to non-pretty-printed JSON — was hitting Groq's free-tier 8000 TPM limit and leaving no room for the auto-correction retry.
  • API error responses no longer leak raw exception text/paths to the browser — routed through a shared handler that logs the full exception server-side with a correlation id and returns a short generic message instead.
  • quote() now returns open interest (oi) across all three broker adapters (Kite passthrough, Breeze/Dhan unverified live).

Fixed

  • Live-candle instrument-token lookups are now cached per symbol instead of re-resolved on every call.

[0.33.2] — 2026-08-19

Added

  • Trade journal CSV (lib_v1_paper_trade_journal.csv) is now encrypted at rest and in backups, same path as run_history.db/performance.db (issue #370).
  • Cheap RSS/thread-count snapshot logged every reconcile cycle, piggybacked on the existing broker round-trip — no documented CPU/RAM footprint for the always-on service previously (issue #375).
  • Installer's Finished page now checks whether the NSSM Windows service actually registered instead of assuming success, and reports clearly if it didn't.

Changed

  • docs/setup.md documents the broker read-call retry/backoff behavior (2 retries, 0.5s/1.5s delay, 5 consecutive failures raise a "broker degraded" alert).

[0.33.1] — 2026-08-19

Added

  • Build with AI in the Strategy Composer is now an interactive in-app chat when an AI provider is configured (Settings → AI): every reply is validated server-side before it reaches the UI, with one bounded auto-correction retry on an invalid spec. Falls back to the original copy/paste handoff when no provider is configured. Bring-your-own-key via any OpenAI-compatible endpoint (Groq default, Ollama for local/no-key use, OpenAI, or custom).

Fixed

  • "Build with AI" occasionally producing an invalid spec with no feedback — replies are now run through the composer's own validator instead of trusted as-is.
  • The AI prompt's block catalog dump cut from ~7960 to ~2960 tokens (dropped UI-only description/label text, compacted JSON) — was causing 413s and leaving no headroom for a retry within free-tier per-minute token limits.

[0.33.0] — 2026-08-19

Added

  • Manual takeover: square_off() accepts an optional legs=[...] param to close a single position and leave the rest running (real order, not just a record). A strategy that's had a leg closed this way sets manual_control — autopilot (entries, exits, adjustments, reentry) leaves it alone until every leg is closed, then resumes on its own. Strategy card and trade-status modal show a ✋ MANUAL badge; the manual-exit form gets a per-leg "Close Now" button.

Fixed

  • Token-freshness nudges and broker session-dead notifications (Kite/Breeze/Dhan) no longer fire outside trading hours — gated to market hours ± 2h via new holidays.in_reminder_window().

[0.32.17] — 2026-08-18

Added

  • "External Data Sources" card on the Data Management page: last-updated status for the cached NFO instrument snapshot, NSE holiday calendar, and Yahoo-derived regime calibration. New docs/data-sources.md documents each source.
  • tools/replay_session.py: replays a historical volatile trading session through the real scheduler/monitor loop in paper mode with an optional broker-throttling burst injected, to verify ticks keep pace (issue #368).
  • Entry fills now warn (never abort) when a live fill lands more than 5% from its pre-trade reference price (STRATEGY_MAX_SLIPPAGE_PCT).

Fixed

  • A close order that never confirms filled is no longer silently dropped: the leg is flagged _exit_unconfirmed, blocks new entries/re-entries until a human confirms via Mark Exited, is picked up immediately by reconciliation, and the trade notification now says "may still be open at the broker" instead of a plain ✅.
  • A disabled strategy with an open position now keeps running its monitor tick (PT/SL/EOD exits still fire) — only new entries are blocked, not the exits that protect an existing position.
  • run_entry() now takes an exclusive file lock so a cron tick and a manual/retry trigger landing in the same window can no longer both place orders.
  • state.json writes go through tmp+rename for atomicity, avoiding a truncated read by a concurrent dashboard poll.
  • Entry-rejection notifications report the actual per-leg broker status instead of a hardcoded "(margin?)" guess.

[0.32.16] — 2026-08-18

Added

  • Custom 404/500 error pages (templates/error.html); API routes now return a generic JSON error with a logged correlation id instead of leaking raw exception text/paths (_safe_error_message/_api_error, issue #366).
  • Live automation-latency metric: SpecEngine times signal-detected → order-acknowledged for real trades (_mark_signal/_pop_latency); performance.latency_stats() computes p50/p95/max; Reports shows "Latency p50/p95 (live)" summary cards, green/red at the 200ms target.

Fixed

  • broker/regime.py: today's cached regime bar is no longer frozen, so intraday regime reads stay live instead of going stale until the next day's fetch.

Changed

  • live_candles: resolved instrument_token is now cached per symbol, avoiding a redundant lookup on every poll.

[0.32.15] — 2026-08-17

Added

  • long_eod exit: hold_below_pts skips the pre-expiry long-leg close when P&L is already worse than the expiry-guaranteed floor. Left at 0, a bracket long-combo (long call struck below a long put) auto-derives that floor from strikes/entry prices; any other leg shape just honors the DTE trigger as before.

Fixed

  • long_eod ctx now prices the long legs live instead of using the 0.0 pnl_pts default — needed for hold_below_pts (and any leg-scoped PT/SL) to evaluate correctly.

[0.32.14] — 2026-08-17

Added

  • KiteAdapter: session-dead alerting and a degraded-streak warning, ported from the Breeze/Dhan adapters — Kite was the one adapter that could fail silently on a dead token or a flaky run of 5xx errors.
  • Pre-market token-freshness check (08:45 IST): notifies once per trading day if a broker with an enabled real-money strategy still has a stale/missing session token, instead of the first sign being a failed entry at market open.
  • Scheduler self-heal: detects and restarts a silently-dead APScheduler background thread (confirmed real — one ran dead for 3 days with no error logged) and notifies when it happens.
  • Coupon tool / license server: coupons can now be bound to a single email (--email), so paid-sale codes aren't redeemable by anyone; beta/promo codes stay open by omitting it.

Removed

  • skip notifications (filter-blocked entries) — noisy, not actionable per-event.

[0.32.13] — 2026-08-15

Fixed

  • Markdown lists in several docs/*.md files were glued into the preceding paragraph instead of rendering as a list — CommonMark requires a blank line before a list to parse it as one. Fixed across 10 doc files; also switched composer-strategy-examples.md's numbered items from - 1. Foo to plain 1. Foo so the embedded number isn't parsed as a nested list.
  • PremiumTarget leg selector: failure message now distinguishes "no live/BS price at all" from "priced but none within the configured band" instead of one generic error.

Changed

  • docs/strategy-usage.md: added a broker-selection comparison table for the Simulate modal (Breeze/Kite/Dhan data retention and synthetic-price caveats).

[0.32.12] — 2026-08-14

Fixed

  • Entry rejection handling could unwind live positions that were never actually rejected: an async accept-then-reject race in Kite's RMS could leave an order reading REJECTED in the order book while the position had actually filled. _cancel_and_rollback now cross-checks broker.positions() before trusting a REJECTED verdict, and no longer auto-closes long legs (LC/LP) when only short siblings are rejected — those are parked into _long_active instead.

Added

  • edit_trade_state: confirm_active — manual, broker-verified escape hatch to move a PARTIAL_ENTRY_FAILED trade's rejected legs into ACTIVE, for state predating the fix above.

[0.32.11] — 2026-08-14

Added

  • Docs viewer: sidebar search across doc titles and body content, auto-built on-page table of contents with scroll-spy, prev/next doc footer, mobile off-canvas sidebar.

Changed

  • Flat-card visual pass: dropped shadow-sm/shadow across dashboard, auth, reports, performance, scanner, history, and onboarding templates.
  • Login/license splash: navy background matching the DESIGN.md palette, prefers-reduced-motion support, responsive width on small screens.
  • DESIGN.md: added Hero and docs-reading-scale typography tokens, documented accepted one-off utility values.
  • Accessibility: skip-to-content link and <main> landmark on every page, hidden dashboard h1.

Fixed

  • Windows installer build: yfinance wasn't bundled into the Nuitka build (broker/ is nofollow'd, so the static import scan misses its lazy import), causing installed builds to fail on India VIX lookups.

[0.32.10] — 2026-08-14

Added

  • roll_long_legs for roll_untested: opt-in delta-drift rolling of held diagonal/calendar long legs (LC/LP), independent of the short-leg roll — a long hedge leg has no decay/pressure signal, so it rolls on live delta drifting past long_target_delta ± long_delta_band instead. Short-leg roll widened to keep working across short re-entry cycles instead of stopping once a long leg was parked.
  • delta_otm leg selector: OTM strike closest to a target |delta|, scanned by %-of-spot, no time-value constraint.
  • stop_loss_pct_premium exit: stop loss at -N% of entry premium (credit or debit), adapting across volatility regimes better than a fixed points stop.
  • Composer UI: reorder blocks within a slot (move up/down); payoff-diagram compute errors now shown inline instead of silently hiding the chart.

Fixed

  • "Long legs held" status message always reported DTE=1 regardless of the configured long_eod trigger.

Changed

  • long_eod dte trigger range widened from 1–7 to 1–60.

[0.32.9] — 2026-08-13

Fixed

  • Legs with no live tick now refresh via Black-Scholes fair value (_reprice_bs) instead of sitting frozen at a stale price — previously showed flat +0.00 pts P&L all session.
  • Day-rollover state reset (P&L, closed-trade log, re-entry tracking) factored into _reset_if_new_day() and called from both run_entry() and run_monitor(), so a crashed/skipped entry job no longer leaves _short_closed_today stuck from the prior day and permanently blocking re-entry.

[0.32.8] — 2026-08-13

Added

  • roll_short_legs for Diagonal/Calendar (default on): re-sells the short leg(s) each time they close while the long leg(s) stay held, instead of closing once and sitting out until the long leg's own exit. Configurable via the composer's generic param form.
  • Dhan: expired option contracts now fall back to the rollingoption endpoint for historical data (spot-relative strike/expiry slot) when the instrument master has dropped the exact contract, matching Kite's expired-contract limitation. Fallback candles are tagged approx and flagged in sim-availability messaging/hints as indicative, not exact fill history.

Fixed

  • Multi-sim day parsing (multi_sim.py) summed only the last short_exit/exit event per day, silently dropping P&L from earlier same-day rolls under roll_short_legs — now sums every exit event seen that day.
  • NSSM service stop/remove (installer/nssm_setup.bat) could hang the installer indefinitely if the app didn't shut down cleanly; stop is now bounded to 15s before force-killing the backing process, with fast-path exits for a not-installed or already-stopped service.

Changed

  • dhan enabled by default in config.json.

[0.32.7] — 2026-08-12

Added

  • Multi-day sim extend/resume: widen a completed or stopped multi-day sim session with new days before its start date, after its end date, or both, without re-running already-stored days. Forward extension restores the last day's saved state snapshot before continuing; backward extension runs clean and stops at the existing start date without overwriting stored days. A stitch-risk banner and per-day badge flag when a carry-overnight position spans the join.

[0.32.6] — 2026-08-11

Fixed

  • SIMULATION COMPLETE summary no longer buckets 100% of a combined single-phase close (e.g. iron condor's LC/LP + SC/SP closed together) as Short P&L — split is now computed per-leg instead of by an entry-level long_exit flag, so Long P&L no longer always prints +0.00 for these structures.
  • storage.py retries file removal on PermissionError — OneDrive can briefly hold an exclusive lock on a just-written file while syncing.

Changed

  • Clone-strategy dialog shows the correct .spec.json extension label for spec-based strategies instead of always .py.

[0.32.5] — 2026-08-11

Fixed

  • Reports/summary P&L no longer double-counts a diagonal's long-leg close (long_exit) against a same-day short_exit — long_exit's totals fold in the day's short-leg P&L, which was being summed a second time. Added performance.realized_pnl() to pick the right field per event type.

[0.32.4] — 2026-08-11

Removed

  • One-off defect-review-report.md/.pdf and generate_pdf_report.py that were accidentally tracked in 0.32.3 — internal release artifacts, not app functionality.

[0.32.3] — 2026-08-11

Added

  • Trade data encryption at rest: run_history.db and performance.db are now encrypted (AES-256-GCM) whenever ThetaPrime isn't running. The key lives in Windows Credential Manager alongside broker tokens, so scheduled/automated trading survives a reboot unattended. A one-time recovery code is shown on the Auth page on first run after upgrading, and can be regenerated from the Data Encryption card.
  • Scheduled backups: a new section under Backup & Restore runs the existing backup automatically on a daily timer to any local folder (including one synced by OneDrive/Dropbox/Google Drive), with configurable retention. Trade data is encrypted inside the backup zip; strategy files/settings are not.

Changed

  • Legacy strategy_builder.html/strategy_generator.py moved to archive/ — superseded by the composer/spec-only strategy authoring path.

[0.32.2] — 2026-08-10

Fixed

  • _leg_candles() now falls back to the cached NFO instrument-token dump when quote() misses a token — same fallback chain as option_history_api (confirmed 2026-08-10: Option History resolved a token live_candles reported as missing).
  • Manual square-off now records the closed position into state['_closed'], closing a gap where engine.run_entry()'s "already closed today" guard didn't fire — a manual square-off could be followed by an unwanted second entry the same day (confirmed in prod 2026-08-07).
  • broker/regime.py: NIFTY/VIX series are now aligned by date before the volatility vote walk (an unmatched bar from either ticker was silently shifting indices out of sync). A "medium" hold that outlives _STALE_CAP sessions with no reconfirmation now decays to a plurality read instead of riding a stale label indefinitely.

[0.32.1] — 2026-08-10

Fixed

  • live_candles's _leg_candles() swallowed exceptions from the Kite instrument-token lookup (quote()) with no log line, unlike the sibling _fetch() helper and the matching pattern in sim_availability.py/sim_ltp.py. A leg whose candle chart silently came up empty (e.g. "No candle data today for LP") left no trace of why. Now logged.

[0.32.0] — 2026-08-09

Added

  • Standard/Pro license tiers: license keys now sign a broker-count tier (expiry|tier|signature) — Standard allows one broker enabled at a time across all strategies, Pro allows as many as you've connected (Kite, Breeze, Dhan). Enforced at both strategy-enable time and at launch time, so a stopped-app metadata edit can't bypass it. Legacy keys issued before this change keep working, grandfathered as Pro. tools/generate_license.py --tier.
  • The 15-day free trial now grants full Pro-equivalent access (live trading, every broker) for its entire duration — the tier you purchase only starts to matter once the trial ends.

Changed

  • /license: activating a lower-tier key that would exceed your currently-enabled broker count now warns (naming the affected strategy) instead of silently letting it happen — activation itself still always succeeds.

[0.31.0] — 2026-08-09

Added

  • In-app license purchase requests: /license now has a "Request Standard" / "Request Pro" form — email + optional coupon code, machine fingerprint included automatically (no more copy-pasting it into an email). A valid coupon approves instantly; otherwise the request queues for manual review. The page auto-fills the License Key field once a request is fulfilled, ready to Activate.
  • New Cloudflare Worker routes (license-server/src/index.js) and D1 tables (license_requests, coupons) backing the above — atomic coupon redemption, a UUID+email dual-check on status lookups so a request ID alone can never pull back a signed key, and a per-fingerprint pending-request cap.
  • tools/create_coupon.py (issue a coupon) and tools/process_license_requests.py (offline poller — signs keys for approved requests on a schedule; delivery is a manual email-these list for now, see plans/plan-license-purchase-intake.md).

Planned, not yet shipped

  • plans/plan-standard-pro-tiers.md was revised to grant the 15-day trial full Pro-equivalent access (live trading, unlimited brokers) — not implemented in this release; the existing app.py gate blocking Real-mode trading during trial is still active. Tracked for a future release.

[0.30.1] — 2026-08-08

Fixed

  • docs/strategy-usage.md: 3 broken TOC anchor links (#money-mode-paper--real, #config-lots--monitor-interval, #summary-cards-1) didn't match the actual heading IDs python-markdown generates, found by the website docs sync's anchor verifier.

[0.30.0] — 2026-08-08

Added

  • Build with AI: new Composer button opens a modal where you describe a strategy in plain English; the app bundles that ask with the live block catalog, a worked example, and guardrails an AI can't infer on its own (NIFTY-only weekly expiry, F&O stock DTE floor, covered leg scope, VIX filter scope) into a prompt, hands it off to your chat AI (same flow as the existing Ask AI button), and you paste the reply into the existing Import spec dialog. ai_prompts.build_strategy_request_prompt() + POST /ai/prompt/build_strategy. See plans/plan-ai-strategy-builder-prompt.md.
  • Composer: Export button (download the current spec as .spec.json) and an Advanced edit mode to hand-edit the spec JSON in place, both reusing the existing spec-normalization path.
  • manual_exit now accepts a partial prices map covering a subset of a trade's open legs — for when only some legs were manually closed at the broker. Closing every open leg still behaves as a full close (CLOSED); closing a subset parks the rest under _long_active and moves status to SHORT_CLOSED, mirroring the two-phase transition strategies/engine.py's _close_legs() makes automatically when an exit condition fires on just the short legs.

Fixed

  • static/ai_handoff.js: a prompt longer than the URL length limit (or a clipboard-only provider like Gemini/Copilot) was still built into a ?q=-style URL, opening a dead 414 tab — the catalog-sized Build with AI prompt hit this on every use. Now falls back to the provider's plain URL and copies the prompt to the clipboard instead.
  • Composer's Import spec dialog and new Advanced-edit box both now strip a wrapping ```json ... ``` code fence before parsing — chat AIs wrap JSON replies in one by default even when told not to, so the first paste back from Build with AI was failing with a raw JSON parse error.

Changed

  • Installer: setup exe now carries the app icon (SetupIconFile); routine version upgrades (not the pre-licensing case) get a plain "Upgrading ThetaPrime — your data will be preserved" notice instead of no notice at all.

[0.29.0] — 2026-08-06

Added

  • Real covered-call support: the short_call structure gained a covered param — when set, SpecEngine.run_entry refuses to arm the position unless a live broker holdings lookup shows at least one lot's worth of the underlying already held (new BrokerClient.holdings(), implemented on all three adapters), and margin_estimate.estimate_margin_from_legs/spec.estimate_min_capital price it as covered (no SPAN margin) instead of a naked short call once holdings confirm the cover. Composer template restored as Covered call (F&O stock, income — requires holding the lot already), alongside yesterday's honestly-renamed Short call (F&O stock, income — uncovered, no equity leg) for the naked variant. See plans/plan-covered-call-equity-leg.md.
  • Extended the same covered gate to short_strangle/short_straddle — applies to the SC leg only (SP stays naked/cash-secured, unchanged). New Covered strangle (F&O stock, income — call side requires holding the lot) composer template.

Fixed

  • Composer payoff diagram and min_capital estimate ignored the selected underlying for any F&O stock: strategies/payoff.py's compute_payoff fell back to a static _DEFAULT_PARAMS table that happened to equal NIFTY's own lot_size/strike_step exactly, so switching e.g. NIFTY → RELIANCE silently kept using NIFTY's numbers. Now resolves the real lot_size/strike_step from the live instrument dump via strategies/instruments.py for any underlying not in the static index table. Uncovered a second bug while fixing it: _parse_symbol's regex was hardcoded to a literal "NIFTY" prefix — harmless while ctx.symbol_prefix was also always "NIFTY" regardless of underlying (self-consistent by accident), but would have broken every non-NIFTY payoff outright once the prefix bug was fixed; both are now correct together. static/composer.js's underlying dropdown also now refreshes the payoff diagram's spot field via /composer/spot_ltp on change (previously only on page load), so the stale-spot half of the same symptom is fixed too.

Changed

  • static/composer.js: stock_covered_call template renamed to stock_short_call / "Short call (uncovered)" — it sold a bare short_call with no equity leg, so payoff/Greeks/min_capital were computed as a naked short call despite the "Covered Call" label. No behavior change, label only.

[0.28.11] — 2026-08-05

Changed

  • docs/mcp-setup.md (ships to customers) trimmed to the installed-service MCP setup only. The running-from-source setup moved to new docs/dev/mcp-setup.md (internal, stripped by build.ps1), matching the existing docs/dev/ pattern (installation.md, licensing-internal.md).

[0.28.10] — 2026-08-05

Fixed

  • installer/build.ps1 wasn't copying mcp_server.py into the installer output, so an installed customer had no file for Claude Desktop to run — added it alongside strategies//broker//config.json/version.py, the other plain-source files that ship next to the compiled app.exe.
  • docs/mcp-setup.md only documented running from source; added a separate "installed Windows service" setup path (port 5000, no dev-mode localhost auth bypass, needs a separately installed Python since app.exe's bundled runtime isn't general-purpose).

[0.28.9] — 2026-08-04

Added

  • MCP server (mcp_server.py) exposing six read-only tools to AI assistants (Claude Desktop, Cursor) over local stdio: list_strategies, get_strategy_status, get_positions, get_pnl, get_reconciliation_status, get_recent_logs. Talks to the already-running app over localhost HTTP (new /mcp/strategies, /mcp/positions routes; reuses existing /api/performance/summary, /api/reconcile/status, /api/events), authenticated with the existing dashboard password via an X-MCP-Auth header. /api/performance/summary gained optional strategy/mode/from_date/to_date filters. See docs/mcp-setup.md. (#263)

[0.28.8] — 2026-08-03

Fixed

  • strategies/instruments.py (framework code for multi-instrument support) was missing from the installer's framework allowlist (build.ps1's $STRATEGY_INFRA_FILES / strategy_manager.py's _INFRA_FILES), so the build stripped it as a customer strategy instance and the compiled build crashed on startup with ImportError: cannot import name 'instruments' from 'strategies'.

[0.28.7] — 2026-08-03

Fixed

  • scanner.py imported Black-Scholes helpers from strategies.lib_v1_trade, a legacy strategy instance the installer build strips from customer installs. Repointed to strategies/components/blackscholes.py (the framework copy shipped since 2026-07-27), which fixes a startup crash (ModuleNotFoundError) in the compiled build.

[0.28.6] — 2026-08-03

Added

  • Multi-instrument support: spec strategies can now run on BankNifty, FinNifty and stocks, not just NIFTY. Lot sizes and strike ladders are resolved from the live NFO instrument dump (strategies/instruments.py) instead of a hand-maintained table — the exchange revises these periodically and a stale hardcoded value would place a real order on the wrong contract. Reuses scanner's existing 4h instrument cache; NIFTY keeps a hardcoded fast path so engine construction needs no network round trip in the common case. Composer UI, leg selectors, filters, exits and spec validation all updated for per-instrument strike steps.
  • sim_availability.py: pre-flight probe that samples broker historical-data coverage (one daily-candle call on the underlying + ATM option minute probes on 3 sampled days) before a multi-day simulation burns ~180s on a date the broker has no candles for. Deliberately a sample, not a guarantee.
  • New sample specs: BankNifty 9:20 ATM straddle, BankNifty diagonal iron butterfly, bear put spread.
  • Playwright-based composer end-to-end test (tests/test_composer_e2e.py; run playwright install chromium once).

Changed

  • One-off import/check scripts moved to archive/.

[0.28.5] — 2026-07-31

Added

  • Data-driven regime calibration: regime classifier thresholds (realized-vol, VIX delta, USDINR, crude, VIX-RV spread cutoffs) are now derived yearly from historical data via broker/regime_calibrate.py and loaded from regime_calibration.json, replacing hardcoded cutoffs. The FII/DII net-flow risk-appetite signal (NSE endpoint had no date param, blocking backtests) is replaced with a VIX-RV spread proxy.
  • Notifier now formats composer-engine multi-leg exits and long-leg exits (previously only the strangle and LIB V1 shapes were handled), with a new long_exit notify event.
  • New regime API endpoint.

[0.28.4] — 2026-07-31

Added

  • Telegram notification channel: alerts (entries, exits, adjustments) can now be sent directly to a Telegram chat via the Bot API, alongside the existing email and webhook channels — no bridge process required. Configure a bot token + chat ID in the Notifications card, with a Send Test button. Notification setup docs (email, Telegram, webhook, trigger events, troubleshooting) moved into docs/setup.md.

[0.28.3] — 2026-07-30

Fixed

  • Composer strategies with a time_window filter later than 09:45 could never enter: the scheduler's one-shot entry job always fired at 09:45 regardless of the spec's own filter (composer sidecars never expose an entry_time param), and monitor mode never retried entry — any transient miss silently forfeited the whole day. The entry job now derives its fire time from the spec's time_window filter when set; run_monitor() now retries entry on every tick while no position is open; run_entry() gained a guard against re-entering after the day's trade has already closed (reentries exhausted / max daily loss hit), so the new retry path can't restart trading past that limit. Also fixes nifty_strangle_adjustment_12jul2026 and rent_collector_condor_dev, which had the same latent bug.
  • Webhook notification failures are now logged as an app event (notify.whatsapp_failed) instead of only a log line, so a broken WhatsApp webhook is visible in the Event Log.

[0.28.2] — 2026-07-30

Fixed

  • Diagonal strategies could get stuck refusing to re-enter after a long-leg EOD close: _close_long finalized the trade to CLOSED without clearing _active, so if short legs (SC/SP) were still tracked there when the long legs closed, they stayed marked "open" in state forever — blocking every future entry with "Position already open" even though nothing was actually open at the broker (root cause of the 2026-07-27 DIB incident).

[0.28.1] — 2026-07-29

Changed

  • Strategy card controls redesigned: the 9-item â‹® dropdown (Custom Schedule, Real-Time, separate Arm/Disarm) is replaced by two card-face controls — an Automation switch that directly creates/removes the strategy's scheduled entry+monitor jobs, and a Paper/Real toggle where switching to Real is now the arm action itself, no separate arm step. Turning Automation off or switching Real→Paper while a Real position is open now asks for confirmation first, since it stops exit monitoring.
  • Reports page adds an Active / Archived / All status filter (applies when no single strategy is selected) so a retired strategy's trade history stays reviewable after archiving.
  • Performance page's Paper/Real mode badges now use the same solid-fill contrast as the dashboard's toggle, instead of a washed-out tonal style.

Removed

  • Custom Schedule (interval/daily cron trigger) is gone — every strategy now schedules via the Automation switch's fixed entry+monitor jobs.

[0.28.0] — 2026-07-29

Added

  • Archive / Unarchive strategy: strategies can now be hidden from the active dashboard list without deleting them, via â‹® → 🗄 Archive Strategy. Archived strategies live in a collapsed Archived panel with Restore and ✕ Delete Permanently actions. Archiving requires the strategy to be disabled with no open position first, and automatically disarms it and removes its scheduled jobs.

Fixed

  • Dashboard and Performance pages showed strategy identity inconsistently: the Dashboard showed the display name with the filename hidden behind a dev-only flag, while the Performance page mostly showed the raw filename. Both pages now always show "Display Name (filename)" together. The Performance page's strategy-label lookup also now matches on the extension-stripped base name, so a trade event recorded under a slightly different identifier still resolves to the right display name instead of falling back to a bare, unlabeled string.

Changed

  • Scheduler no longer logs a strategy.skipped event to the App Event Log every time a disabled strategy's job fires and no-ops — this was pure noise since disabling is already a deliberate, visible action.

[0.27.0] — 2026-07-28

Added

  • First-run setup wizard: a fresh install (no broker credentials saved yet) now redirects to /onboarding — a welcome screen leading into the existing Auth page (shown with a "Setup" banner and Continue →) and a final step offering to run a paper simulation on yesterday's data. Skippable at any point; never reappears once dismissed or completed.
  • Edit Trade State: a new action in the Trade Status modal for correcting lots/lot_size/expiry or a leg's symbol/action/strike/entry price/exit price after the fact — never opens or closes a position, only fixes bookkeeping, with a Validate vs Broker check and every change appended to the state file's edit_log.
  • Manual Entry / Mark Exited now use a generic N-leg form (not hardcoded to SC/SP/LC/LP, + Add leg for anything beyond the default four) and a Validate vs Broker check that confirms each symbol is still tradable and flags a price far from the live LTP before saving.
  • Adjustment (strangle-roll) events now send a WhatsApp notification, and show up as their own event types (adjustment / adjustment_failed) in the Performance event log filter and the multi-sim day log — the sim summary now also shows an Adjustments card (count, realized pts, failures).
  • Strategy Composer: a Simulate button appears once a strategy is saved, jumping straight into a paper sim for it.

Fixed

  • Disabled strategies still showed a live next-run schedule: toggling a strategy off only flips its enabled metadata flag — the underlying APScheduler entry/monitor jobs stay registered (they correctly no-op when they fire) — but the dashboard card kept showing "Entry: Tomorrow 09:45 · Monitor: Tomorrow 10:00" regardless, implying the strategy would still trade. The schedule is now hidden on the card whenever the strategy is disabled.
  • manual_exit double-counted P&L for a SHORT_CLOSED position's already-closed long legs: it read "still open" legs from the un-pruned top-level legs dict instead of _long_active, adding a closed long leg's P&L on top of what was already carried forward. manual_exit and the new Edit Trade State now share one P&L recompute (_recompute_pnl) derived from actual leg prices.
  • Multi-day sim runs now report "no historical price data available for {symbols}" as its own error instead of folding it into a generic no-trade day, when the broker has no candles for an entry's symbols (e.g. an expired/unlisted contract).
  • sim_ltp no longer burns 3 retries + backoff per instrument when a contract has no resolvable instrument_token (already expired/unlisted) — treated as no-data immediately instead of guaranteed-to-fail retries.
  • Simulated-date runs now compute gap filters' previous close from the correct prior trading day's daily candle, instead of the broker's live (today's) OHLC quote.

[0.26.11] — 2026-07-27

Fixed

  • Scheduled Jobs showed live countdowns for disabled/unarmed strategies: the Scheduled Jobs list had no idea whether a strategy was enabled or armed, so a fully-off strategy still displayed active entry/monitor jobs. Each job row now shows "disabled — will not run" or "unarmed — will run as paper, not real" based on the strategy's actual state.

Added

  • Manual entry/exit now warns in the response if the performance-log write fails, so a backfilled trade doesn't silently drop out of reports.

[0.26.10] — 2026-07-27

Added

  • Manual entry / manual exit: when automation fails to fire an entry or exit and the position is placed or closed directly at the broker, a strategy's Trade Status modal now has Manual Entry and Mark Exited (Manual) actions to backfill ThetaPrime's own state file with the real leg details — neither ever places a broker order. Once saved, the position resumes normal monitoring (exits, adjustments, EOD handling) from the next scheduled run.

Fixed

  • Diagonal long leg closing before the short leg: when a short leg's own exit and the long legs' long_eod DTE=1 exit were both due on the same tick, the long leg closed first, stripping the short leg's margin hedge and leaving SC/SP open and uncovered for the rest of the day (the scheduler stops monitoring once the long-leg close sets status to CLOSED). The short leg now closes and is verified first; the long-leg close is deferred to the same tick, after.

[0.26.9] — 2026-07-26

Added

  • Simulation Notes: multi-day sim sessions now have a free-text notes field for recording parameter-setting observations when reviewing past tests.
  • Recorded strategy config: each sim session now snapshots the strategy's full config as it was at launch (composer spec blocks, or flat params for script strategies), shown as a collapsed, human-readable breakdown — not a raw JSON dump — in both the results page and Reports → Sim History, so later live edits to the strategy don't retroactively change what a historical report shows.
  • Run time: multi-day sim sessions now record and display how long the run took, in both the session list and the detail view.

Changed

  • Adjustment rolls now apply to any position with a short SC/SP pair, not just structures with exactly two legs (e.g. iron condor's long wings are left untouched by the roll instead of blocking it entirely).
  • Default strategy entry time moved from 9:45am to 10:00am.
  • Multi-Day Sim's "Start" button now shows a spinner while the run is being launched, instead of the modal closing instantly with no feedback.

[0.26.8] — 2026-07-26

Fixed

  • 0.26.7 installer crash-looped on startup: installer/build.ps1's strategy-instance allowlist (and its strategy_manager.py counterpart) predated strategies/margin_estimate.py, so the build stripped it before packaging and every compiled app.py import of it raised ModuleNotFoundError at startup. Added the file to both allowlists.

[0.26.7] — 2026-07-26

Added

  • min_capital field on strategy specs (issue #232): a capital-tier tag stored on the spec, settable manually or via a new Estimate button in the Composer next to the strategy name, displayed in Indian number format (click to edit the raw value). Validated by validate_spec().
  • Deterministic margin estimate (strategies/margin_estimate.py) replaces the flat 20%-of-notional heuristic used whenever a real broker margin API isn't available (paper mode, Breeze, Dhan, or a Kite basket_margin() failure): defined-risk spreads (condor/butterfly/vertical) are estimated from wing width minus net credit; undefined-risk positions (naked strangle/straddle) from a stress-move-of-spot heuristic. The same estimate powers the Composer's min_capital auto-estimate, reusing the payoff diagram's leg resolution.

Changed

  • Live dashboard's "Capital" label renamed to "Notional Exposure" and centered on the strategy card, to avoid it reading as capital actually committed rather than notional position size.

Fixed

  • Margin estimate for defined-risk spreads was ~10-15x too high: the old flat 20%-of-notional fallback ignored that a condor/butterfly/vertical's risk is capped by its wings, so it priced a defined-risk spread as if it carried naked-strangle-level risk.

[0.26.6] — 2026-07-25

Fixed

  • Installer shipped the developer's own dev strategies to every customer: installer/build.ps1 copied strategies/ wholesale, only stripping legacy raw .py scripts — every composer .spec.json instance (e.g. dib_30jun2026, nifty_strangle_live, rent_collector_condor_dev), their sidecars, metadata.json (real broker/lots/enabled settings), and a 13MB stale instruments_cache/ dump all reached the customer install. Now strips everything in strategies/ except the framework files and the blank strategy_template.py/.json pair, so a fresh install ships with zero pre-configured strategies.

[0.26.5] — 2026-07-25

Added

  • Capital deployed / margin required per strategy on the live dashboard: each strategy card now shows notional capital deployed (spot × open quantity) alongside margin required — a real Kite basket-margin call (KiteConnect.basket_order_margins(), spread-benefit-adjusted) for real-mode strategies, or a 20%-of-notional heuristic for paper mode and brokers without a margin-calculator API (Breeze, Dhan). Cards with no open position show a muted placeholder instead of an empty line.
  • Regime skip filter (regime_skip): vetoes entry when the current market regime — NIFTY trend × India VIX volatility, gated by a USDINR/Crude/FII-DII risk-appetite read — matches a chosen regime. Broker-independent (reads Yahoo Finance + NSE's public endpoints instead of historical_data()), so it works the same under Kite, Breeze, or Dhan. See Regime skip filter.
  • Rent Collector Condor template in the Strategy Composer's Load template dropdown: a weekly premium-target iron condor carried across days via Carry position instead of closed daily, aimed at capital-efficient multi-day theta collection. Documented in docs/composer-strategy-examples.md (example 13).
  • Four more Composer templates, each putting a previously-unused block to work: Gamma-Fenced Daily Condor (0DTE iron condor with shorts placed at constant gamma via gamma_target, backed by a delta_stop), Put Calendar Skew Harvester (put_calendar — the put-side mirror of the existing call calendar, harvesting NIFTY's put-skew richness), Self-Centering Condor (iron condor whose shorts roll on both the tested and untested side via roll_untested's side: both), and Monthly FD-Plus Condor (far-OTM monthly condor sized with capital_pct instead of fixed lots). Documented in docs/composer-strategy-examples.md (examples 14-17).

Fixed

  • Multi-day sim silently discarded carry_eod positions: run_fullday() deleted its state file at the start of every simulated day, so a position a carry_eod block left open overnight was wiped before the next day's subprocess even started — every day re-entered fresh and never realized any P&L, showing as a flat breakeven for the whole backtest. multi_sim.py now carries state forward across days for the session it's running (day 1 still starts clean); a day where a carried position sits open with no entry/exit now shows a distinct Holding status instead of being mislabeled No Trade/Breakeven.
  • diagonal_iron_fly template's long-leg exit was dead code: its profit_target/stop_loss/long_eod exits were never leg-scoped, so the short-leg/long-leg phase split (_long_active) never triggered and long_eod's DTE=1 protection could never fire. Now scoped to ["SC","SP"] and ["LC","LP"] respectively.
  • Multi-sim reports understated win rate for carried positions: compute_summary()'s trades count used entry_taken, which the carry-state fix above (correctly) sets True on both a carried position's entry day (pnl=0) and its later exit day (real pnl) — doubling the trades denominator for every multi-day trade. Now counts exit_reason (set only the day a position actually closes) instead, so win rate reflects real trades, not calendar days touched.
  • SpecEngine._load()/_save() could crash a sim day on a transient file lock: seen as a one-off PermissionError reading the state JSON mid-backtest (repo lives in a synced folder, which can briefly lock a file on write). Both now retry a couple times with a short backoff before giving up.

[0.26.4] — 2026-07-22

Fixed

  • License fingerprint no longer flaps on restart: get_fingerprint() now reads Windows' per-install MachineGuid instead of uuid.getnode() (MAC address). The old scheme was unstable on any machine with more than one network adapter (e.g. Wi-Fi + a VPN like Tailscale) — adapter enumeration order can race the VPN service coming up at boot, silently changing the fingerprint and invalidating an otherwise-valid license until the app was restarted again. Every key issued before this fix needs reissuing against the new MachineGuid-based fingerprint.

[0.26.3] — 2026-07-22

Added

  • POP (Probability of Profit) on the Composer payoff panel: the payoff diagram now shows a Black-Scholes-estimated probability that the structure is profitable at expiry (India VIX as the IV proxy), alongside max profit/loss and breakevens. Computed by integrating the risk-neutral spot distribution over the existing payoff curve grid (strategies/payoff.py), so it works for any leg shape, not just single-breakeven strangles. Also included in the Composer's Ask AI prompt.
  • Option Greeks section on the Composer payoff panel: a dedicated section below the chart shows the net position's Delta/Gamma/Theta/Vega (word + symbol, Black-Scholes estimates at entry, per lot).
  • P&L what-if table: below the payoff chart, a small table shows expiry and mid-trade P&L at spot ±1/2/3%.
  • Gamma target leg selector: new gamma_target block scans an OTM% band for the strike whose Black-Scholes gamma is closest to a target — an alternative to OTM %/premium/delta targeting for strategies that care about rate-of-change of delta.

[0.26.2] — 2026-07-22

Changed

  • Production server swapped from Flask's dev server to waitress: app.run() was never meant for production (single-threaded, no real concurrency); waitress.serve(app, threads=8) replaces it. The debug-reloader double-scheduler guard comment is updated to reflect that the reloader only applies via flask run --debug, not the waitress entrypoint.

[0.26.1] — 2026-07-21

Added

  • Installer warns before an upgrade that breaks licensing: upgrading from any version older than 0.26.0 (pre-Ed25519 licensing) now shows a page, right after Welcome, explaining that the existing license key will stop validating and the app may show trial-expired with no grace period. The user can Cancel there with nothing installed or changed yet, or Next to proceed. Skipped entirely for fresh installs and for upgrades already on 0.26.0+.
  • License status surfaced in the UI: a persistent navbar badge (Licensed / Trial Nd / Trial Expired, linking to /license) is now shown on every page — previously the trial banner in base.html only appeared for trial/trial_expired states, so once a license was activated there was no way to see it was active or when it expired short of guessing the /license URL. The /license page itself now also shows the actual expiry date, not just "days left".

[0.26.0] — 2026-07-20

Fixed

  • False orphan-position alerts for arm-bypassed trades: reconciliation compared a strategy's configured mode (strategy_manager.get_mode()) against the broker, not what a specific trade actually ran as. When the arm-on-restart gate silently downgraded a Real-configured strategy to paper for the day, its state file looked identical to a real trade, so reconcile() kept expecting a broker position that was never placed — flagging a false orphan_state error every ~5 minutes (confirmed live 2026-07-20, DIB_07Jul2026_Kite). Every state-file writer (strategies/engine.py, Lib_v1_1_lot.py, lib_v1_trade.py, niftyStrgl_AI2.py, nifty_strangle_live.py) now stamps the trade's actual exec_mode; reconciliation trusts that per-trade field over the stale config mode when present.
  • Trade Status modal showing closed short legs as live: a SHORT_CLOSED diagonal rendered its original full leg set (SC/SP/LC/LP) with live LTP-based P&L, even though SC/SP are already closed — misleading whenever their cost basis is stale (e.g. paper-entered then real-exited by the arm-bypass bug above). The modal now renders only the still-open _long_active legs once a trade is SHORT_CLOSED.

Changed

  • Arm state now persists across restarts: previously in-memory only, so every restart silently downgraded all Real-mode strategies to paper until manually re-armed (by design, as a safety gate — see docs/security.md). Now saved to .armed.json and reloaded on startup, at the user's explicit request; an armed strategy stays armed through a restart. Unarmed Real strategies still run as paper and now send one notification per strategy per day (previously only a log line), instead of every monitor tick.
  • License keys now Ed25519-signed (licensing.py): the forgeable symmetric HMAC scheme (secret shipped inside the exe) is replaced by Ed25519 signature verification against a compiled-in public-key list; the private key never leaves the operator side. Key format stays expiry|signature, so the License page and activation flow are unchanged. No legacy fallback — existing customers are re-issued keys carrying their current expiry (personal outreach; old keys stop validating on this version). generate_key()/HMAC_SECRET removed; signing moves to tools/generate_license.py. Grace period extended 3 → 7 days.
  • Daily online revocation check: once per day the app asks license.thetaprime.in/v1/license/status (Cloudflare Worker + D1, new license-server/ directory) whether its key was revoked. Fail-open — an unreachable service never disables the app — and a revocation takes effect only at the next app start, never mid-session. New storage.get/set_revocation_state(); cryptography pinned explicitly in requirements.

[0.25.3] — 2026-07-19

Changed

  • Single source of truth for NSE holidays: removed five duplicated hardcoded holiday sets (app.py _MARKET_HOLIDAYS, strategies/engine.py HOLIDAYS_2026, strategies/strategy_template.py HOLIDAYS, the strategy_generator.py AI recipe, and MARKET_HOLIDAYS_2026 in Lib_v1_1_lot.py / nifty_strangle_live.py). All trading-day gates now resolve through the canonical holidays.py calendar (NSE fetch → disk cache → bundled fallback), so a yearly calendar update happens in exactly one place and 2027+ dates stop silently falling through to "not a holiday". SpecEngine(holidays=...) still accepts an injected set for tests; when omitted it now uses the live calendar instead of a frozen 2026 list. Tests pin NSE_HOLIDAYS_JSON to the bundled list via an autouse fixture so date-sensitive gates (e.g. the reconcile-loop holiday test) don't depend on the dev machine's fetched cache.

[0.25.2] — 2026-07-19

Fixed

  • Installer overwriting user config on upgrade: setup.iss shipped config.json (feature flags, safety limits, broker toggles) as a plain file in the bulk copy, so re-running the installer to upgrade silently reset it to build defaults. The installer now detects fresh install vs. upgrade (Inno Setup's standard uninstall-registry check) and only seeds config.json on a fresh install; upgrades keep the existing file. Also skips the EULA page on repeat installs and shows a distinct finish message confirming data/credentials were preserved.

[0.25.1] — 2026-07-19

Fixed

  • Dhan "session expired" spam from a misclassified Data API entitlement gap: confirmed live 2026-07-19 that a Dhan account without Data API access gets error 810 "ClientId is invalid" on marketfeed/charts calls even though the same credentials work fine for Trading API calls (margins/positions/orders) — the adapter was treating this as a dead session (triggering repeated re-login-required alerts) instead of the same permanent entitlement gap already handled for Dhan's other error shape (451/"not subscribed"). Also fixed the underlying cause of the volume of alerts: notifier.send_notification() now de-dups an identical message within a 10-minute window, since a broker adapter with a stuck-bad token was re-sending the same alert on every poll (each request builds a fresh adapter instance, so its own "alert once" guard never carried across requests).

[0.25.0] — 2026-07-19

Added

  • Dhan broker integration: a third supported broker alongside Kite and Breeze — OAuth-style API Key login (like Kite/Breeze) with a manual access-token paste as a fallback, contract-validated before a token is stored, and wired into the JSON-spec strategy/sim engine and multi-day sim. See docs/brokers/dhan.md.
  • Per-broker docs: docs/brokers/ now has a dedicated page per broker (kite.md, breeze.md, dhan.md), split out of the old single setup guide.
  • Real broker icons on the Auth page cards (Kite, Breeze, Dhan), replacing the previous generic styling.
  • Per-broker enable/disable toggle: each Auth card has a switch that dims the card and hides that broker's navbar badge live, without a restart — the navbar shows "No broker enabled" if all are off.

Fixed

  • Dhan contract check falsely marking the session dead: contract_validator.py called Dhan's error-swallowing adapter methods (margins()/positions()/orders()) instead of the raw client — one failed check's side effect was poisoning the other checks with a misleading "session expired" error instead of the real failure reason.

[0.24.0] — 2026-07-17

Added

  • Long straddle / long strangle structures in the Strategy Composer (long_straddle, long_strangle): buy an ATM call+put or OTM call+put on the same expiry — the long-volatility counterparts to the existing short strangle/straddle.
  • short_entry notifications: notifier.py now recognizes short_entry (diagonal short-leg re-entry) and the composer engine's <LEG>_symbol/<LEG>_entry event shape — previously this event type produced no notification at all despite live re-entries firing it.
  • Playwright UI test suite (tests/ui/): browser-driven coverage for the Composer, login, Reports, indicators, strategy toggle, and AI handoff flows, run via npm run test:ui.

Changed

  • strangle/straddle structures renamed to short_strangle/short_straddle for symmetry with the new long variants. Old type keys still resolve everywhere a spec is built or validated via a new legacy-alias registry in strategies/components/base.py, so previously saved strategies keep working unchanged; only the Composer palette shows the new names.

[0.23.3] — 2026-07-17

Fixed

  • Misleading "breeze-connect package is not installed" error: breeze_connect does a blocking network call at import time (downloading ICICI's security master); a transient DNS blip there was permanently marking Breeze unavailable for the process lifetime and surfacing a "not installed" message even though the package was present — confirmed in production 2026-07-15. The import now retries on transient network errors (same backoff already used for runtime Breeze API calls), and if it still fails, the token-test and adapter-init error messages show the real underlying reason instead of the hardcoded "not installed" string.
  • Breeze login silently dropped, bounced to ThetaPrime's own login screen: ICICI POSTs the session token back to /breeze_callback cross-site; the default SameSite=Lax session cookie isn't sent on a cross-site POST navigation (unlike Kite's GET-based callback), so the dashboard's login gate saw no session, redirected to /login, and the incoming token was lost before the callback route ever ran — confirmed in production 2026-07-15, reproduces every time since it's a cookie policy, not a transient failure. /breeze_callback is now exempted from the login gate, same as /login//static//license; the token is still validated against ICICI's API inside the route itself.
  • False "position missing from broker" reconciliation alerts: KiteAdapter.positions() and BreezeAdapter.positions() caught every exception — including a transient/intermittent broker API error — and returned an empty list instead of raising. Reconciliation read that as "the broker genuinely has nothing open" and flagged every active leg as orphaned, even though the position was still open at the broker — confirmed in production 2026-07-16. Both adapters now let a fetch failure propagate, so reconciliation reports it as a check failure instead of a false mismatch.

[0.23.2] — 2026-07-14

Fixed

  • OTM % selector picking ITM strikes for put legs: otm_pct computed the strike the same way for both option types, so a positive percent moved the strike above spot regardless of CE/PE — correct for a call, but in-the-money for a put. It's now option-type aware: positive percent always moves the strike out-of-the-money (above for a call, below for a put); negative moves it ITM. Existing saved strategies (including the live weekly strangle) and Composer templates were migrated to the new sign convention with no change in the strikes they actually pick.

[0.23.1] — 2026-07-13

Added

  • Navbar Nifty 50 ticker: primary source is whichever broker (Kite or Breeze) currently has a fresh session; falls back to NSE's public indices endpoint when neither is authenticated.

Fixed

  • Entry/re-entry state loss on placement failure: leg-placement and tag-reconciliation logic (previously duplicated three times) is now unified in _place_legs. _run_short_reentry was missing the PARTIAL_ENTRY_FAILED state save that run_entry already had — confirmed live 2026-07-13 — so a real, filled leg could be silently dropped from local records on an unreconciled placement failure. Short re-entry also now verifies fills the same way run_entry does.
  • Unreachable long-leg DTE exit: the long_eod exit check only ran once a diagonal's short legs were fully closed, so it never fired while short legs stayed active (e.g. kept getting re-entered) — confirmed live 2026-07-13. It now runs independently every tick.
  • Stale "(exit)" price on reopened legs: a leg reopened via short re-entry or rebuild kept its previous exit-price flag, so the dashboard showed the old closing price/P&L instead of the new position's live price.
  • Dashboard price priority: live LTP now takes precedence over the one-time long-leg snapshot price, which was never refreshed while a long leg was held.
  • Mode filter (All / Paper / Real) selection now persists across Dashboard, Performance, and Reports via localStorage instead of resetting on navigation.

Changed

  • CI now gates strategies/engine.py coverage at 80% — it's the live trading state machine, and each of the bugs above lived in a thinly-covered function there.

[0.23.0] — 2026-07-13

Added

  • Three new Exit blocks in the Strategy Composer: Delta stop (delta_stop) exits when the position's net delta — a Black-Scholes estimate using India VIX as the IV proxy — exceeds ±N, reacting faster to a directional move than a premium-based stop; Volatility stop (iv_stop) exits if India VIX has expanded N% from its level at trade entry (new entry_vix captured on every entry/short-re-entry); Profit target (% of premium) (premium_pct_target) exits at N% of the credit collected at entry instead of a fixed points target, adapting across volatility regimes.
  • Adjustment (roll a leg) block in the Strategy Composer (roll_untested): when one short leg of a strangle/straddle has decayed to a fraction of its entry premium while the other is under pressure, closes the decayed leg and re-sells it at a fresh strike via its own selector, recentring the position. Guarded by an inversion check (won't cross the other leg's strike), a per-trade roll cap, and a cutoff time; scoped to plain two-short-leg positions only. Ships with a worked example (nifty_strangle_adjustment_12jul2026, disabled by default) and a matching composer template.
  • Adjustment side param (untested default / tested / both): the same block can now roll the pressured leg instead (further from spot, once it reaches trigger_mult× its entry price) or check both legs each tick and roll whichever qualify — reuses the existing roll plumbing, no new block.

[0.22.0] — 2026-07-11

Added

  • Broker API contract check on login: Kite and Breeze tokens are now validated against the read-only endpoints the app depends on (margins/positions/orders for Kite; funds/portfolio/order list for Breeze) immediately after exchange. A failing check means the token is not saved, and the Auth page names which feature would have broken — catching a broken/changed broker API before the market opens instead of mid-strategy.
  • Carry position (DTE exit) block in the Strategy Composer (carry_eod): overrides the previously-unconditional daily EOD close, letting the whole open position (not just short legs — e.g. an iron condor's wings ride with the shorts they hedge) carry overnight until a configurable DTE trigger (0 = expiry day) is reached. Not leg-scopable by design.

[0.21.0] — 2026-07-11

Added

  • Ask AI (Phase 0, browser handoff) on Reports, Performance, Multi-Sim results, and the Strategy Composer: builds a context-rich prompt server-side (ai_prompts.py) and hands it to the user's own ChatGPT, Claude, Perplexity, Gemini, or Copilot session via URL or clipboard. No API key stored, no provider calls from the app.

Fixed

  • The background orphan-position reconciliation now only runs on trading days between 09:15 and 16:00 IST. Outside market hours the broker's position book can be stale or incomplete — a Saturday Kite session omitted two genuinely open NRML long legs, producing a false orphan alert every 5 minutes all day. Positions can't change while the market is closed, so nothing is lost; the window extends past the 15:30 close to still catch broker EOD square-offs. Manual /api/reconcile is unaffected.

[0.20.0] — 2026-07-10

Added

  • Iron butterfly, single-leg, and vertical spread structures in the Strategy Composer: iron_butterfly (ATM-shorted iron condor variant), single-leg blocks (short/long call, short/long put), and credit/debit vertical spreads.
  • Template loader in the Strategy Composer: a dropdown to load pre-built recipes (matching docs/composer-strategy-examples.md) into the working draft. Selecting one only fills the browser draft — nothing is saved or schedulable until reviewed and explicitly saved.

[0.19.1] — 2026-07-10

Fixed

  • reconciliation.py looked up a strategy's real/paper mode with a hardcoded .py extension, so spec-based (Composer) strategies always resolved to paper and had their parked long legs (held after SHORT_CLOSED) dropped from the comparison — the broker's genuinely open positions then showed up as false orphan_broker alerts.
  • Daily loss limit check now normalizes the configured threshold's sign when read, instead of trusting whatever sign is stored in config.json. A stale/hand-edited positive value inverted the comparison so that any profit under the threshold counted as a breach — today a +26.1pt winning trade falsely disabled all real strategies for the rest of the day.

[0.19.0] — 2026-07-10

Added

  • Iron condor structure in the Strategy Composer: short strangle (SC/SP via any selector) with bought wings auto-placed at short strike ± a configurable width (wing_pts). ATM selectors on both shorts make it an iron butterfly. Wings carry a Black-Scholes fallback price and are placed before the shorts for spread margin.
  • Composer Strategy Examples doc (docs/composer-strategy-examples.md, in the in-app docs sidebar): validated ready-to-build recipes for the 9:20 straddle, weekly premium-target strangle, VIX-regime straddle, iron condor, iron butterfly, and diagonal iron butterfly, plus a roadmap of shapes not yet expressible.

Changed

  • Strategy Composer guide's block catalog refreshed to cover all registered blocks (VIX floor, combined VIX+gap filter, premium target selector, time-locked/soft/per-leg-price/long-leg exits, re-entry).

Fixed

  • Composer UI: structure blocks with their own parameters (e.g. iron condor's wing width) now get a params object on add, so the Block settings panel can edit them.

[0.18.0] — 2026-07-09

Added

  • Close paths (manual square-off, closing long legs, expiry exit, re-entry rollback) now confirm each leg's fill and roll back / flag anything unconfirmed, reusing the same tag-reconciliation and LIMIT-conversion machinery entries use — previously these were fire-and-forget, with exit price taken from the last LTP snapshot rather than the real fill.
  • place_order() on both broker adapters accepts an order tag so a leg can be found at the broker after place() raises but the order actually went through (a network drop between broker acceptance and our response).
  • Kite and Breeze adapters can both rescue a MARKET order stuck OPEN on a thin book by cancelling it and re-placing as a marketable LIMIT priced off live depth.
  • Dashboard shows live entry progress (order placed / fill poll attempt / rollback) since a real entry can now take up to a minute of fill polling.
  • DESIGN.md / PRODUCT.md: design-system reference for the /impeccable skill.

Changed

  • Breeze rate-limit responses now retry with backoff like other transient errors; poll interval raised to 3s per leg so concurrent multi-leg fill polling stays under ICICI's account-wide rate cap.
  • Loss-limit config values (points / rupees) are normalized to a signed magnitude, so typing 75 and -75 behave identically.

Fixed

  • reconciliation.py now also checks a SHORT_CLOSED trade's held long legs against the broker — previously only ACTIVE state's top-level legs were reconciled, missing the case where the broker's own EOD MIS square-off silently closed a held long leg with nothing catching it.

[0.17.0] — 2026-07-08

Added

  • Breeze adapter resilience against ICICI server-side failures (2026-07-07 saw 114 non-JSON 5xx responses and a mid-day session expiry): read-only calls retry transient errors with short backoff; session expiry is detected, alerted once (App Event Log + notification), and fails fast until re-login; ltp() serves a last-known price (≤90s old) through blips; ambiguous place_order failures verify against the order book before a single retry, preventing both duplicate and silently missing legs; sustained outages raise a "Breeze degraded" event after 5 consecutive failures.
  • Trade preview now shows each leg's delta alongside its price.
  • Order placement now waits for full confirmation before proceeding: every leg's fill is polled to a terminal broker status; if a leg doesn't confirm filled, the entry rolls back — closing any legs that did fill and cancelling any still-open orders — instead of leaving a partial, unhedged position with no record of it.
  • Diagonal entries place both long legs (LC, LP) before either short leg, and confirm both are real filled positions before ever placing a short — a broker's margin engine needs an actual filled long, not just a pending order, to grant spread margin on the paired short.
  • Dashboard shows a clear "ENTRY REJECTED — rolled back" status with per-leg outcome (closed / cancelled / still-rejected) for a PARTIAL_ENTRY_FAILED trade, instead of an ambiguous unlabeled state.
  • trade_reset now refuses to clear live state while a position is active, or when a partial-entry rollback itself had errors — forces manual verification against the broker first.
  • Scratch parameter-override panel in the Trade Status modal: tweak a leg's min_delta / time-value caps for a single Preview run without saving to the strategy, for finding delta-target parameters that actually have a live quote right now.

Changed

  • Kite place_order() sets market_protection on MARKET orders (Zerodha rejects MARKET orders via the API without it) and retries as a marketable LIMIT order — priced off live order-book depth, not the (often stale) last-traded price — when Kite rejects a MARKET order outright for having no recent trade history.

Fixed

  • delta_target selector could silently drop its min_delta requirement when fallback_tv_cap_1 collided with min_time_val, producing a zero-width matching band.
  • Stale broker quotes priced below an option's intrinsic value (impossible for a live market) were treated as real prices, corrupting strike selection; now rejected as no-quote.
  • A second, unvalidated price re-fetch in entry/preview could silently overwrite an already-validated selection price with a stale quote for the same strike, mislabeling it "live."

[0.16.1] — 2026-07-07

Fixed

  • Kite margin tag showed ₹0 available despite real account funds — available.cash reads 0 when funds sit as collateral rather than raw cash; now uses net, the same figure Kite's own app displays as "Available Margin".

[0.16.0] — 2026-07-07

Added

  • Manual "Preview Entry" / "Place Entry Now" controls in the Trade Status modal — resolves what a real entry would do (legs, strikes, and whether each price is a live broker quote or a fallback) without placing anything, so a real entry can be gated on live quotes actually being available.

Changed

  • BreezeAdapter.ltp() batches option-chain lookups via get_option_chain_quotes() (one call per expiry/right) instead of one get_quotes() call per strike, keeping large delta-target scans under ICICI's documented 100 calls/minute limit.
  • India VIX is now sourced from NSE's public API for Breeze strategies, since Breeze cannot quote it directly.
  • A real-entry placement failure now saves whatever legs did fill into state as PARTIAL_ENTRY_FAILED and blocks further auto-entry until manually reconciled, instead of losing track of a partially-filled real position.
  • Crash notifications now quote the last lines of stderr (the actual exception) instead of the first (often unrelated warning spam).

Fixed

  • Stale scheduled job ids weren't migrated when a strategy's filename-derived base changed, leaving duplicate entry/monitor cron job pairs.
  • Breeze's India VIX product_type was invalid ("others"), causing quote failures.
  • BreezeAdapter.place_order() crashed instead of raising a clear error when Breeze returned a plain string in Success (an error message) rather than the documented order-confirmation dict.
  • Breeze's product value for NFO options was sent as Kite's "MIS"/"NRML", which ICICI silently rejected as "Quotes not available"; now sends "options", the only value Breeze's place_order accepts for NFO.
  • The diagonal structure placed its short put (SP) before its hedging long put (LP), so the broker priced SP as a naked short requiring full margin instead of recognizing the spread; every leg's BUY now precedes its same-side SELL.
  • The atm selector had no fallback price of its own (unlike delta_target/premium_target), so a single flaky broker quote could abort an entire entry; it now carries the same Black-Scholes estimate.
  • Real-entry leg prices now all resolve before any order is placed, closing a partial-fill window where a late missing quote could abort mid-sequence after earlier legs had already filled for real.
  • The Trade Status modal showed an empty legs table for a PARTIAL_ENTRY_FAILED trade instead of the legs that actually filled.
  • Clicking Preview Entry / Place Entry Now stacked a second Bootstrap modal on top of the Trade Status modal instead of rendering inline; opening either modal now closes the other first.

[0.15.1] — 2026-07-06

Added

  • BrokerClient.order_fill() (Kite + Breeze) reads back real order status, filled quantity, and average price for a placed order.

Fixed

  • Strategies built from strategy_template.py assumed a market order fully filled at the pre-trade quote; entries now poll the broker for the actual fill, and exits close only the quantity that actually filled instead of always closing the full requested size — previously a partial fill on entry could get flipped to an unintended opposite position on exit. calc_pnl() now weights each leg by its own filled quantity so mixed fill sizes across legs still net correctly.
  • reconciliation.reconcile() matched broker positions to strategy state by symbol + direction only, so two strategies holding the same strike (or a partial fill) could both silently match a single broker position line even if the combined quantity didn't actually reconcile. It now sums each strategy's leg quantity per symbol before comparing to the broker's netted line.

[0.15.0] — 2026-07-06

Added

  • Shared toast()/confirmDialog() UI feedback module (static/ui-feedback.js), built on Bootstrap's own Toast/Modal components, replacing native alert()/confirm() which didn't respect the app's dark theme. Adopted across the data management, index, multi-sim results, option history, performance, reports, and strategy builder pages.
  • Option history lookup now supports Breeze as well as Kite via a broker parameter, fetching candles through BreezeAdapter when requested.

Changed

  • Extracted scheduler.strip_strategy_ext() as the single source of truth for deriving a strategy's job-id/state-file base from its filename, used by both add_strategy_realtime_jobs() and app._trade_state_base() so the two can no longer drift apart.

[0.14.17] — 2026-07-06

Added

  • Composer UI now shows the dashboard's live lots value for fixed_lots sizing (readonly, mirrored from the strategy's dashboard config) instead of the spec's saved default, so the two can no longer visibly disagree.

Fixed

  • Running a Breeze-backed strategy without configured Breeze API credentials failed deep inside BreezeAdapter with a raw KeyError. app.py now checks upfront and returns a clear 400 error, and BreezeAdapter itself raises a RuntimeError with guidance if constructed without credentials.
  • fixed_lots sizing read its own spec params.lots instead of ctx.lots (the dashboard/STRATEGY_LOTS value), so changing lots in the dashboard silently had no effect on live sizing for Composer strategies. It now uses ctx.lots as the single source of truth.

[0.14.16] — 2026-07-06

Fixed

  • Trade status modal footer kept showing a stale Exit HARD_EXIT / Short P&L snapshot from the moment short legs closed, even while long legs stayed open and kept moving — the backend only writes total_pnl once the entire trade closes, so the footer never had a current figure to fall back to. It now shows a single Total P&L computed live from the same per-leg P&L already rendered in the legs table.

[0.14.15] — 2026-07-06

Fixed

  • A .spec.json-based strategy (e.g. Composer strategies) stopped being monitored after any server restart while its real-time jobs were registered — confirmed in production when nifty_strangle_composer_02jul2026.spec.json entered a trade at 10:13, then sat unmonitored for ~95 minutes after a 10:13 restart. Root cause: scheduler.py's _refresh_realtime_jobs(), run on every scheduler start to re-register jobs, rebuilt each strategy's filename from its job ID via base + '.py'. That round-trips for plain .py strategies, but a spec strategy's job ID is <name>.spec.json_entry, so this produced <name>.spec.json.py — a key absent from metadata.json, causing get_enabled() to default to False and every monitor tick to silently skip as "disabled". _refresh_realtime_jobs() now reads the strategy filename directly from the job's stored args[0] instead of reconstructing it.

[0.14.14] — 2026-07-06

Fixed

  • Running a plain .py strategy (not a .spec.json) on a compiled install failed with an argparse error like unrecognized arguments: --run-strategy-subprocess <path>. Root cause: app.py's --run-strategy-subprocess handler runs legacy strategies via runpy.run_path(..., run_name='__main__'), but left sys.argv as ['app.exe', '--run-strategy-subprocess', <path>] — the strategy script's own argparse.parse_args() then choked on those wrapper args. sys.argv is now reset to [<strategy_path>] before runpy.run_path, matching the uncompiled python strategy_path invocation.

[0.14.13] — 2026-07-05

Fixed

  • Running/simulating a spec-based (Composer) strategy on a compiled install silently failed (recorded as rc=2, no useful error) — confirmed via production log during a fullday simulation. Root cause: runner.py spawns the strategy as a subprocess via [sys.executable, '-m', 'strategies.engine', spec_path], but in a compiled build sys.executable is app.exe itself, not a real python.exe — it has no -m/generic-script support, so this silently launched a second copy of the Flask server instead of the strategy engine. app.py now intercepts a --run-strategy-subprocess <path> flag at the very top of the module, before any of its own heavy top-level side effects (scheduler.start() etc.), and dispatches directly into strategies.engine.run() (spec strategies) or runpy.run_path(..., run_name='__main__') (legacy .py strategies) instead of starting the server. runner.py uses this new flag only when compiled; dev-mode invocation is unchanged.

[0.14.12] — 2026-07-05

Fixed

  • Running/simulating a strategy failed with FileNotFoundError: C:\...\ThetaPrime\strategies\<name>.spec.json on a compiled install — runner.py's run_strategy() built the strategy file path from os.path.dirname(__file__) (the install directory in a compiled build) instead of config.STRATEGIES_DIR (DATA_ROOT), even though the same function already imports STRATEGIES_DIR and uses it correctly elsewhere. __file__-based base_dir is still used for PYTHONPATH (needed so the strategy subprocess can import strategies/broker/config, which do live in the install directory) — only the strategy file's own path was wrong.

[0.14.11] — 2026-07-05

Fixed

  • Breeze connection still failed after 0.14.10 with the same module 'config' has no attribute 'SECURITY_MASTER_URL', even with breeze_connect correctly present as a real on-disk package. Root cause: our own config.py is Nuitka-compiled (unlike strategies/broker/breeze_connect, it isn't excluded from compilation), so sys.modules.pop('config', None) doesn't help — the next import config falls through to sys.meta_path, where Nuitka's own finder recognizes 'config' as one of its compiled modules and satisfies it directly, without ever consulting sys.path, no matter what's inserted there. broker/breeze_adapter.py no longer pops-and-hopes; it loads breeze_connect's own config.py from disk via importlib.util.spec_from_file_location and places it directly in sys.modules['config'] before from breeze_connect import BreezeConnect runs — Python checks the sys.modules cache before consulting any finder, so breeze_connect's internal import config resolves correctly without Nuitka's finder ever being asked.

[0.14.10] — 2026-07-05

Fixed

  • Build itself failed with module 'config' has no attribute 'SECURITY_MASTER_URL' — self-inflicted in 0.14.7's own build.ps1: locating breeze_connect's directory used python -c "import breeze_connect, ...", which actually imports and executes it (its module level runs urlopen(config.SECURITY_MASTER_URL)), and from the repo root cwd, breeze_connect's bare import config resolved to our own config.py instead of its bundled one — the exact shadowing bug this whole fix chain exists to solve. Now uses importlib.util.find_spec('breeze_connect').submodule_search_locations[0], which locates the package without executing it.

[0.14.9] — 2026-07-05

Changed

  • 0.14.8 missed some of pandas's test-only surface: pandas.conftest (pytest fixtures), pandas._testing (internal test helpers), and pandas.util._tester still triggered anti-bloat warnings and slowed the build. Confirmed neither our code nor pandas's own runtime paths use these (only pandas's public pandas.testing API re-exports from _testing, which we never call) — excluded from compilation, not just pandas.tests and pandas.util._test_decorators.

[0.14.8] — 2026-07-05

Changed

  • Build time regression from 0.14.7's --include-package=pandas: it forces Nuitka to bundle pandas's entire tree indiscriminately, including its own internal dev test suite (pandas/tests/, plus pandas/util/_test_decorators.py), which pulls in pytest/pydoc and noticeably slows compilation for no runtime benefit. build.ps1 now excludes both from compilation.

[0.14.7] — 2026-07-05

Fixed

  • 0.14.6's fresh install still showed Option Scanner / Strategy Builder enabled (should be off for release) and had no strategies visible after saving one via Composer. Root cause: DATA_ROOT/config.json never gets created on a compiled install (nothing seeds it there — the repo's config.json only lands in the install directory, a different path from DATA_ROOT), so app.py's _load_features() was silently falling back to its own hardcoded defaults, which didn't match the shipped config.json. Fallback now matches the intended release values (option_scanner/strategy_builder off).
  • Composer-saved strategies were being written to the install directory instead of DATA_ROOT. strategies/spec.py had its own duplicate STRATEGIES_DIR = os.path.dirname(os.path.abspath(__file__)) instead of using config.STRATEGIES_DIR — and since strategies/ is deliberately excluded from Nuitka compilation (--nofollow-import-to=strategies, so it stays plain, dynamically-editable Python), __file__ resolved to the install directory, not DATA_ROOT. Now imports STRATEGIES_DIR from config like everything else does.
  • Breeze connection test failed with "package not installed" — the real error, logged but swallowed into a generic warning: module 'config' has no attribute 'SECURITY_MASTER_URL'. breeze_connect's SDK does a bare import config expecting its own bundled config.py to shadow ours (broker/breeze_adapter.py works around this via a temporary sys.path/sys.modules swap) — but breeze_connect was --include-package'd, so Nuitka compiled it into the binary, leaving no real on-disk directory for the swap to point at. build.ps1 now excludes it from compilation too (--nofollow-import-to=breeze_connect) and copies the real package directory into the distribution, same treatment as strategies/broker; pandas and socketio (breeze_connect's own runtime deps, previously only bundled as a side effect of statically following into breeze_connect) are now included explicitly so excluding it from that static-follow analysis doesn't silently drop them.

[0.14.6] — 2026-07-05

Fixed

  • Fresh installs on a clean machine still landed DATA_ROOT somewhere invisible to the user — confirmed via the diagnostic log line: DATA_ROOT=C:\WINDOWS\system32\config\systemprofile\AppData\Roaming\ThetaPrime. The __compiled__ detection itself was working correctly; the problem was that nssm runs the ThetaPrimeDashboard service as LocalSystem by default, and %APPDATA% for that account resolves to the hidden systemprofile profile, not any real user's. config.py's _data_root() now uses %ProgramData% for the compiled/frozen path — the standard Windows location for a service's own data, independent of which account runs it.

[0.14.5] — 2026-07-05

Fixed

  • /api/today-pnl (and any other lazy from config import ...) could intermittently 500 with ImportError: cannot import name 'DATA_ROOT' from 'config', resolving to breeze_connect's bundled config.py instead of ours. Root cause: broker/breeze_adapter.py works around breeze_connect's SDK doing a bare import config internally by temporarily popping sys.modules['config'] and prepending breeze_connect's own directory to sys.path — but (a) breeze_connect's own import machinery also inserts its directory into sys.path independently, so removing only our own insert left a duplicate behind permanently, and (b) if this workaround ran before our own config.py had been imported anywhere in the process, there was nothing to restore, so the pop left config out of sys.modules entirely with the polluted sys.path still in place — meaning any later import config anywhere would resolve to breeze_connect's copy instead of ours. broker/breeze_adapter.py now snapshots and restores the full sys.path list rather than trying to track a single insert/remove. app.py also now eagerly imports broker.breeze_adapter right after config (before the startup-reconciliation background thread or any request thread can start), closing the narrow window where a concurrent from config import ... could still race the temporary pop.

[0.14.4] — 2026-07-05

Fixed

  • 0.14.3's sys.executable-based fix for the installer's data path still wrote to Program Files instead of %APPDATA%\ThetaPrime: the new startup diagnostic log line showed DATA_ROOT resolving to the repo-relative branch even though sys.executable printed as app.exe later in the same run — meaning sys.executable's value at config.py's import time didn't match what it read later, so it isn't a dependable signal after all. _data_root() now checks Nuitka's __compiled__ marker on sys.modules['__main__'] (the compiled entry point, app.py) instead — a documented Nuitka guarantee that doesn't depend on import-order timing, unlike checking __compiled__ on config.py's own module globals (which 0.14.2 tried and which doesn't propagate reliably to non-entry modules) or sys.executable (which 0.14.3 tried).
  • The startup diagnostic log line now also reports main.__compiled__ so any future data-root regression is diagnosable from the log alone.

[0.14.3] — 2026-07-05

Fixed

  • Packaged installer ran in dev mode (exposing dev-only nav items, endpoints, and disabling login on localhost) because a standalone install has no .git directory: git rev-parse failed silently (non-zero exit, no exception) and the empty branch name fell through to 'dev' instead of 'prod'. _detect_env() now treats a failed git lookup as 'prod'.
  • Composer's pre-seeded "ATM Strangle" example strategy failed to save with unknown parameter for 'atm'. The default leg selector (alphabetically first in the catalog, 'atm') was never updated to 'otm_pct' when the seed template set its pct params, so validation rejected it — this blocked creating/enabling any strategy on a fresh install.
  • 0.14.2's __compiled__-based fix for the installer's data path (writing to Program Files instead of %APPDATA%\ThetaPrime) didn't actually work — _data_root() now keys off sys.executable instead, which Nuitka standalone reliably replaces with the compiled binary itself (a documented, deliberate behavior, unlike __compiled__'s per-module injection).

[0.14.2] — 2026-07-05

Fixed

  • Packaged installer wrote all app data (config, secrets, strategies, databases) next to app.exe in Program Files instead of %APPDATA%\ThetaPrime, because Nuitka standalone builds never set sys.frozen (a PyInstaller/cx_Freeze-only convention) — _data_root() now also checks for Nuitka's __compiled__ marker.

[0.14.1] — 2026-07-05

Changed

  • The reconcile badge now shows the affected symbols (broker-only vs state-only) directly beneath it on a mismatch, instead of only in a hover tooltip.

Fixed

  • Reconcile falsely flagged every active paper-mode strategy as an orphan position, since it compared paper trades (which never touch the broker) against broker positions like real ones. Paper-mode legs are now excluded from the comparison entirely.

[0.14.0] — 2026-07-05

Added

  • Orphan-position reconciliation now runs periodically in the background (every 5 min, not just at startup), gated by a new Safety Settings toggle ("Automatic orphan-position reconciliation", on by default).

Changed

  • Replaced the dashboard's orphan-position red banner + Reconcile button with a small status badge next to Strategies (gray/green/red), which also auto-refreshes every minute.

Fixed

  • Breeze connection test (/test_breeze_session) failed with AttributeError: ... has no attribute 'SECURITY_MASTER_URL' because it imported breeze_connect directly instead of going through the existing guarded import — now routes through the same adapter path used elsewhere.
  • Reconcile failed with EnvironmentError: API_KEY env var is not set when run from the web UI (as opposed to a strategy subprocess) because get_broker() never injected stored credentials into the process environment. It now does, for both Kite and Breeze.
  • Reconcile falsely flagged every open Breeze option position as a mismatch: BreezeAdapter.positions() returned the bare underlying (stock_code) instead of the composed Kite-style tradingsymbol reconcile compares against. It now reconstructs the full symbol from stock_code/expiry_date/strike_price/right.

[0.13.0] — 2026-07-04

Added

  • Orphan position detection: compares active state files against the broker's live positions and alerts on any divergence (dashboard banner + notification), without ever auto-closing a position. See docs/security.md.
  • Data Management page (/data): download/restore a full backup of app data (auto-snapshots before restore), plus one-click simulation and temp-file cleanup. See docs/data-management.md. (#82)

Changed

Fixed

  • performance.db, run_history.db, sim_sessions.db, and the app event log now live under DATA_ROOT instead of the source directory, so they're included in backups and land in %APPDATA%\ThetaPrime in the packaged installer like the rest of the app's data.

[0.12.8] — 2026-07-04

Changed

  • installer/publish-release.ps1 now always sets a dedicated product identity (Deepak Sharma <info@thetaprime.in>) for commits to ThetaPrime-Dist, instead of falling back to the personal git identity used for the main source repo. Note: the GitHub Release itself is still attributed to whichever account's token created it (GitHub's account model, not something git config controls).

[0.12.7] — 2026-07-04

Fixed

  • installer/publish-release.ps1's $ErrorActionPreference = "Stop" doesn't actually halt on a failed native command (git commit, git push, etc.) — discovered when the v0.12.6 publish silently continued past a git commit failure (no git identity configured in the freshly-cloned ThetaPrime-Dist checkout) and reported success despite version.json never actually being updated. Added explicit $LASTEXITCODE checks after every native git call, and the script now sets a local git identity in the clone if none is configured.

[0.12.6] — 2026-07-04

Changed

  • Removed release.yml and windows-build.yml: a real v0.12.5 release attempt showed Nuitka's --standalone C-compile takes 2+ hours (still running when cancelled) on a hosted GitHub Actions Windows runner — it never completed a build there. The installer build and ThetaPrime-Dist publish step now always run locally: installer\build.ps1 then the new installer\publish-release.ps1 (tags main, creates the GitHub Release + uploads the installer, commits a fresh version.json). installer/build_version_json.py extracts the manifest-building logic previously inlined in the removed workflow. DIST_REPO_TOKEN is no longer used by anything.

[0.12.5] — 2026-07-04

Fixed

  • release.yml and windows-build.yml never installed the project's Python dependencies before running installer/build.ps1 — the Nuitka compile step (added in 0.12.2) has been failing with No module named nuitka since it was introduced, masked until now by the earlier nssm.cc failure in the same script. Both workflows now pip install -r requirements-prod.txt before building.

[0.12.4] — 2026-07-04

Fixed

  • installer/build.ps1 had a hard runtime dependency on nssm.cc being up during every CI release build, with no fallback — an outage there (503 observed during the v0.12.3 release attempt) hard-fails the whole release. Vendored installer/nssm.exe (NSSM 2.24, verified 64-bit binary) into the repo; the existing Test-Path check already skips the download when the file is present, so CI no longer depends on nssm.cc's uptime at all.

[0.12.3] — 2026-07-04

Fixed

  • square_off() and close_long_legs() read/wrote active-trade state from the app's install directory (BASE_DIR) instead of config.STRATEGIES_DIR (%APPDATA%\ThetaPrime\) — invisible in dev (the two paths coincide when not frozen) but would silently lose state on every update in a real Nuitka-frozen install (app.py)
  • Same bug in runner.py: the max-lots safety clamp read config.json from the install directory, and the pre-entry re-entry guard read active-trade state from the install directory — both silently no-op via a bare except: pass in production, meaning the lot cap and duplicate-entry guard could be silently disabled after an update
  • Removed a dead, duplicate update-available banner in templates/base.html that polled /api/version/check client-side and pointed at a non-functional "your account portal" link; the server-rendered banner (with a working Download link) already covers this

Changed

  • release.yml now publishes the GitHub Release and version.json to the public dsharma01/ThetaPrime-Dist distribution repo instead of the private source repo, matching what app.py's version-check already reads from
  • Deploy process reconciled: CLAUDE.md's routine deploy checklist now updates CHANGELOG.md on every version bump; tagging/installer publish is documented as a separate, occasional "commercial release" step, not part of every deploy. docs/dev/release-strategy.md and windows-build.yml updated to match — the release/* branch ceremony was dropped since it wasn't actually being followed
  • Backfilled CHANGELOG.md entries for 0.11.0–0.12.2, which had been skipped since the deploy process forked from the documented release flow

[0.12.2] — 2026-07-04

Changed

  • Installer build (installer/build.ps1) now compiles core app modules with Nuitka (--standalone) instead of shipping plaintext .py + a bundled venv, so the distributed installer no longer ships readable source for the core app. strategies/ and broker/ are excluded from compilation and still ship as plain Python (dynamically editable). nssm_setup.bat and requirements-prod.txt updated to match.

[0.12.1] — 2026-07-03

Fixed

  • Widened delta_target fallback bands and fixed the spec engine's day-done lookup (strategies/engine.py, strategies/components/selectors.py)
  • Strategy composer UI now displays a spec strategy's configured params instead of leaving them blank (templates/index.html)

[0.12.0] — 2026-07-03

Added

  • Re-entry component and day-candle caching for the strategy composer framework (strategies/components/reentry.py, broker/day_candle_cache.py), plus expanded exit and selector blocks (strategies/components/exits.py, selectors.py)
  • Deploy verification steps and a branch/merge safety protocol documented in CLAUDE.md

Fixed

  • Restored the animated splash login page, lost during the trial-licensing merge (templates/login.html)

[0.11.0] — 2026-07-02

Added

  • Trial-based licensing: 15-day simulation-only trial, no activation required on first install (licensing.py, templates/license.html)
  • Update management: config.py adds a DATA_ROOT constant separating user data (%APPDATA%\ThetaPrime\) from the app install directory; a background thread checks a hosted version manifest and shows an in-app update banner
  • Strategy engine: short re-entry support, VIX + gap filter, broker-agnostic trade LTP fetching (broker/sim_ltp.py)
  • Windows installer: NSSM service integration fixes (quoted AppParameters, .pth-based sys.path setup for the embeddable Python build), auto-cleanup of the staging folder after build, bundled installation guide PDF
  • Docs restructured — superseded strategy docs moved to docs/archive/, new docs/dev/installation.md
  • CSV export for dashboard tables (static/table-csv-export.js)

Fixed

  • Update manifest's download_url validated as https:// before being injected into the banner link
  • Version comparison now uses tuple comparison instead of lexicographic string comparison, avoiding an ordering bug (e.g. "0.9.0" > "0.10.0" as plain strings)

[0.10.2] — 2026-06-30

Fixed

  • Trade Status modal showed the wrong strike for composer/spec-engine trades (e.g. 723900 instead of 23900) and never showed Delta/IV for long legs — leg selectors now return strike/delta/iv_pct and the engine persists them instead of relying on the frontend to regex-parse the strike out of the symbol
  • Same strike-display bug in nifty_strangle_live.py — its dashboard leg dict now includes strike from the live position
  • Manual square-off computed zero P&L for strategies (nifty_strangle_live.py) that store leg symbols with the NFO: exchange prefix already included — square_off() no longer double-prefixes the symbol when fetching LTP
  • nifty_strangle_live.py could go unmonitored for the rest of the day: a monitor tick at MIN_ENTRY_TIME that ran before that day's entry would mark the day complete and never get cleared, silently skipping every subsequent monitor tick even with a live position open
  • Scheduler hardcoded every strategy's entry job to fire at 09:45 IST regardless of its own entry_time param, so nifty_strangle_live (entry_time 10:00) always self-rejected its scheduled entry — add_strategy_realtime_jobs() now reads each strategy's configured entry_time
  • Strategy list no longer lists payoff.py as a runnable strategy

0.5.0 — 2026-06-21

Added

  • Multi-day simulation runner (multi_sim.py) — iterates over a date range, runs a full-day sim per trading day, and stores results in sim_sessions.db
  • sim_sessions.py — SQLite store for multi-day simulation sessions and per-day P&L results
  • templates/multi_sim_results.html — results page showing per-day breakdown and aggregate stats
  • holidays.py — NSE holiday calendar fetched from NSE public API, cached in .nse_holidays.json, with bundled fallback
  • config.json — feature flags file (option_history, option_scanner) to enable/disable sections at runtime
  • Strategy parameter JSON files (lib_v1_trade.json, niftyStrgl_AI2.json, nifty_strangle_live.json, strategy_template.json) as companion metadata for each strategy
  • runner.py multi-sim API endpoints to start/cancel/poll multi-day simulation sessions
  • strategy_manager.py improvements for parameter schema management

Changed

  • sim_engine.py skips NSE holidays automatically via holidays.is_trading_day()
  • broker/breeze_adapter.py and broker/sim_ltp.py reliability improvements
  • Strategy files (lib_v1_trade.py, niftyStrgl_AI2.py, nifty_strangle_live.py, strategy_template.py) updated to use parameter JSON schemas
  • templates/index.html and templates/reports.html overhauled with expanded controls and improved layout
  • static/style.css extended with multi-sim and reports page styles

Removed

  • strategies/niftystrangle_aiGen.py — superseded by niftyStrgl_AI2.py

0.4.2 — 2026-06-20

Added

  • Broker dropdown in the Simulate modal — pre-selects the strategy's configured broker (Kite/Breeze) but allows per-run override without touching metadata.json
  • runner.run_strategy() accepts broker_override param that takes precedence over the per-strategy metadata setting
  • broker/__init__.get_broker() logs Broker selected: kite/breeze at startup so logs are unambiguous
  • KiteAdapter.__init__ logs KiteAdapter: session ready (mirrors existing BreezeAdapter session log)

0.4.0 — 2026-06-20

Added

  • Dark/Light theme toggle with localStorage persistence
  • templates/base.html — Jinja2 base template with shared navbar and theme toggle button
  • static/theme.js — Theme switcher logic (☀ï¸�/🌙 icon, instant theme application)
  • Light mode CSS tokens and navbar overrides in static/style.css
  • All templates refactored to extend base.html for consistent UI
  • Option Scanner page with premium % filtering
  • scanner.py module: scan NFO options by premium as % of spot price
  • Two scan modes: specific underlying or top N F&O stocks (by value/volume)
  • IV (implied volatility) and delta calculation for each option
  • Sortable results table with CE/PE row highlighting
  • /api/option_scan/* endpoints for expiry listing, scanning, and top stocks
  • Inline runner status per-strategy on the main dashboard
  • Full-day simulation engine fixes and improved reliability
  • WhatsApp notification toggle (set_notifications_enabled / is_notifications_enabled in storage)
  • Notification toggle UI on Auth page with live HTTP status feedback
  • sim_engine.py smoke tests

Changed

Fixed


0.3.0 — 2026-06-18

Added

  • Multi-broker support: broker/ adapter package with BrokerClient ABC and BrokerConstants dataclass
  • KiteAdapter wraps the existing KiteConnect flow; strategies continue to work unchanged
  • BreezeAdapter wraps ICICI Breeze API with automatic NFO symbol parsing (weekly + monthly expiry)
  • Per-strategy broker selection via "broker" field in strategies/metadata.json
  • runner.py injects BROKER, BREEZE_API_KEY, BREEZE_API_SECRET, BREEZE_SESSION_TOKEN into strategy subprocesses
  • Breeze credential storage in keyring (set_breeze_credentials, get_breeze_credentials, set_breeze_session_token)
  • Auth page Breeze card: API key/secret form, session token form, and connection test button
  • Per-strategy broker selector dropdown on the main dashboard
  • PORT env var support in app.py for flexible deployment
  • pytest.ini with pythonpath = . so pytest resolves project-root modules in CI
  • PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring in CI workflow to prevent NoKeyringError on Linux

Changed

  • lib_v1_trade.py, nifty_strangle_live.py, and strategy_template.py now use get_broker() / BC constants instead of importing KiteConnect directly

0.2.0 — 2026-06-14

Added

  • version.py as single source of truth for application version
  • Tag-triggered GitHub Release workflow with versioned Windows exe
  • Manual Windows build workflow triggered on release/** branches
  • Branch protection on main and develop
  • CHANGELOG.md and PR template
  • docs/release-strategy.md documenting branching and release process
  • develop branch for integration, feature/breeze-integration for multi-broker work

0.1.0 — 2026-06-14

Initial release tagged as v0.1 on GitHub.

Added

  • Flask web dashboard for Kite Connect token management
  • Strategy manager with metadata-driven CRUD
  • APScheduler integration with SQLite jobstore
  • Paper trading and live simulation modes
  • Full-day simulation using 1-minute Kite historical candles
  • Performance and reports pages with Chart.js
  • WhatsApp alerts via Bailey notifier
  • Windows exe build via PyInstaller
  • CI workflow with pytest