changelog.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to FlintTrade will be documented in this file.
Format: [Keep a Changelog](https://keepachangelog.com/).
Versioning: [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Changed
- **Mode, broker, and decision-model docs.** The Mode chip is taken from the JWT session claim (`explore`, `practice`, or `live`) and shows **Example**, **Practice**, **Live**, or **Unavailable**. An unconfirmed claim shows Unavailable, never Example. Connected (read) is a broker status, not a Mode and not a menu item. Choosing Practice as Guest opens Setup and does not call `POST /v1/auth/mode`. The Status row is labelled Laya. Its value can be Downloading. `GET /api/v1/operator/status` reports that check as the decision model, under `laya`. The market chip reads Transition only from 7 December 2026. The Practice sandbox is no longer described as paper trading. A broker session still cannot be started from the terminal. Bug reports and the security policy list Practice and Live as Modes. The product modes page is published at `/docs/product-modes`, and `/product-modes` opens that same page.
- **Entry admission and the runner session are separate answers.** `accepts_new_entries` is true only when a new risk-increasing order would be admitted, and it matches order admission and market timings. `session_active` stays true through the runner's exit window, including MCX until 23:55 and NCDEX at the 17:00 gate. The practice strategy tick and the autonomous trader keep running through the exit window. A new risk-increasing order needs both the runner session and admission. An exit or square-off needs only the runner session, so cash auction entry does not open a new order after 15:30. Square-off times, NCDEX standard hours, and the timings payload are unchanged. The old scheduler open check remains as a deprecated alias of the runner session.
- **One session answer.** Order admission, the scheduler, the strategy runner, and market timings read one session decision. Timings publish that live decision, including weekends, exchange holidays, and special sessions. A closed day does not advertise the ordinary close. NCDEX timings use the 17:00 gate close. MCX timings use the 23:30 gate close on an ordinary day, and the Muhurat window on that session. The MCX runner still treats 23:55 as the session end. Cash phase helpers take the session date. The Order Pad session badge follows the dated cash phase, or the selected exchange.
- **NSE session clock from 7 December 2026.** Cash continuous trading runs until 15:30. The closing auction is 15:30–15:40. Order matching is 15:40–15:45, then a transition until 15:50, and the post-close session stays 15:50–16:00. Cash auction order entry stops from 15:38, when that entry can already have closed at random. Equity derivatives are not in the auction. NSE and BSE equity derivatives run until 15:45. An index contract stops at 15:30 on the expiry written on its symbol. A supplied expiry calendar confirms that date. A new order stays closed when the expiry cannot be read, or when a supplied calendar does not list it. Stock derivatives stay open until 15:45, including on expiry day. Order admission, including a live order and a modify, reads the expiry on the contract symbol and confirms it with the expiry calendar when one is supplied. A new order is refused after that expiry date, and on a Saturday, Sunday, or exchange holiday. A reduce-only exit is not blocked by a missing calendar on a day that symbol is not expiring, and it stays closed when the exchange itself is closed. NSE and BSE index names, including BANKEX, use the index close. The safety check uses the Muhurat session hours and stays closed outside them. The scheduler and the timings API use this same close. Dates before 7 December 2026 keep the current clock, and the Muhurat session still overrides it. Follows SEBI circular HO/47/11/11(3)2025-MRD-POD2/I/23843/2026 (15 October 2026), sections 4.2.4 and 4.2.5, and NSE circulars NSE/CMTR/77355 and NSE/FAOP/77356 (6 November 2026).
- **Electron 45.2.3 desktop shell.** The shell bundles Node 24.22.2. The supported Node floor is `>=22.23.4`; the development target remains Node 24.
- **Unified application shell.** The app bar lists Terminal, Testing,
Portfolio, Automation, Reports, Learn, Brokers, Settings, and System.
Older addresses open the matching app and keep the query string, hash,
and navigation state: `/home` and `/trade` open Terminal, `/invest`
opens Portfolio, `/lab` opens Testing, `/automate` and `/ai` open
Automation, `/ditto` opens Brokers, `/admin` opens System, and `/login`
opens `/welcome`. The public site permanently redirects `/explore`
(HTTP 308) to the Demo at `/demo-app/welcome`.
The public demo bundle replaces `/explore` with Terminal. An installed
build replaces `/explore` with `/welcome`; a signed-in Welcome then
continues through `/trade` to Terminal. A signed-out installed visit
stays on `/welcome`.
- **Execution sessions, protection grants, diagnostics, incidents,
datasets, and knowledge.** Loopback session routes now cover
`/api/v1/execution-sessions` (a Testing run keeps the Practice account
for that run), `/api/v1/protection/grants`,
`GET /api/v1/system/diagnostics`, `/api/v1/system/incidents`,
`GET /api/v1/data/datasets`, and `/v1/knowledge` with `/v1/learning`.
- **Mode header check.** When `X-FlintTrade-Mode` is present and differs
from the JWT `mode` claim, order routes return HTTP 403
`{"status":"error","message":"X-FlintTrade-Mode does not match the authenticated mode"}`
with no `code` field.
- **Quantity admission.** Before Laya, a lot multiple or a freeze breach
is HTTP 400 `quantity_admission`. The Order Pad shows the lot sentence
once, inline: `Quantity must be a multiple of the lot size (N).` An
unverified lot is `Not placed. The lot size for <name> isn't verified.`
An order over the per-order freeze limit is refused with HTTP 400 and
code `quantity_admission`. The message gives the limit, names the
instrument, and asks for smaller orders. The order is not split. The
figures are the contract's lot and the broker or exchange freeze limit,
not published constants. The Practice sandbox returns an unverified lot as HTTP 400
with that message and no `code` field.
- **Exit-all fails closed.** Unavailable position evidence is HTTP 503
`Position evidence unavailable; no square-off was sent.` Ambiguous
evidence is HTTP 409 `Position evidence is ambiguous; no square-off was sent.`
When broker orders cannot be read, a further exit is HTTP 409
`exit_orders_unreadable`: `Not placed. Broker orders for SYMBOL are unavailable. Reconcile them before another exit.`
### Added
- **Operator status.** `GET /api/v1/operator/status` is an authenticated read of the session mode, whether Live is unlocked, whether a broker is Connected (read), the decision model, the kill switch, the session, broker account status, dispatch counts, strategy rows, and whether startup is paused. Connected (read) is a broker status, not a mode. Each part is taken from the check that already owns it. A part that cannot be read is unknown, with a fixed reason, and the rest of the document is still returned. The route does not place an order, latch the kill switch, start or stop a strategy, or refresh a broker token.
- **Mode honesty bar.** One line under the TopBar for Example, and for
Practice and Live. Widgets also identify illustrative and fallback figures
with Sample/Example source labels. An incident, when one is showing, sits
above the mode line and does not replace it.
- **Service connections and in-process `BrokerReadPort`.** The backend now
ships a rights-aware service-provider catalogue, static LLM/data profiles,
and an inert persisted service-connection control plane. Listing or saving a
connection does not resolve, probe, authenticate to, or start that provider.
Exact broker reads in this change are the in-process `BrokerReadPort`
contract (quotes, depth, history, balances, books, and related methods) —
not a restored terminal Brokers screen or a new public HTTP read family.
- **Native broker account transaction foundation.** Strict mutation contracts,
encrypted durable claims, witnessed workspace recovery and retained worker
custody underpin the synthetic account coordinator. Read generations verify
durable enrolment and workspace coherence. Terminal receipts retain redacted
audit events with stable identities and verified retryable delivery. Native
account HTTP mutations remain `503` and native reads remain `409` until their
separate production cutovers. Refs #328.
- **Estimated charges on Practice fills.** Every Practice fill runs through the shared Indian statutory calculator and stores the breakdown and the day's charges total. Fills show `Charges ₹X (estimated)`. Positions and Performance show net P&L, with gross and charges in the tooltip. Home and Invest subtract those charges from net worth. The Practice source line is `Practice account, after estimated charges`.
### Fixed
- **Practice stop wakes waiting observers.** Stopping a Practice run records the stop, releases its lock, and only then signals observers. An observer that already holds the publication signal and needs that lock is not left waiting for the quote-read allowance.
- **Mode chip follows the session.** After a session is restored, the mode chip shows the mode in that session before the desk paints. A saved mode in the browser is only a cache and is written after the session mode is assigned. It does not override the session. Unlocking with a PIN keeps the restored mode and does not change the server mode. A session whose mode is missing or not one of Example, Practice, or Live shows Unavailable, and no order is sent. Connected (read) stays a broker status.
- **Unavailable is not Example.** When the session mode cannot be confirmed, the Scalper and the line above the widgets say Unavailable and ask the operator to sign in again or reload. That copy does not say Example or Sample. Example appears only for an example or demo session.
- **Connected (read) is a broker status.** The mode chip shows Practice, Live, Example, or Unavailable. The mode menu lists Practice and Live. A read-only broker connection is reported on the broker status, including the Status menu.
- **Workspace log teardown.** Building the backend again in the same process closes the previous workspace log only while that file is still the one the handler opened. A descriptor that is already gone does not abort startup. A descriptor number the kernel has given to another file stays open.
- **Uncertain order outcomes.** A submit whose response was dropped, timed out, or came back without a definitive broker or engine result says "Order status unknown. Check Orders before trying again." The order pad, positions, and other submit surfaces do not show that as a rejection and do not offer a retry. A cancel that was only acknowledged says "Cancellation requested". "Order cancelled" appears only after the order status is confirmed cancelled. Practice and Live use the same sentences. The order API, the practice agent, and the MCP order-intent result add `order_outcome` and `reason` beside the existing status. A retry still cannot create a second order while the dispatch ledger holds an unknown write.
- **Uncertain Forever orders.** A dropped or timed-out GTT place or modify says "Order status unknown. Check Forever Orders before trying again." It does not offer a retry and it is not sent again on its own. A lost GTT cancel says "Cancellation requested" and is remembered, so a later trigger read can settle it. "Order cancelled" appears only after that read says the trigger is cancelled. A later triggered or filled row does not keep the pending-cancel line and does not say the order was cancelled. The forever-order cancel and modify results carry the same `order_outcome` and `reason` values as the other order surfaces.
- **Forever cancel after reload.** A dropped, lost, or timed-out GTT cancel stays pending for the signed-in tab session, and only for the account and mode that requested it. Reloading the terminal still shows "Cancellation requested". Signing out removes that pending cancel, so signing in again on the same tab does not bring it back. A complete Forever list that no longer contains the trigger also removes it. A failed, partial, or stale list does not. "Order cancelled" appears only after a later trigger read says the trigger is cancelled. A later triggered or filled row clears the pending line and does not say the order was cancelled. The dispatch ledger's duplicate-order protection is unchanged.
- **Strategy Monitor unknown process.** When an uploaded strategy's process tree cannot be read, the monitor shows Unknown and keeps the runner's ownership flag on the row itself. A missing or unrecognised state is not shown as Stopped. Confirmed running and stopped reports stay on those labels. A strategy list that fails to load says so, and an empty list still says no strategies are configured. Start, pause, and stop controls stay unavailable on this preview.
- **Strategy status reasons.** Uploaded strategy status adds a stable reason: idle, running, leader_exited, tree_unreadable, exit_nonzero, exit_clean, exit_missing, or cleanup_unproven. When the runner holds a strategy, the status also includes that lifecycle: ACTIVE, PAUSED, STOPPED, or ERROR. The Strategy Monitor shows Paused for a paused lifecycle. Unknown stays Unknown. A runner whose process tree cannot be read, or that still holds ownership, is not shown as stopped and is not treated as free.
- **Illegal strategy lifecycle moves.** Starting, pausing, resuming, or stopping a strategy from a state that does not allow that move raises an error. A second start of a runner that is already running still returns without raising.
- **NSE F&O Muhurat session.** Sunday 8 Nov 2026 opens for F&O 14:20–15:40 IST. Equity and indexes use the normal-day close: non-CAS cash to 15:30, CAS names 14:20–15:15, closing auction 15:20–15:35. Currency and commodity run 14:20–15:30. Equity after-market orders are collected until 14:03 IST. A missing special-session record stays closed.
- **Broker sessions are not available in the terminal.** Broker-account
mutations — `/v1` account and auth writes, native connect / login /
set-primary / delete, OAuth start and callback, and the other guarded
account-authority routes — return a stable `503` with
`{error: broker_account_cutover_unavailable}`. Native HTTP account and
market-data reads return `409` with zero provider calls. The terminal still
calls those routes, so Setup → Brokers and Settings → Brokers show that
response rather than a working native session. Practice mode and example
data work without a broker. Live order placement stays disabled unless it
is explicitly unlocked and every safety layer passes. Service connections
remain inert only.
- **Desk routes, one Practice place path, and desk polling.** `/positions`,
`/holdings`, `/monitoring`, `/schedules`, and `/glossary` open the screen
that owns that book. `/login` opens sign-in. Practice places go through
the admitted order route only; Settings does not place. A missing price
says so plainly, or fills at the last stored close. The Trade book starts
open. Desk reads pause while the tab is hidden and back off after HTTP 429.
- **Laya chip, runtime key, and snapshot launch.** The desk polls
`GET /api/v1/ping` every 1.5 seconds. That ping reconciles the pid
file, the key file, and the runtime record the same way an order
does, so the chip and the order gate read the same state. A
command-line stop or start shows on the chip by the next 1.5-second check.
After Start Laya, until that ping confirms the new state, the chip
says Checking in the neutral colour and the popover says Checking
Laya…. It does not show a stale Ready during that wait. An admitted
place while the chip is not Ready or Degraded also shows Checking
until the next ping. A confirmed
first load still says Still loading. A place refused with exactly
"Laya is Down. New orders are paused until it's Ready. You can still close positions." sets the chip to
Down on that response. The refusal line is unchanged. When
`LAYA_API_KEY_FILE` names `<workspace>/runtime/laya/api.key`, `start`
replaces that file and does not treat it as missing. A different path
that is not there still refuses with "The Laya API key file is
missing." A model already in the standard Hugging Face cache is
accepted. When that file is the cache symlink
`snapshots/<revision>/model.safetensors`, the launch path is the
snapshot file, not the blob. A blob path is still refused. An admitted
Practice place with an empty note skips the model and writes one
decision-log line, `effect=clamp` with `failure=note_absent` and no
proof. When this run's record stood in, each model allow keeps its own
`effect=allow` `proof=runtime` line.
### Added
- **Opt-in Laya decision sidecar for place admission.**
After the hard rules, the sidecar may deny or clamp a place only. It
cannot raise a quantity or overturn a rule refusal, and it does not
place the order. Laya starts Down. A ping does not invent Ready.
Live stays fail-closed until a qualification record uses
LIVE_DECISION evidence for the exact model revision, weight digest,
and policy version. When Laya is actually Down, every mode is refused
with "Laya is Down. New orders are paused until it's Ready. You can still close positions." A Practice
refusal never says Live. A Live place without that qualification,
while Ready or Degraded, says "Laya isn't qualified for Live yet.
Practice orders are available." Opt in with
`python -m flinttrade_core.laya_runtime install` or `start`.
The CPU install pins `torch==2.14.0+cpu`, `laya==0.3.21`,
`huggingface_hub==1.33.0`, and `tqdm==4.70.1`
in the Laya install constraints file (no extras; the install requirement
is still `laya[serve]==0.3.21`). The download progress class subclasses
that pinned `tqdm.auto.tqdm`. Torch is installed first from the CPU
index, and the managed Laya process can share the base interpreter with FlintTrade.
`LAYA_PORT` defaults to 8000. The host stays `127.0.0.1`.
It is not Ready by default. A first start with the checkpoint missing
or incomplete downloads the commit named by `[checkpoint] revision`
(not the model repository's default branch) into `runtime/laya/staging`.
That download sets `HF_HOME` to `<workspace>/runtime/laya/hf-home` and
`HF_HUB_DISABLE_XET=1`, so transfer logs stay out of the shared cache.
FlintTrade hashes the files there against the `[checkpoint]` and
`[checkpoint.manifest]` pins, and on a full match moves that directory
onto `runtime/laya/checkpoint` before the offline launch. When a new
pin replaces a checkpoint already on disk, the next start downloads it
the same way and the current copy stays in place until the new files
match. On a full match that checkpoint is renamed aside to
`checkpoint.old-<random>` in the same runtime directory, staging is
renamed onto `checkpoint`, and the old copy is deleted. If that second
rename fails, the old checkpoint is renamed back, the chip is
`download_failed`, and the managed Laya process does not start. While the download
runs, including a pin change, the status word is Downloading (neutral
colour), not Still loading or Down, and the chip reads "Downloading the model · 1.2 of 3.4 GB".
The model is about 2.37 GB, and that size is reported once.
There is no separate Updating label. Orders are refused with
"Laya is Down. New orders are paused until it's Ready. You can still close positions." A dropped
connection, a partial download, or a failed swap is `download_failed`
("Can't download the model"; tooltip "Check your connection, then
Start Laya again.") and deletes only the staging directory. That chip
stays `download_failed` even when an older snapshot is still on disk,
whether that snapshot would be `wrong_revision` or `unverified`.
`unverified` stays when this start did not download. `wrong_revision`
is only a complete download whose files
do not match the pin, a snapshot already on disk that this start is not
replacing, or a running managed Laya process that reports another revision or digest.
Leftover staging directories and `checkpoint.old-*` copies are removed
at the start of `start` once a checkpoint is in place, with no chip change and
no message. If `checkpoint` is missing after a crash between the two
renames, the last `checkpoint.old-*` name is restored and checked
against the pin.
The managed Laya process does not start on files that do not match the pin, including
that restored copy. The managed Laya process always runs offline. When its health
document leaves the revision empty, FlintTrade fills the pinned
revision from the verified manifest, so the chip leaves Still loading.
- **Shared symbol bus from Watchlist.**
Selecting a symbol in `/trade` Watchlist retargets
Chart, Option Chain, and Scalper to that symbol —
no retype. Keyboard retarget is optional later.
Example retarget is allowed. Disclosure stays the
Mode honesty line, not a widget Sample chip.
An empty watchlist never silently retargets.
- **Option Chain OI profile + PCR strip.**
The Option Chain strip shows OI profile + PCR for
the selected expiry/symbol. Example does not invent
live OI. The Mode honesty line is the disclosure —
there is no Sample chip on the chain strip. An empty
expiry is an honest empty, not zeros-as-data.
### Changed
- **Home and Invest net worth, greeting, benchmark legend, and Example markers.**
Home and Invest share one total: ledger cash, including blocked
margin, plus holdings at market value, plus open positions. Opening
an F&O position does not reduce the total by its margin. A Practice
round trip at an unchanged price leaves it at the starting cash, for
example ₹10,00,000. Options add signed market value. Futures add
unrealised P&L. Dhan marks from the mark-to-market average, or from
`costPrice` when that average is absent. Kotak Neo marks an open
future from the open-leg average. Practice marks a future from the
entry price. An estimated futures mark shows `≈`. Dhan does this for
`costPrice`. Kotak Neo does this for an open future. Practice never
does. The mark clears when that position is flat or the average
arrives. The tooltip and `≈` sit on the Home Net Worth amount, the
Known Total amount, and the Open Positions value. The Invest
Dashboard label `Net Worth (Cash + Holdings + Positions)` carries
the tooltip, and `≈` sits on the amount under it. Available Funds
shows that `≈` with no tooltip. Dhan's tooltip, when the average was
missing, is
`Approximate. Your broker didn't send an average price for NIFTY-JUN2026-FUT, so profit or loss from earlier days may be counted twice.`
Neo's is
`Approximate. The price for NIFTY25JUNFUT is estimated from the open position's average, so profit or loss from earlier days may be counted twice.`
Several positions of one kind say `N futures positions` instead of
the symbol. The screen-reader name is `Net Worth, approximately …`.
Allocation percentages are not marked. Home allocation stays on the
Example split until funds, holdings, and positions have all loaded.
Home and Invest both wait for the position book before they publish
the total. The sample book does not. While that book is pending or
has failed, Home shows `—` and does not draw cash alone. A negative
total is the number, for example `-₹50,000`, or `≈ -₹50,000` when
the mark is approximate. Home derives each open position's P&L
percent from cost, and shows `—` when cost is missing or not above
zero.
The greeting uses the saved display name, then the username, and
stays plain `Good morning` (or afternoon or evening) until a name
is known. It never uses `Trader`. On Benchmark, real holdings use
`Your holdings (unrealised)` instead of
`Your Portfolio (since first buy)`. An empty book stays
`Your Portfolio`. Example holdings stay `Your Portfolio` with an
Example label, and the hard-coded index returns keep the Example
chip. Dashboard Net Worth, Available Funds, Invested Value, and Day P&L
show the final formatted value on the first frame in every mode, with
no count-up from zero, including `≈`, `-₹50,000`, and `—`.
The sample Dashboard XIRR is the inline `XIRR` figure plus
one Example chip. Portfolio Allocation on that sample dashboard omits
its broker sentence and does not carry its own Example chip. There
is no Portfolio XIRR card. On sample figures, Net Worth reads
`Example equity and cash. Connect a broker to see yours.`; the
allocation label is `Allocation` with the Example chip. Equity
Holdings and Cash leave their notes blank, and those rows do not
carry their own Example chip. In Example, Baskets show one Example
chip while quotes are loading and after they have loaded. ETFs show
one Example chip and `Example prices. Connect a broker for live quotes.`;
Practice and Live keep the live quote wording. Sector's header reads
`Example sector split. Connect a broker to see yours.` and the footer
reads `Example data. Not from your holdings.` Social shows exactly one
Example chip. The sample XIRR chip and the Net Worth allocation chip
paint only on example data. The Benchmark chip stays in Practice and
Live. A connected
book keeps the live equity sentence,
`Allocation (live assets only)`, and `Live from broker`. Mutual
Funds on example data reads
`Example NAVs · as of 10-Sep-2026`. Example order review confirms
with **Continue**. Practice review keeps **Confirm simulation**.
- **Desk Laya chip follows the current mode.**
Practice shows sidecar Ready, Degraded, Down, Still loading, or
Checking. Live shows Live-facing status. "Not qualified for Live" is the tooltip
and the popover line when Live lacks qualification. Ping
publishes Live-facing `laya`, sidecar `laya_practice`, and
`laya_live_qualified`, plus a reason code and port. During the first
load the chip says Still loading and does not read Down. Orders stay
refused with the Down sentence. A port clash says which port is in use.
The Live Blocked strip follows Live-facing
Down only. It mutes Live place and Position Mirror start. Practice
is not muted by that strip. Clicking the chip opens a popover with the
plain-words reason, the link "How to start Laya"
(`docs/USER_GUIDE.md#start-laya`), and an operator-only "Start Laya"
button. The chip tooltip carries
`python -m flinttrade_core.laya_runtime start`. Reason codes are
`not_started`, `stopped`, `port_in_use`, `still_loading`,
`downloading` (Downloading the model · 1.2 of 3.4 GB),
`download_failed` (Can't download the model),
`unreachable` (Unreachable), `wrong_revision` (Wrong model version),
`unverified` (Can't verify the model), `key_rejected` (Can't reach
Laya), and `key_missing` (The Laya API key file is missing.). A health
check does not replace `key_missing` with Not started. When
`LAYA_API_KEY_FILE` names `<workspace>/runtime/laya/api.key`, `start`
replaces that file and does not treat it as missing. A different path
that is not there still refuses with that same sentence.
`downloading` has no Next line. The `download_failed` tooltip is
"Check your connection, then Start Laya again." A signed-in operator can start the managed Laya process from the popover
(`POST /api/v1/laya/start`); the chip then says Checking in the
neutral colour, and the popover says Checking Laya…, until the ping
confirms Ready, Degraded, or a reason other than `not_started`. A
confirmed first load still says Still loading. A dead managed Laya process is reaped
and reported Stopped. A clamp says "Not placed. Laya allows
up to N." with Place N and Cancel, and never auto-places. Place N
sends that quantity, and the Practice review panel shows it. A Down
refusal is "Laya is Down. New orders are paused until it's Ready. You can still close positions." and
does not show a quantity ceiling. A Live place that
is not qualified says "Laya isn't qualified for Live yet. Practice
orders are available." The Order Pad note is the collapsed line "Add a
reason (optional)" under Quantity. Once open, the field's accessible
name is the same. Under a denial, the server reason is named
"Laya decision" inside one alert (`role="alert"`). That alert is the
only live region. The reason line is not its own status. A place with
no note still gets
Laya's policy decision: Practice clamps and Live denies. It is not a
hard reject. Desk place
surfaces go through this admission.
- **GTT is refused on every submit route.** `"variety": "gtt"`, in any
case or separator spelling, is HTTP 422 `gtt_unsupported` on place,
routed place, exit-all, and a bracket, before Laya, SafetySystem, and
any broker call. The message is `Not placed. GTT orders aren't supported right now.`
No submit route reaches a broker forever or super-order endpoint. The
Kotak Neo adapter refuses a `gtt` place. Order Pad keeps GTT visible and
disabled, with the tooltip `GTT orders aren't supported right now.`
`POST /api/v1/orders/forever` does not place: a valid body is HTTP 501
`Orders are placed through /api/v1/orders/place.` A Live bracket with
exactly one stop-loss or one target submits on
`POST /api/v1/orders/bracket` after admission. Practice on that route is
HTTP 403 `practice_unsupported`. A broker-held variety, a stop-loss and
a target together, and a trailing stop are refused before admission. A
second exit on the same broker account, while this desk's exit on that
contract is unfilled, is HTTP 409 `exit_pending`: `Not placed. An exit for <symbol> is already pending. Wait for it to fill, or cancel it and try again.`
The row keeps **Exit pending**. The Live hold is
for that broker account. When the broker
order book cannot be read, that refusal is HTTP 409
`exit_orders_unreadable`: `Not placed. Broker orders for <symbol> are unavailable. Reconcile them before another exit.`
A signed-out reset with no
authenticator enrolled reads `Sign in to reset this account. You'll
need your password.` With an authenticator enrolled it reads `Sign in
to reset this account. You'll need your password and authenticator
code.` Recovery asks for an authenticator code only once one is
enrolled.
- **Session auth and probes.** A session JWT on
`Authorization: Bearer` or `X-FlintTrade-Token` passes the global check.
An API key on `X-FlintTrade-Token` does not. A reduce-only close can be admitted while
new orders are paused. PIN unlock replaces the session token. Reset of
a finished account needs a session and the password. A wipe ends other sessions. Practice restore marks fills and leaves them out
of the Laya, strategy, benchmark, and training readers. `GET /healthz`
and `GET /readyz` are public and return status only.
- **First-run Setup resume, Start over, and a fixed vault step count.**
Reloading `/setup` mid-flow resumes the
unfinished setup session. **Start over (deletes this unfinished
operator)** deletes that unfinished operator and restarts at step 1.
A workspace data wipe is not required for either path. When the vault
is already secured on this machine, the vault step is skipped and the
count is fixed from the start: **Step 1 of 2 - Create operator**, then
**Step 2 of 2 - Practice desk**. That path never shows "of 3". On that
Practice step only, **Your vault is set up and secured on this
machine.** appears above **Open Practice desk**. When the vault is not
yet secured, Setup still shows **Step 1 of 3 - Create operator**,
**Step 2 of 3 - Vault**, and **Step 3 of 3 - Practice desk**. After
Setup completes, `/setup` does not restart step 1. A signed-in
operator is sent to `/trade`. A signed-out operator sees **Setup is
complete. Sign in to open the desk.** with **Sign in** as the primary
button. Refs #297.
- **AI Chat Practice + native live-read context.**
When an LLM is configured, AI Chat may use Practice
SandboxEngine fills and native live-read feeds for analysis.
Suggest stays labelled illustrative.
Chat never shows green **Connected** without a real LLM.
AI does not place Live orders — Live place stays fail-closed.
Measure later: profitable alphas are not a release criterion.
This does not lift the native broker HTTP freeze. Refs #254.
- **Desk chrome: one TopBar + one ticker + flex shell.**
Dual TopBar index slots are replaced by one dedicated
scrolling TickerStrip under TopBar. TopBar keeps Mode,
status, and overflow — not a second quote rail. Three
Settings/Tools entries collapse into one Tools overflow
menu plus at most one primary Settings entry. App chrome
is a flex column: TopBar, then the operator status strip when
one is showing, then the Mode honesty line, then TickerStrip,
then the route body.
Trade ships first; the same shell then rolls to Invest,
Automate, Learn, and Ditto. Not a silent widen of
Compact-only-on-Trade, and not a big-bang
rewrite.
- **Mode vocabulary and Trade desk density.**
Example is sample data. Practice and Live are the Modes.
Example Order Pad uses Example Buy / Example Sell; Practice
keeps Practice Buy / Sell; Live uses Place BUY/SELL Order.
TopBar session chips (Continuous · CAS · Matching ·
Post-close · Closed) stay session status.
Comfortable is the new-install default. Compact Trade at
~1280 and wider keeps chart, order pad, and positions
primary, with ticker, tool ribbon, and watchlist /
indices collapsed behind one desk-tools toggle. Selecting Compact on Trade at ~1280 and wider always
starts with that disclosure collapsed.
At most one primary banner (Example sample > Practice sample >
Live risk > feed disconnected). External-action gates stay fail-closed
for Example. Phone product and the marketing site are unchanged.
### Fixed
- **Ticker venue badges match the marquee.**
Pinned badges follow the venues that feed the tape: NSE, BSE, and
MCX on the default tape, and NFO when an F&O symbol feeds. A venue
with no feeding symbol is omitted. Symbols that do not resolve to a
venue show **Unavailable**. An empty tape omits the badge strip.
The marquee runs continuously when motion is allowed. With
`prefers-reduced-motion: reduce`, the tape freezes and shows
**Reduced motion**. The Sample freshness chip may stay; it must
not hide venue honesty.
- **Tools Quick Settings on the Trade desk.**
Tools → Quick Settings opens density, theme, and similar controls
without leaving the desk. Tools → Settings still opens the full
Settings route for deep pages (Monitoring, brokers, auth). Compact
Trade keeps Quick Settings on the TopBar when the tool ribbon is
collapsed. Dropping Quick Settings so only full Settings remains
fails desk-first. Refs #279.
- **Docs: correct GTT proxy, Practice walkthrough, and Live safety path.**
`USER_GUIDE` Practice walkthrough no longer treats Example
Buy as a sandbox Positions/Orders fill.
`API.md` no longer claims `/orders/gtt-*` is gated like
regular Live place — those verbs return HTTP 501 after unlock
and do not place. `POST /api/v1/orders/forever` does not place.
`API.md` documents
`GET /api/v1/advisor/status` `source` (`env` / `stored` /
`default`). `ARCHITECTURE.md` mode-guards Practice to
`SandboxEngine` and runs L1–L5 only on Live.
`DEVELOPER_GUIDE.md` no longer says every order is checked
by L1–L5: those layers are Live-only. Practice goes to
`SandboxEngine`. Example placement is `mode_blocked`;
Example Buy is a local fill.
- **Restore Connected honesty on `/ai`.**
LLM readiness is global config truth (stored Settings
`#llm` plus explicit `advisor/status`), not Mode-derived.
Example and Practice share one source. A blank stored
provider plus the empty→ollama default is **Not
configured** / **Not installed** — never green
**Connected**. An explicit `LLM_PROVIDER` env-only
setup stays **Connected** when `advisor/status` reports
it usable. Composer stays gated until a provider is
actually configured.
- **Connected badge matches LLM install state.**
`/ai` Chat (AI Hub) follows the real LLM status from
Settings → AI / `#llm`, including Managed Ollama
install state — not a green **Connected** while
Settings shows **Not installed**. Managed Ollama
**Not installed** shows **Not installed** (warning
badge, not green) with a primary **Open Settings → AI**
CTA to `/settings#llm`. Unconfigured stays
**Not configured**. Composer input and Send stay
disabled until the runtime is installed and
configured — the same bar as Settings. A provider
string of ollama is not Connected while the managed
runtime is absent. A configured-but-broken probe still
shows **Error** / **Disconnected** with Retry.
Signals **Live** / **Polling** stay separate from
Chat LLM readiness.
- **Authed `/home` skips password Welcome Back.**
`/home` is the canonical Home / Welcome dashboard.
A signed-in operator who opens it (address bar,
refresh, or same-tab bookmark) sees the same Home
as SPA nav (sidebar, Alt+H, TopBar) — not the
password Welcome Back gate. That gate stays on
`/welcome` for unauthenticated visitors only.
- **Leverage tab content or honest empty.**
`/settings#leverage` shows real leverage content when
the broker snapshot is available. When leverage cannot
be shown — unsupported broker, missing snapshot, or
load failure — the pane shows the honest empty
`Leverage settings unavailable.` plus **Retry**.
Selecting the Leverage tab never leaves a highlighted
tab over a blank content pane.
- **Kill All fail-closed when risk runtime unavailable.**
Whenever the Ditto risk runtime is unavailable —
including Example `/ditto` Risk — **Kill All
Positions** stays muted and disabled. Helper:
`Risk runtime unavailable — Kill All disabled.`
The control is never the armed red emergency CTA
in that state. The backend rejects a Kill All if
the UI slips. Live and Practice with a live
runtime and managed accounts still keep the
armed control (empty-account disarm is
).
- **Log refs no longer create a missing workspace.**
A cold `log_ref` salt cache no longer calls
`workspace_dir()` in a way that mkdir's the default
workspace. Isolated callers (explicit-path TOTP
stores) stay off the default pair.
- **Example Schedules Pause gated for sample jobs.**
Example `/automate` → Schedules seeded jobs: the
Mode honesty line owns disclosure — no extra
Sample chip once Pause is gated. Seeded
Example jobs show status Sample/Demo (or muted),
not a production-looking Active badge. Pause on
those jobs is disabled, with title helper `Example
schedule — control unavailable`.
Practice/Live jobs keep Pause/Resume. The backend
rejects Example pause/resume with `mode_blocked`.
- **Example Stock Baskets disable demo Edit/Delete.**
Example `/invest#basket` seeded cards (NIFTY IT,
Banking, and any other sample set): the Mode
honesty line owns disclosure — no card-level
Sample chip. Bare ₹ / P&L under that banner is
acceptable once actions cannot look live.
Edit and Delete on seeded Example baskets are
disabled, with title helper `Example basket —
editing unavailable`. User-created
Practice/Live baskets keep full Edit/Delete.
Empty Example is an honest empty or a clearly
labelled sample set.
- **Feed-freshness honesty.**
Example disclosure is the Mode honesty line.
Widgets also label Sample/Example fallback figures. Per-symbol
ticker Sample chips are optional. The Market
Clock freshness chip appears only when that
widget is mounted. Practice / Live still need
TopBar or the ticker: Live · Delayed · Sample
(and muted Stale / Unknown plus age when
known). Silent-stale is a fail. Feed provenance
stays separate from Example and from the Practice and Live
Modes (Mode honesty).
- **Resource Hub User Guide first-open honesty.**
Example `/learn` → Resource Hub → User Guide shows
`Loading document…` while the local backend/doc
settles — never a red backend error on a cold race.
A timeout or race is a muted soft fail:
`Document isn’t ready yet.` plus a primary **Retry**
(one automatic retry is allowed). Hard fail copy
`Couldn’t load this document from the local backend.`
plus **Retry** appears only after retry is exhausted.
Order Safety Notes already loading in the same
session does not mark User Guide permanently broken.
- **Holdings badge matches the visible table.**
Practice/Example `/invest` → Holdings with no broker
shows `N holdings` for the rows currently in the table
— never `0 holdings` over a populated sample table.
Dashboard and "N stocks" use the same N. Practice waits
until the holdings query has settled empty before the
sample fallback, so a cold load does not flash the
wrong N. Dashboard `Net Worth (Cash + Holdings + Positions)` uses the
same shared demo book as Holdings. There is no Sample
chip on the Holdings table or header. When the sample
book is shown — Example always, and Practice after that
empty settle — Holdings and Dashboard keep `DemoBanner`
(`Showing sample data — connect a broker for live data`)
in Example as well as Practice. Example also has the
Mode honesty line (`Example data. No broker is connected and no orders are sent.`). The Practice Mode
line (`Practice — simulated fills, no real money.`) does not call that book sample;
`DemoBanner` is the required Practice disclosure for it.
A broker read failure shows muted `Failed to load holdings`
plus `Refresh` —
never `0 holdings`, `No holdings`, or a sample table
under a failed load. A connected broker with no
positions shows `0 holdings` and an honest empty state
(no sample table under a zero badge). Connected
positions use the live count only.
- **Example Execution Logs mode honesty.**
Example `/automate` → Execution Logs shows the muted
empty state `No execution logs for Example. Switch to Practice or Live to see real run history.`
and never the outage line on a healthy sample session.
Practice/Live with a 200 OK and 0 rows use
`No execution logs for this date.` — not an outage.
The red `Failed to load logs. Backend may be offline.`
copy (with Retry) is reserved for a real load failure.
Loading shows a spinner / `Loading logs…` and never
flashes the outage line.
- **SEBI CAS session clock / TopBar phases.**
TopBar market-status chips are Continuous · CAS ·
Matching · Post-close · Closed (one active).
Tooltip/title is the window, e.g.
`CAS · 15:15–15:35 (as of Aug 2026)`. When
equity F&O still runs after cash continuous ends, a
secondary `F&O open · till 15:40` chip appears.
Market Clock follows Continuous (~09:15–15:15)
→ CAS 15:15–15:35 → Matching → Post-close
15:50–16:00. Non-CAS cash still continuous to
15:30. Cash is never green "open" after 15:15;
CAS is not Closed. Flat "Market open until
15:30" and "VWAP last 30 min" closing-price
copy are gone. The September 2026 consultation
stays out of the UI.
- **Position Mirror Start fail-closed gate.**
Example `/ditto` → Position Mirror Start stays
muted and disabled when unavailable — never
the primary green armed CTA. Example is
always disarmed (sample-only). Helper:
`Mirroring is blocked for Example. Switch to Practice or Live with broker accounts connected.` Practice stays disarmed
— the backend is Live-only. Helper:
`Mirroring requires Live with broker accounts
connected.` Live enables Start only with a
source selected, at least one target, and
broker accounts ready. Missing source/targets:
`Select a source account and at least one
target to start mirroring.` A successfully
empty account list: `Connect a source and at
least one target account to start mirroring.`
Pending and failed account fetches stay muted
with `Loading accounts...` / `Could not load
accounts.` and never look like an empty
connect state. The backend rejects Example,
Practice, or incomplete starts
(`mode_blocked` or equivalent).
- **Example Scalper fail-closed order path.**
Example `/trade` → Scalper is disarmed —
Buy CE / Sell / 1-CLICK never open Confirm
Order and never place. Buy/Sell stay disabled.
Helper: `Orders are blocked for Example. Switch to Practice or Live with a broker connected to trade.` 1-CLICK stays
OFF / disabled; title `One-click is unavailable
for Example`. Sample quote preview is allowed;
Confirm BUY / Confirm SELL chrome is not.
Practice and Live use Confirm only when the
mode allows it and a gateway is configured.
The backend rejects Example orders with
`code: mode_blocked` if the UI slips.
- **OI Chart shares Option Chain expiries.**
Example `/trade` Analysis layout → maximize OI Chart
now shares Option Chain expiries for the
symbol/exchange. A non-empty list shows the
expiry control; charts and statistics cover
only the selected expiry (the chain’s
selection when both widgets are open;
otherwise the nearest listed). Example sample
expiries stay listed. The Mode honesty line is
the disclosure — expiries are not badged Sample.
No expiries or no OI is an honest empty —
`No expiries for this symbol` or `No OI for
this expiry` — with no bars and no PCR/max-pain
stats, never “No expiries” over fake charts.
- **Watchlist LTP / % change use ticker sample quotes.**
Example `/trade` → Watchlist with Sparkline + LTP +
% change checked now paints those column headers
and cells. LTP and % change read the same sample
quote source as the ticker tape (Jotai tick atoms
from the Example demo feed) for NIFTY, BANKNIFTY,
SBIN, RELIANCE, HDFCBANK, and any other symbol on
that feed. The Mode honesty line is the disclosure.
Missing quotes show `—` (or a brief `…` while the
first fetch is in flight), never silent blank
chrome. Unchecking a column hides it; checking it
again shows the sample value or the honest empty.
- **Options Builder Net Debit and Max Loss share a position ₹ basis.**
Example `/lab` Options Builder now shows Net
Debit/Credit, Max Loss, and Max Profit on a
shared position ₹ basis. Net Debit/Credit is
the signed premium × lots × lot size. Max
Loss and Max Profit are expiry-payoff results
(intrinsic at the strikes, strike width, or
unlimited) scaled to that same position basis
— not premium × lots × lot size on every card.
For a long call, Max Loss equals the Net Debit
(the premium paid for the position). A muted
sublabel `₹X per lot · N lots · lot size L`
sits under those figures and is never the only
number. Header and Legs chips use the same
position basis as Payoff. When lots differ and
a single per-lot breakdown cannot be formed,
the primary figure is tagged `position` —
never two unlabelled ₹ on mixed bases.
Blank premiums still show `—`.
- **Backtest headline P&L vs trade-log.**
Example `/lab` backtest results now show labelled
dual metrics. **Total Return (%)** is initial
capital → final equity (including a forced last-bar
close), with subtitle `Initial capital → final
equity`. Live `total_return` percentage points are
not passed through `fmtPct` a second time.
**Net trade P&L (₹)** sums Trade Log `net_pnl`
when present; if `net_pnl` is missing the card is
labelled **Trade log P&L** with `Gross — net P&L
not in result` rather than calling a gross sum net.
When that sum matches the equity change, a quiet
`Reconciles with trade log` note appears; when
they diverge (fees, open marks, partial fills)
both numbers stay visible with
`Trade log sum ≠ equity change — fees / open
marks`. Monthly P&L stays trade-based so the
chart matches the log.
- **Learn Glossary Lot Size freshness.**
Example `/learn` → Glossary → Lot Size now teaches the
Jan 2026 NSE-cycle index lots — NIFTY 65, BANKNIFTY 30,
FINNIFTY 60, MIDCPNIFTY 120 — with an explicit “as of”
date and a Verify on NSE link to circular NSE/FAOP/70616
(3 Oct 2025). Stale `NIFTY=25` / `BANKNIFTY=15` copy is
gone. Learn market facts that exchanges revise must ship
dated, not as forever hardcodes. Independent of #213.
- **Performance follows the Review date range.**
Trade Review **Performance** uses the same date range as
Log and the other Review tabs. Changing Review dates
updates Performance. A visible **Review range** | **YTD**
control keeps YTD as an explicit choice; the active chip
always shows the effective window (for example
`YTD · 01 Jan–11 Sep 2026`). Opening Performance no longer
auto-jumps to YTD. An empty range or no fills is an honest
empty for that window, not a quiet YTD fallback. Metrics
cover the labelled window up to the journal's 1,000-fill
analytics page; a larger window is disclosed rather than
silently sliced.
- **Heatmap Group by Exchange shows labelled group bands.**
Example `/trade` Positions → Heat → Group by Exchange (and
Group by Sector) now draws a labelled band per group: a
name chip (`NSE`, `NFO`, …) plus an optional exposure
sublabel, with a stronger gutter than the leaf-tile
borders. A single group still carries its label. Positions
with no exchange metadata show `No exchange groups in these
positions` instead of an undifferentiated treemap. Flat
stays leaf-only, with no group chrome.
- **Example Mutual Fund NAVs no longer claim a daily AMFI feed.**
Example data on `/invest#mutual-funds` labels the fixture
`Example NAVs · as of 10-Sep-2026` and drops “Updated daily
after market close.” The sample date was refreshed once so
the as-of is not months stale; it does not auto-update.
Practice and Live keep the daily-update sentence when the
live AMFI feed is in use.
- **Options Builder Example Long Call no longer looks zero-risk.**
Example `/lab` Options Builder now treats a blank premium as
unknown: Payoff summary cards show `—` and
`Enter premium to model payoff` instead of modelling ₹0.
The Long Call template seeds the Example sample-chain ATM CE
LTP, labelled `Sample premium — edit to model`, so Max Loss
and breakeven follow maths on a non-zero cost.
Typing an explicit ₹0 still uses that maths and warns
`Premium is ₹0 — payoff treats cost as free`.
- **Security PIN requires exactly six digits.**
Example `/settings#security` keeps New and Confirm as digits-only
fields with `maxLength` 6. Set/Change PIN stays disabled until the
account password is present, both fields are exactly six digits,
and they match. Blurring a field with 1–5 digits shows
`PIN must be exactly 6 digits`; blurring Confirm when both are
filled and different shows `PINs do not match`. A five-digit value
no longer looks valid. `POST /v1/auth/pin/set` still rejects
anything that is not `^\d{6}$`.
- **Progressive TopBar collapse at ~390px.**
The terminal chrome no longer clips workspace, status, or ticker
behind a horizontal TopBar scroll at about 390px. Logo mark, the chip
(Example, Practice, or Live), and compact session/status stay
visible and tappable. The ticker strip hides first (default off
under ~480px). Workspace, account, Tools, search, fullscreen, and
clock move into a More overflow menu with hit targets of at least
44px. Workspace and ticker stay reachable via More; no control is
clipped and unreachable.
- **Suggest recommendations refresh when mood changes.**
Example `/ai` Suggest treats market mood as a filter, not a draft.
Changing mood (chip or **Next mood**) immediately replaces the
recommendation list and the selected mood chip from one mood
state. A previously focused strategy card is cleared, so a prior
mood's card (for example Iron Condor after leaving Sideways)
cannot remain. An empty mood + risk match shows an honest empty
state with **Try another mood**.
- **Honest unconfigured LLM state on `/ai`.**
`/ai` Chat probes `advisor/status` (including Example /
`demo-user`) and aligns the badge and composer with Settings
`#llm` hydration — not a stale local store, and not an
env-default `configured: true` while Settings looks empty.
Unconfigured shows a warning **Not configured** badge, empty
**LLM not configured**, a primary **Open Settings → AI** CTA to
`/settings#llm`, and an outline **Retry**. When a leftover
transcript hides that empty state, the header still offers
**Retry** and **Open Settings → AI**. Returning to Chat after
saving Settings → AI re-checks readiness. The composer input and
Send stay disabled, so there is no send-then-`no reply` path. A
configured but broken probe shows **Error** / **Disconnected**
with Retry — never a green Connected. Signals **Live** /
**Polling** stay separate from Chat LLM readiness. Do not add a
fake Connected sample advisor in Example; any later demo replies
must be labelled **Sample replies**.
- **Telegram Send Test stays enabled in Example.**
Example `/automate#settings` Telegram Alerts now disables Send Test
and keeps the prefilled message as a preview-only sample. Helper:
`Telegram tests are blocked for Example. Switch to Practice or Live with Telegram configured to send a real test.`
Practice and Live enable Send Test only when Telegram is configured;
otherwise the control stays disarmed with "Configure Telegram first".
The backend rejects Example test sends with `mode_blocked`.
- **P&L columns unusable at ~390px.**
Example `/trade` Positions and Invest Holdings switch to stacked
cards below 480px, so each row shows symbol, quantity, LTP, P&L
and P&L% on one screen. The wide nowrap table no longer clips
those figures off-screen behind a tiny scrollbar.
- **Invest deep-link hash tabs ignored on load.**
Opening `/invest#holdings` (and the other Invest tab hashes)
now selects the matching tab on load. A direct `#holdings`
URL no longer falls back to Dashboard.
- **Duplicate Watchlist widgets from Add widget picker.**
Example `/home` Add widget no longer adds a second Watchlist when
one is already on the dashboard. Already-present widget types are
disabled or hidden in the picker; choosing Watchlist focuses the
existing card instead of duplicating it.
- **Home greeting uses local evening at noon IST.**
Example `/home` greets from the Asia/Kolkata clock, so ~12:01 IST
is Good afternoon (or Good morning before noon), not Good evening
from a non-IST browser clock.
- **Chart stays stale when timeframe selector changes.**
Example `/trade` timeframe buttons now refresh the chart
series and visible range to match the selected interval.
Switching 5m → 1D no longer leaves candles and intraday
timestamps on the prior range, or a brief blank that stays
stale.
- **Empty Monitors has no Strategy Builder/Lab CTA.**
Example `/automate` Monitors empty state now includes an
"Open Strategy Builder" link to `/lab`. Operators no longer
have to find Strategy Lab independently from copy-only text.
- **Settings `#llm` load failure with no recovery.**
Example `/settings#llm` no longer treats a demo or unconfigured
session as a broken load. It shows an empty LLM state with Retry
and guidance that Example cannot persist LLM secrets. Live still
fail-closes on a real load error to protect a saved configuration,
and now offers Retry.
- **Kill All Positions armed on empty Example Ditto dashboard.**
Example `/ditto` Risk Dashboard with ₹0 totals and no accounts
listed now disables Kill All Positions and shows an empty
state. The control is no longer a bright red armed emergency
CTA on an empty dashboard. Live and Practice with managed
accounts still keep the armed control.
- **Practice orders in Example without a live broker.**
Example `/trade` Order Pad Practice Buy opens the Practice review and
records a sample fill — no live broker is required. Live still
uses the gated `placeOrder` path and still requires a broker
connection. Native broker freeze is excluded.
- **Daily Sign In 2FA field while authenticator is deferred.**
Login Sign In probes `/auth/status` every time it is shown and hides
2FA unless `totp_enabled` is explicitly true. A stale Welcome
`totpRequired={true}` after Sign Out can no longer keep the field.
Enrolment still requires TOTP; Live still needs enrolment plus PIN.
- **Duplicate zero placeholders on backtest metrics.**
Example `/lab` backtest headline metrics (Sharpe ratio, max
drawdown, win rate, profit factor) now show a single formatted
value. The leftover count-up `0.00` / `0.00%` beside the real
figure is gone.
- **Zero-premium long-call payoff (unbounded max profit + breakeven).**
Options Builder Payoff now summarises expiry P&L from strike kinks
and the right-hand slope, not the ±15% chart sample. A zero-premium
long call shows Unlimited max profit, max loss equal to the premium
(₹0), and breakeven at the strike. Paid-premium long calls and
other unbounded legs (short calls, straddles) use the same rule.
- **Trade Review date filter and mangled timestamps in Example.**
Example `/trade` Trade Review now clips the Log to the committed IST
date range (the same predicate as the sample-journal badge) and
renders fill timestamps as `D Mon YYYY HH:MM:SS` with a literal
space, so `13 Apr 26` can no longer glue onto `14:55:42`. Live and
Practice still use the journal/tradebook path; only the shared IST
format and range helpers changed there.
- **Example /ai chat produces no assistant reply.**
`/ai` and the floating tutor now share one advisor chat path:
a short status probe (including Example/sample-data), SSE
streaming, then the non-streaming fallback. A missing LLM, an
unreachable backend, an empty completion, or an SSE error
becomes a visible assistant error instead of a blank bubble.
Empty assistant placeholders are no longer persisted, so a
reload cannot restore the silent blank. The 45-second stream
budget is first-token only: once a token arrives, a longer
healthy completion is not aborted mid-reply. Native broker
freeze is excluded. MF Optimizer and AI suggestions + deploy
are unchanged.
- **Unfinished Setup recovery without the authenticator.**
**Try with example data** marks a durable Example session so `/home`
survives a refresh. **Start over** wipes the unfinished account via
the account-create setup JWT, so a lost QR seed is recoverable
without the TOTP secret. Daily-login session tokens cannot wipe the
account. Daily login is password-only until an authenticator is
enrolled. Live still needs that authenticator and the PIN.
- **Strategy Lab stays empty after AI Deploy.**
Deploying a suggestion from `/demo-app/ai` (for example “Trend EMA
Crossover”) opens `/demo-app/lab?strategy=TrendEMACrossover`. Strategy
Lab now hydrates the Backtest selector from `?strategy=`, keeps a
linked registry key in the catalogue when the loaded list does not
include it, and enables Run Backtest once a strategy is selected.
Sample demo backtests are unchanged; a Lab opened without the query
is unchanged.
- **Invest dashboard holdings count vs listed sample stocks.**
On `/demo-app/invest` the sample dashboard header now uses the example-data
holdings book (`getDemoHoldings`) as its count source, so the
badge matches the listed sample stocks and the Holdings tab. Live and
Practice still read the live book — an empty funded account stays at
0.
- **Remaining homepage nav overflow at ~390px.** After
#180, the primary nav wrapped without clipping Contribute, but at
about 390px four chips still packed onto the first row and left
“Demo (example data)” cramped against Docs/API. Below 480px the marketing
nav now uses two-across chips with slightly smaller type and tighter
padding, so every primary label stays on one line with slack. The
900px wrap from #180 is unchanged.
- **Docs page summary duplicated as first body paragraph.**
On `/docs*` routes the page summary no longer appears twice. The
generator still lifts the first body paragraph into frontmatter for
SEO, but marks `hideDescription` when that extract matches the opening
body paragraph. The docs page skips `DocsDescription` unless the page
opts in with a distinct subtitle (fail-closed if the flag is missing).
- **Primary nav overflow clips Contribute.** The marketing
header no longer uses a shrinking `overflow-x: auto` row below 900px.
Primary links wrap without shrinking, so Contribute and the other
destinations stay fully visible on phones around 390px.
- **Dependabot qs and fflate (medium).** `qs` 6.15.2 (via `http-server` →
`union`) is overridden to 6.16.0, clearing GHSA-4mjr / GHSA-px8p. `fflate`
0.6.10 (via `three-stdlib`) is overridden to 0.6.11, clearing GHSA-x5fp.
No new allowlist entries.
- **Node audit blockers on main.** Newly disclosed HIGH/CRITICAL advisories
against the existing lock (next Windows/AVIF RCE, maplibre-gl XSS,
`@xmldom/xmldom` name-injection/ReDoS, sharp libheif, js-yaml merge-key
DoS, and four fast-uri host-confusion/SSRF issues) are cleared by real
version bumps: next 16.3.4, and overrides for fast-uri 4.1.4, js-yaml
4.3.2, sharp 0.35.4, maplibre-gl 6.8.0 and `@xmldom/xmldom` 0.8.15. No
new allowlist entries.
- **Production systemd install.** `infra/scripts/setup-production.sh` hardcodes
`/opt/flinttrade` (the prefix `flinttrade.service` already uses), refuses
`FLINTTRADE_DIR`, symlink targets and non-git trees, and requires Python
>= 3.12 before creating `$INSTALL_DIR/.venv`. The unit exports
`FLINTTRADE_WORKSPACE_DIR=/opt/flinttrade/.flinttrade` so Workspace writes
stay inside `ReadWritePaths`, `FLINTTRADE_BACKEND_PORT`, and starts
`python -m flinttrade_core.app` with every workspace package on
`PYTHONPATH`. First-time setup (and later deploys) build
`packages/apps/terminal/dist` with the pinned pnpm and run
`python -m flinttrade_core.cli init --provision-master-password` as
`www-data`, so the non-interactive backend can start and serve the UI.
Checkout-mode normalisation skips `.flinttrade` and `data` so hardened
`0600` secrets stay owner-only. Code and `.venv` stay root-owned; only
runtime workspace/data/log paths are `www-data`. `infra/scripts/deploy.sh`
updates that tree with `sudo git` and does not take ownership.
- **Workspace path unification.** Nineteen modules resolved their own storage as
the literal `~/.flinttrade` instead of asking `flinttrade_core.workspace`. On
Linux that happens to be the workspace, so it never failed in CI; on macOS
(`~/Library/Application Support/flinttrade`) and Windows (`%APPDATA%\flinttrade`)
every one of them wrote to a second, invisible directory that the rest of the
app did not read and the uninstaller could not find. Affected state included
the TOTP secret store and its install key, the trade journal and its
screenshots, saved presets, keyboard shortcuts, quantity-freeze limits, the
pending-order approval queue, the watchlist, expiry and FII/DII stores, and the
operator's own FlowBuilder flows, trained signal models and strategy files.
Every module now resolves its path inside a function body at call time, so
`FLINTTRADE_WORKSPACE_DIR` and `FLINTTRADE_HOME` are honoured on every
construction rather than frozen at import. On a default install each artefact
is **copied** into the platform workspace once, under a cross-process lock; the
pre-workspace original is left untouched, so the upgrade is reversible. Where a
workspace copy already exists it wins and no merge is attempted — an
approval-queue merge could dispatch the same order twice. The TOTP store and
its install key move as one unit, verified by a decrypt round-trip before the
legacy pair is trusted, and the trade journal moves with its screenshot
directory or not at all. Migration probes are skipped entirely when a workspace
environment override is in force.
- Both uninstallers now enumerate every pre-workspace dropping written directly
at `~/.flinttrade/<name>` — flows, models, strategies, journal screenshots,
presets, the TOTP pair and the remaining stores — as named `--purge`/`-Purge`
candidates. They were deleted before, but only as part of the managed root, so
the confirmation list never mentioned the operator's own strategy code.
- `FlowBuilder` and the trade journal no longer fall back to a home-directory
path when `flinttrade_core` cannot be imported. A broken install now fails
loudly and the affected routes degrade to 503, instead of silently opening an
empty shadow store.
### Changed
- Vulnerable ChromaDB persistence is replaced by FlintTrade's local
SQLite/NumPy vector store. Existing vector directories are deliberately not
auto-migrated because Chroma's on-disk index and embedding space are not
compatible with the replacement. If `chroma.sqlite3` is present, FlintTrade
refuses to create `flinttrade_vectors.sqlite` beside it: the database and
vector-segment files are left untouched, RAG stays disabled, and agent
learning uses its logged in-process fallback. To recover existing lessons or
custom documents, export them with the previous release. To intentionally
start empty, move the complete legacy directory aside as a backup before
restarting; do not delete individual segment files. Each collection now
persists one embedding dimension and refuses mixed-width writes after the
first vector, inner-product distance stays unnormalised, and shutdown joins
the optional background RAG indexer before closing the store.
- The traffic and latency observability logs (`traffic_log.duckdb`,
`latency_log.duckdb`) are not migrated: they are disposable, and both were
already workspace-routed in production. On macOS and Windows their history
restarts from empty.
### Security
- **Clear-text secret logging in service-connection tests.** CodeQL
`py/clear-text-logging-sensitive-data` alerts
[#420](https://github.com/navaneeshnagarajan/FlintTrade/security/code-scanning/420)
and
[#421](https://github.com/navaneeshnagarajan/FlintTrade/security/code-scanning/421)
flagged `packages/core/core/tests/test_service_connections.py` for passing a
`secret_version` object to `logger.debug`. The redaction contract still
asserts that binding identity and supplied credential material never appear
in `str`/`repr`, public DTOs, exception text, or captured logs; the logger
now receives only the masked `ServiceSecretVersion(<redacted>)` fixture.
## [0.0.1] — 2026-07-23
Clean-slate baseline. Pre-1.0, pre-usable, and marked as a pre-release: anything
may change without notice until the project reaches a stable 1.0.0.