# FlintTrade User Guide This guide walks you from a fresh install to local setup, sandbox workflows, and Live-mode safeguard verification. The default reading order is top-to-bottom — every section builds on the one before it. If you already have FlintTrade running, jump to the [Workspace tour](#5-workspace-tour) or use the section list in the sidebar. > **Beta software.** FlintTrade `v0.0.1` is not production ready and > does not provide financial advice. Read [disclaimer.md](../disclaimer.md) > before connecting a broker or switching to Live mode. > **One local app, nine workspaces.** The app bar lists Terminal, Testing, > Portfolio, Automation, Reports, Learn, Brokers, Settings, and System. > Older addresses still open the matching workspace and keep their query > string and hash. The map is in [The main routes](#the-main-routes). --- ## 1. Installation FlintTrade runs on Windows, macOS, and Linux (including Raspberry Pi). It is a **self-hosted web app first**: one backend process serves the full terminal UI and API on a single origin (port 5100), usable from any browser. The desktop apps are convenience wrappers around that same backend for people who prefer a one-click install. ### One-line install (recommended — no prerequisites) The web-app installer runs in the shell your OS already ships (bash or zsh on macOS and Linux, built-in PowerShell on Windows) and needs no other toolchain: no Python, no Node, no git and no make. ```bash # macOS / Linux curl -fsSL https://flinttrade.vercel.app/web-install.sh | bash ``` ```powershell # Windows 10/11 # Run in a normal (non-Administrator) PowerShell window irm https://flinttrade.vercel.app/web-install.ps1 | iex ``` It provisions a pinned, checksum-verified toolchain (`uv`, Python 3.14, Node and pnpm) under `~/.flinttrade/tools`, builds FlintTrade from a managed source checkout at `~/.flinttrade/web-src/FlintTrade`, and installs a `flinttrade-web` launcher — `~/.local/bin/flinttrade-web` on macOS and Linux, `%LOCALAPPDATA%\Programs\FlintTradeWeb\flinttrade-web.cmd` plus a **FlintTrade Web** Start Menu shortcut on Windows. The Electron desktop shell keeps its own launcher, Start Menu entry and source checkout, so the two installs never collide and can be run in either order. Open http://127.0.0.1:5100 and follow the first-time Setup flow — no `.env` file is required. Removing it again is covered in [Uninstall](#13-uninstall). #### If the site is unreachable (repo-direct fallback) Use this whenever the command above fails. The hosted URLs are only a redirect to the scripts in this repository, so a site outage or a deployment without an immutable source commit answers `503` — and piping that error page into a shell does nothing useful. These commands fetch the same script straight from GitHub and depend on no deployment: ```bash # macOS / Linux curl -fsSL https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-web-install.sh | bash ``` ```powershell # Windows 10/11 # Run in a normal (non-Administrator) PowerShell window irm https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-web-install.ps1 | iex ``` To read the script before running it, clone the repository and run it from the checkout instead: ```bash git clone https://github.com/navaneeshnagarajan/FlintTrade.git cd FlintTrade bash scripts/install/flinttrade-web-install.sh ``` On Windows, run the checkout copy with `powershell -ExecutionPolicy Bypass -File scripts\install\flinttrade-web-install.ps1`. ### Run from source (contributors) Use this when you are developing FlintTrade itself. It expects you to supply the toolchain yourself: git, Python 3.14+, Node 22.23.4+, `uv` and `pnpm`. ```bash git clone https://github.com/navaneeshnagarajan/FlintTrade.git cd FlintTrade uv sync pnpm install pnpm --filter @flinttrade/terminal build python scripts/ft.py start ``` The same six lines run unchanged in Windows PowerShell — do not join them with `&&`, which Windows PowerShell 5.1 does not support. `python scripts/ft.py ` is the cross-platform runner (no make and no bash needed); `make ` is the POSIX alias. The terminal build step is not optional: the backend serves the UI only when `packages/apps/terminal/dist/index.html` exists, so skipping it leaves you with an API and no interface. Docker (`make docker-up`) is an advanced, POSIX-only path — it needs make and Docker, so it is not a zero-prerequisite way in. A `.env` file is optional: the default profile starts the backend, the terminal build and nginx, and the UI is served at http://localhost:8080 (override with `FLINTTRADE_HTTP_PORT`). ### Native desktop (Electron convenience shell) The desktop package is a small Electron shell. It verifies pinned tools and builds an inspectable local source checkout on first launch instead of downloading a frozen backend payload. Its release contract is one universal macOS DMG, one Windows x64 NSIS installer, Linux x64 and ARM64 AppImages, and `SHA256SUMS.txt`. The public [download surface](https://flinttrade.vercel.app/download) distinguishes the source-built web-app install above from Electron-shell installers. It withholds Electron commands and downloads unless one release contains all four canonical installers plus `SHA256SUMS.txt`. For a release that passes that gate, the supported commands are: ```bash # macOS / Linux curl -fsSL https://flinttrade.vercel.app/install.sh | bash ``` ```powershell # Windows 10/11 irm https://flinttrade.vercel.app/install.ps1 | iex ``` If the site is unreachable, the same desktop installer runs repo-direct — swap `flinttrade-web-install` for `flinttrade-install` in the fallback commands under [If the site is unreachable](#if-the-site-is-unreachable-repo-direct-fallback). The installed shell builds the hash-verified, integrity-locked source on first launch (progress on the splash; needs internet), creates the OS workspace, and opens Setup only after the source guardian is healthy. Source/runtime updates are staged and health-proved separately from Electron-shell installer updates. Manual downloads and per-OS caveats are covered in [DESKTOP.md](DESKTOP.md). The matching desktop uninstall is in [Uninstall](#13-uninstall). Contributors can package the shell locally (these lines run unchanged in bash, zsh and Windows PowerShell): ```bash pnpm install --frozen-lockfile python scripts/ft.py desktop-test python scripts/ft.py desktop-package ``` On POSIX, `make desktop-test` and `make desktop-package` are aliases for the same targets. Install the generated package from `packages/apps/desktop/release/electron/`, launch FlintTrade, and follow the first-time Setup flow. Local macOS output is always ad-hoc sealed and has no Developer ID trust. Only release CI can use complete Apple distribution-signing and notarisation secrets. ### Terminal dev server (contributors) When you are changing terminal code, run the Vite dev server alongside the backend instead of the built UI. It requires Python 3.14+, Node.js 22.23.4+, Git, and optionally Rust for `core/ticks`. ```bash python scripts/ft.py setup python scripts/ft.py dev ``` Open `http://localhost:5173` once the dev server is ready. On POSIX, `make setup` and `make dev` are aliases for the same targets. ### Platform-specific setup For step-by-step instructions tailored to each operating system, see: - [Windows setup](setup/windows.md) - [macOS setup](setup/macos.md) - [Linux setup](setup/linux.md) - [Raspberry Pi setup](setup/raspberry-pi.md) - [Quick start (cross-platform)](setup/QUICKSTART.md) ![Welcome screen](screenshots/01-welcome.jpg) *The /welcome route — first-time cinematic introduction. Get Started opens Setup. Try with example data opens sample data, labelled Example.* --- ## 2. First broker connection Native broker access is **not usable on this unreleased line**: native broker UX stays down on `main`. Do not treat catalogue metadata as a working operator path. Broker connections use six native adapters: Dhan, Upstox, Kotak Neo, INDmoney, Groww, and Delta Exchange. A broker session cannot be started from the terminal today. Practice mode and example data work without a broker. Broker writes return 503 and broker reads return 409. Live order placement stays disabled unless it is explicitly unlocked and every safety layer passes. **Broker HTTP.** Broker-account mutations (`/v1` account and auth writes, native connect / login / set-primary / delete, and OAuth start / callback) return `503` with `broker_account_cutover_unavailable`. Native HTTP account and market-data reads return `409` with zero provider calls. Exact reads use the in-process `BrokerReadPort`. Exact broker reads that already exist are that in-process port, not a working terminal Brokers screen. Service connections are an inert control plane only. When native connect returns, Dhan, Upstox, and Kotak Neo are evidence-gated as enabled in the catalogue. Kotak Neo is Connected (read) / API smoke only after persisted REST smoke evidence — never placeable Live; Neo has no sandbox (`Live read only until funded unlock.`). Kotak Neo's v3 async market and order feeds are wired and locally tested with synthetic SDK clients, but no live-account or market-hours stream proof has been recorded. The previous non-funded Connected (read) evidence covers REST reads only. Upstox Developer Apps analytics tokens would connect as read-only sessions. INDmoney uses a dashboard-generated token that resets at the daily 06:00 IST dashboard cycle, but remains disabled until its smart-parent, atomic reduce-only, and live order-safety blockers clear. Groww retains its displayed activation blockers and may also require approving the API-key session in Groww Cloud before FlintTrade can mint a token. Delta Exchange stays disabled until Live order safety and unattended deadman flattening are proven. Its configured authentication method uses an API key and secret for one venue (India or Global, production or testnet): `india_prod`, `india_testnet`, `global_prod`, or `global_testnet`. India and Global keys are not interchangeable, and the venue has no default. Setup → Brokers still fails on those routes. Read smoke on a static-IP host uses the in-process native read path, not a restored Brokers HTTP session. Localhost postback URLs are for diagnostics unless you expose FlintTrade through a broker-reachable tunnel or public URL. ### Why two layers? --- ## 3. First Practice order Before enabling any order-capable integration, exercise the order path in **Practice**. A fresh browser with no saved Mode opens in Practice. Password sign-in and account setup open in Practice. A fresh login opens Practice. Quick Unlock restores the existing session and keeps its Mode. Live is entered only through `POST /v1/auth/live`. `POST /v1/auth/mode` accepts only a switch to Practice. The Mode chip in the TopBar is taken from the JWT `mode` claim when the session token has one, and shows **Practice**, **Live**, **Example**, or **Unavailable**. An opaque sample session has no claim to apply and stays **Example**. The menu lists **Practice** and **Live**. **Unavailable** means the session mode could not be confirmed. The Scalper and the line above the widgets say so, and ask the operator to sign in again or reload. That copy never says Example or Sample. **Connected (read)** is a broker status on the Broker row, not a menu item and not a session claim. **Example** is sample data, not a Mode. The public web demo (`/demo-app`) is **Demo (example data)**. The public site paths `/demo` and `/explore` permanently redirect (HTTP 308) to `/demo-app/welcome`. | Label | What it means | |---|---| | **Practice** | Simulated fills, no real money. Mode line: `Practice — simulated fills, no real money.` Order review: `Confirm places this simulated order.` Review confirm: **Confirm simulation**. Submit labels: **Practice Buy** / **Practice Sell**. | | **Connected (read)** | Broker status for a native Dhan or Kotak Neo account after a read check succeeds. It is not a session claim, not a Mode, and not in the Mode menu. Non-funded live reads use this label. Broker-connect helper copy may describe that read check as API smoke. Copy for Neo is `Live read only until funded unlock.` A broker session cannot be started from the terminal today. Broker-account writes return HTTP 503 and native HTTP reads return HTTP 409. | | **Live** | Real orders on a live broker session. Mode line: `Live — real-money capable when a broker is Connected. Orders place only on a live session.` Submit label: **Place BUY Order** / **Place SELL Order**. | | **Example** | Sample data. The chip reads **Example** for claim `explore`, including **Try with example data**, which has no JWT payload. A JWT whose `mode` is missing or not `explore`, `practice`, or `live` reads **Unavailable**, never Example. The signed-in name is **Guest**. Mode line: `Example data. No broker is connected and no orders are sent.` Welcome and sign-in offer **Try with example data**. Submit labels: **Example Buy** / **Example Sell**. Review confirm: **Continue**. That confirm records a sample fill: `Example order placed`, with an id starting `SAMPLE-`. | | **Unavailable** | The session mode could not be confirmed. The chip reads **Unavailable**. Orders stay blocked. Sign in again or reload. | **Switching Mode.** Open the Mode chip. Its accessible name is the current label plus `mode. Open the mode menu.` Choose **Practice** for simulated fills. Choosing **Practice** opens Setup and does not call `POST /v1/auth/mode` while the signed-in name is **Guest**. A Live session downgrades with that route. **Connected (read)** is not in the Mode menu. A broker session cannot be started from the terminal today. Broker-account writes return HTTP 503 and native HTTP reads return HTTP 409. Choose **Live** only when the item is enabled. Opening the menu does not open the Live dialog. An eligible Live choice opens **Switch to Live Trading?** with the line `You are about to switch to Live mode. All orders will be executed with real money through your broker.` Enter the 6-digit PIN (`Enter your PIN to confirm`) and choose **Switch to Live**. Cancel leaves the current Mode. **When Live is locked.** The Live item lists every reason that applies, in this order: - `Enrol 2FA and connect a broker` — the authenticator is not enrolled, or the desk reports no broker connection. Either gap is enough for this one reason. The connect half is the shipped label, not an available connection action. A broker session cannot be started from the terminal today. When the authenticator is not enrolled, including after **Later** on **Two-factor authentication**, **Enrol authenticator in Settings** links to Settings → Security (`/settings#security`). The Live switch does not ask for an authenticator code. - `Create a PIN in Settings` — no PIN exists. Live stays locked, and those words link to Settings → Security (`/settings#security`). - `Not qualified for Live` — only when the place gate reports that this operator is not qualified. If that report is absent, this reason is not shown. The desk starts **Down**. A missing heartbeat is not painted **Ready**. **Example-data marker.** Where a sample figure still uses the shared banner, that banner reads `Example data. Connect a broker to see your own.` It never shows in Practice. On Invest, an Example chip on a sample view replaces that banner. Where those chips sit is under [Invest](#invest). **Order review.** Practice reads `Confirm places this simulated order.` The confirm button reads **Confirm simulation** (accessible name `Confirm simulated Practice order`). Example data reads `Example only. Nothing is sent to a broker and no order is placed.` The confirm button reads **Continue** (accessible name `Confirm Example order`). **Continue** records a sample fill. The success line is `Example order placed` and the id starts with `SAMPLE-`. Both reviews offer **Back to edit**. While the confirm is in flight the button reads `Confirming…`. **Mode, session, and sample data.** Practice and Live name the session Mode. **Connected (read)** is a broker status, not a Mode. TopBar session chips (Continuous · CAS · Matching · Post-close · Closed) are market-session status, never Live mode. Example data uses **Example Buy** / **Example Sell**. After review, **Continue** records a sample fill (`Example order placed`, id starting `SAMPLE-`). Practice keeps **Practice Buy** / **Practice Sell**. Live uses **Place BUY Order**. Connected / green is never shown for an unconfigured subsystem. **One home per status.** - **Feed** lives once, at the start of the ticker. The chip is feed provenance, not a Mode. Example data always reads **Example**. Practice and Live may read **Live**, **Delayed**, **Stale ·** age, or **Unknown**. Practice with no feed reads `No live feed (Practice)`. The Practice ticker shows `Last close prices · Connect a broker for live prices →` only when the market session is not unavailable and neither the WebSocket nor a fresh REST fallback is producing quotes. The link opens `/settings#brokers` and does not start a broker session. An unavailable session is never labelled last close. - **Broker** lives once, as the **Broker** row in the TopBar **Status** menu: **Connected**, **Connected (read)**, or **Unavailable**, plus a plain failure when a money-path incident darkens the session. Account switching does not add a second broker row. The decision model, labelled Laya, and LLM are the other rows in that menu. - **Market** lives once, in the TopBar market-session chip. A confirmed closed cash session reads `Market closed · opens 09:15`. Unavailable timings read `Market unavailable`. Neither sentence is Live mode. **Market session.** The TopBar market chip shows the open phase (**Continuous**, **CAS**, **Matching**, or **Post-close**). From 7 December 2026 the same chip can also read **Transition**. A confirmed closed cash session reads `Market closed · opens 09:15`, not the word Closed on its own. Unavailable timings read `Market unavailable`, and are never labelled last close. The chip tooltip/title is the window, for example `CAS · 15:15–15:35 (as of Aug 2026)`. Cash is never shown as green "open" after 15:15 IST; CAS is not Closed. When equity F&O still runs after cash continuous ends, a secondary `F&O open · till 15:40` chip appears. The Market Clock widget follows the same cash timeline: Continuous (~09:15–15:15) → CAS 15:15–15:35 → Matching → Post-close 15:50–16:00. Non-CAS cash still trades continuous to 15:30. From 7 December 2026 the same chips follow NSE/CMTR/77355 and NSE/FAOP/77356: continuous until 15:30, auction 15:30–15:40, matching 15:40–15:45, Transition 15:45–15:50, post-close 15:50–16:00, and `F&O open · till 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. BSE equity derivatives use the same close. Dates before 7 December 2026, and the Muhurat session, keep the earlier clock, which has no Transition phase. There is no flat "Market open until 15:30" or "VWAP last 30 min" closing-price copy. The September 2026 consultation stays out of the UI. **Mode honesty.** One line under the TopBar, always, owned by the active label. Example data reads `Example data. No broker is connected and no orders are sent.` Practice reads `Practice — simulated fills, no real money.` Live reads `Live — real-money capable when a broker is Connected. Orders place only on a live session.` Widgets identify illustrative and fallback figures with Sample/Example source labels. Mode is not provenance. A figure that stays fabricated in Practice and Live, such as benchmark index returns, keeps the Example chip. An incident strip, when one is showing, sits between the TopBar and this line and does not replace it. **Operator status strip.** One sticky strip sits between the TopBar and the Mode line. It is Info, Degraded, or Blocked. Example-data copy and the Practice Mode line are not this strip. Live risk, a broken desk, a broker fault, or a local-network fault uses Degraded or Blocked. There is not a second banner for the same fact. While the strip is Blocked or Degraded on the money path, broker chrome normally says **Unavailable** or **Degraded** plus the failure in plain words — never **Connected** or **Connected (read)**. Failure class Laya is the exception: Broker may stay **Connected** or **Connected (read)** while Live place stay muted. Other money-path classes mute Live place the same way, with one rectify line. Kill All and the safety layers stay reachable. Chat being down does not close Live orders. A public site outage does not mean the local desk cancelled broker orders. The TopBar **Status** menu is the one home for **Broker**, the decision model, and **LLM**. The decision-model row is labelled **Laya**. That label is the model name, and the value is the readiness word. Settings shows the same model as **Laya decision model**. Each row has its own label, value, and one-line description. Broker's line is `Your broker account for live orders and holdings.` Laya's line is the current reason, or `Checks every order before it is placed.` LLM's line is `Optional AI model for chat and suggestions.` The same three rows sit in the More sheet on a narrow window. Feed provenance stays on the ticker. The button's dot is the worst state. Example data is neutral and reads `Example data only`, including when Laya is **Down**. After that, Laya **Down** is red, a broker failure that marks the path unavailable is red, and Live with no broker connected is red (`No broker connected`). Laya **Degraded**, a degraded broker path, and an LLM of **Error** or **Disconnected** are amber. **Checking** (the summary reads `Checking Laya`), **Still loading** (`Laya still loading`) and **Downloading** (`Laya starting`) stay neutral. In Practice, broker **Unavailable** with Laya **Ready** is neutral and reads `Broker unavailable`. `All systems ready` appears only when no row is worse than Ready. Broker's value is **Connected**, **Connected (read)**, or **Unavailable**. Laya's value is **Ready**, **Degraded**, **Down**, **Still loading**, **Downloading**, or **Checking**. Laya starts **Down**, including before a heartbeat and when the ping omits `laya`. Missing status is never painted **Ready**. While the value is **Checking**, the row reads `Checking Laya…`. Download and model lines, and how to start the managed Laya process, are in [Start Laya](#start-laya): `Downloading the model · X of Y GB`, `Can't download the model`, `Wrong model version`, and `Can't verify the model`. The refusal while orders are paused is `Laya is Down. New orders are paused until it's Ready. You can still close positions.` Only Live-facing **Down** opens the Laya Blocked strip and mutes Live place. Practice place follows the Practice chip. Live-facing **Degraded** leaves Live open, shows **Laya Degraded — tighter limits**, and does not look Blocked. LLM is **Not configured**, or **Connected (suggest only)** when Chat is ready. Install-host disk, RAM, CPU, GPU, and network stay on Settings → Monitoring (`/settings#monitoring`). That page is its own Settings section. See [Monitoring](#monitoring). It is not folded into this Broker / Laya / LLM menu. FlintTrade does not hold client funds, reverse broker fills, or file a dispute. Rectify steps point at the broker, the exchange, or the host: | Strip | What it means | What to do | |---|---|---| | Exchange (`exchange`) | Unavailable or Degraded — exchange/session. A broker reject names a halt or circuit. The session clock and CAS chip never raise this. | Wait for the session. Check [NSE circuit breakers](https://www.nseindia.com/products-services/equity-market-circuit-breakers). Manage open risk in the broker app. | | edge/CDN | Public site/CDN unreachable. The install/update or public-site fetch failed and the local desk ping (`/api/v1/ping`) still succeeded. | Install from the repository. [Cloudflare status](https://www.cloudflarestatus.com/) and [Vercel status](https://www.vercel-status.com/). A site outage does not cancel broker orders. | | Broker sign-in (`broker_auth`) | The broker session or token failed. Connected (read) only after a read smoke succeeds. | Sign in again under Settings → Brokers. Retry once. No automatic re-smoke. | | Broker connection (`broker_rest`) | The broker API failed. | Check the broker status page, then retry once. | | Broker stream (`broker_stream`) | A Dhan or Kotak Neo market/order stream dropped. Kotak Neo's local v3 lifecycle coverage is not live-account proof. | Wait for the stream. Do not treat stale REST quotes or a reconnecting socket as live. | | Broker rate limit (`broker_rate_limit`) | The broker asked us to slow down. | Wait for the window, then retry once. The account poll stays quiet until then. | | Broker maintenance (`broker_maintenance`) | The broker reported maintenance. | Wait, then check the broker status page. | | Laya (`laya`) | Blocked — Laya is Down ("Laya is Down. New orders are paused until it's Ready. You can still close positions."). New Live place stay closed. Close and Square off stay available, with no extra confirmation. The server admits a close that is the same contract, the opposite side, and no larger than the open quantity minus pending exits. On Live, the position book and the broker order book must both be readable. Pending exits include the broker's open orders and this desk's own pending exits. If the broker order book cannot be read, a reduce-only close is refused. There is no unreadable-book fallback. That order takes the full check and, while this strip is open, is refused with that Down line. A second exit on the same broker account, while one of yours on that contract is still unfilled, is refused with `Not placed. An exit for SYMBOL is already pending. Wait for it to fill, or cancel it and try again.` when the broker order book can be read, and the row shows **Exit pending**. The Live hold is for that broker account. When a retained exit exists and the broker's orders cannot be read, that refusal is `Not placed. Broker orders for SYMBOL are unavailable. Reconcile them before another exit.` The label is the symbol, or `this contract` when the symbol is empty. The place control does not also show **Laya denied** while this mute is up. Broker and LLM keep their own labels; Broker may stay **Connected** or **Connected (read)**. Chat cannot place instead. The strip also says **Kill All stays available.** Cancel-all only cancels. Laya starts Down. The desk ping publishes Live-facing `laya`, sidecar `laya_practice`, and `laya_live_qualified`, and does not invent Ready. Live-facing Ready or Degraded closes this strip. Degraded keeps Live open with a tighter quantity ceiling and the quiet line **Laya Degraded — tighter limits**. The Laya chip label follows the current mode. Practice shows Ready, Degraded, or Down from the managed Laya process, and it does not read Down while Practice orders are being admitted. During the first load the chip says Still loading. After Start Laya, until the ping confirms the new state, the chip says Checking and the popover says Checking Laya…. In Live the chip shows Live-facing status. "Not qualified for Live" is the chip tooltip and the popover line when the managed Laya process is up and Live is not qualified. See [Start Laya](#start-laya). A base checkpoint leaves Live unqualified. | While the strip is open, a new Live place stays muted on that strip. A reducing close can still be sent when the books prove it. Positions, the Practice Order Pad and the Live Order Pad show **Exit requested. Exits are allowed while Laya is Down. Check positions and orders for the outcome.** Exit-all shows **Exit-all requested. Check positions and orders for the outcome.** A Practice place is refused when Practice itself is Down, with "Laya is Down. New orders are paused until it's Ready. You can still close positions." Live-facing Ready or Degraded allows a Live place attempt. Do not treat Chat as a substitute. | | Chat provider (`llm_provider`) | Info — Chat is unavailable. Trading chrome stays as it was. A Laya denial is not this strip. | Retest or switch provider under Settings, or use a local model. Keep trading without Chat. | | Host unhealthy (`host_unhealthy`) | The desk health check failed or is degraded. | Free disk space, restart the desk, and read `/health/detail`. Live stays closed until the desk and broker trust are back. A restart does not recover fills. | | Backend unreachable (`backend_unreachable`) | The FlintTrade backend did not answer, or native broker HTTP returned the freeze (`503`). | Restart the desk and read `/health/detail`. The freeze line stays until the cutover replaces it. Kill All stays reachable when the risk runtime allows. | | Local network (`network_local`) | The desk process is up and cannot reach the public internet (gateway, DNS, or ping). A site/CDN miss with the desk ping still ok is edge/CDN. A single broker timeout is not this class. | Check the link or switch network, then retry. | Dispute and status links, if you need them, are the broker's or the regulator's — not a FlintTrade claims desk: - Dhan support: https://dhan.co/customer-service/ and grievances: https://dhan.co/grievance/ - Kotak Neo trade API: https://www.kotakneo.com/support/trading/trade-api-and-terminals/ and the complaint procedure: https://www.kotakneo.com/support/procedure-for-filing-a-complaint-with-kotak-securities/ - SEBI SCORES: https://scores.sebi.gov.in and SMART ODR: https://smartodr.in ### Start Laya Laya is opt-in and starts **Down**. The desk does not paint it **Ready** until a health probe says so. Admission is fail-closed: while Laya is **Down**, including the first load, orders are refused. **Degraded** is not Ready, and it is not a refusal of every order: a place inside the tighter ceiling can continue. See [Laya on place](#laya-on-place). **Chip.** The label follows the active mode. Practice shows the managed Laya process: **Ready**, **Degraded**, **Down**, **Still loading**, or **Checking**. Live shows the Live-facing status. During the first load the label is **Still loading** and does not read **Down**. Orders are still refused with **Laya is Down. New orders are paused until it's Ready. You can still close positions.** **Checking** uses the neutral colour. The popover says **Checking Laya…**. It is not a stale **Ready**. The chip tooltip is the hover text. For `not_started`, `stopped`, `port_in_use`, `still_loading`, and `unreachable` it is the chip label followed by `. Next: python -m flinttrade_core.laya_runtime start`. `downloading` has no tooltip and no Next line. `download_failed`, `unverified`, `wrong_revision`, and `key_rejected` keep the sentences in the table. When the managed Laya process is **Ready** or **Degraded** and Live is not qualified, the tooltip is **Not qualified for Live**. That line is not painted as **Down** on the Practice chip. Clicking the chip opens a popover. It shows the chip label, the link **How to start Laya** (this section), and an operator-only **Start Laya** button. The button is there for a signed-in operator while the managed Laya process is not **Ready** or **Degraded**. A signed-out desk does not get it, and neither does a demo session. While the start request is in flight the button reads **Starting…**. If the start fails, the popover says **Laya could not be started.** **Start Laya** calls `POST /api/v1/laya/start`. After that click the chip 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`. It does not show a stale **Ready** during that wait. 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. The popover prints the chip label, not the raw code. For `download_failed`, `unverified`, `wrong_revision`, and `key_rejected` it also prints the tooltip, because that sentence does not start with the label. The status word on the chip is **Down**, except during the first load, when the chip says **Still loading**. A download in progress reads **Downloading** in the neutral colour and is not **Still loading**. `` in the port label is the managed Laya port. | Code | Chip label | Tooltip | |---|---|---| | `not_started` | Not started | `Not started. Next: python -m flinttrade_core.laya_runtime start` | | `stopped` | Stopped | `Stopped. Next: python -m flinttrade_core.laya_runtime start` | | `port_in_use` | `Port in use` | `Port in use. Next: python -m flinttrade_core.laya_runtime start` | | `still_loading` | Still loading | `Still loading. Next: python -m flinttrade_core.laya_runtime start` | | `downloading` | `Downloading the model · X of Y GB` | (none) | | `download_failed` | Can't download the model | `Check your connection, then Start Laya again.` | | `unreachable` | Unreachable | `Unreachable. Next: python -m flinttrade_core.laya_runtime start` | | `unverified` | Can't verify the model | `The installed model couldn't be checked against the pinned version. Restart Laya. If it keeps happening, reinstall it.` Applies when this start did not download. A failed download shows **Can't download the model** instead. | | `wrong_revision` | Wrong model version | `Laya is running a different model than FlintTrade expects.` | | `key_rejected` | Can't reach Laya | `Laya restarted with a new key. Reconnecting…` | | `key_missing` | The Laya API key file is missing. | `The Laya API key file is missing.` A health check does not replace this with Not started. | `downloading` uses the status word **Downloading** (neutral colour) and `download_failed` uses **Down**. Neither is **Still loading**. Orders are refused with `Laya is Down. New orders are paused until it's Ready. You can still close positions.` The `downloading` chip text is live progress, one decimal place, decimal gigabytes: `Downloading the model · X of Y GB`, for example `Downloading the model · 1.2 of 3.4 GB`. It has no tooltip and no Next line. A new model version uses that same chip and the same **Down** word. There is no separate Updating label. When FlintTrade pins a new model version, the next start downloads it the same way. Your current copy stays in place until the new one is verified. If the download fails, you'll see **Can't download the model** and Laya stays **Down** until you retry. `download_failed` is that result: a dropped connection, a partial download, or a swap that put the previous copy back. An older copy on disk does not change that chip, including one whose bytes are not the pin and one that is unverified (an extra loadable file, or a missing companion whose weight digest matches). The chip stays **Can't download the model**, and Laya does not start. `wrong_revision` is only a real mismatch: a complete download whose files do not match the pin, a copy already on disk that this start is not replacing, or a running Laya that reports another revision or digest. A dropped connection, a partial download, or a failed download or swap is not this code. A missing digest is not this code. `key_rejected` keeps the chip **Down**. Orders are refused. When a place is refused because Laya cannot be reached, or because it rejects the key, the chip updates on that same order. A connection failure or a timeout shows **Unreachable**. A rejected key shows **Can't reach Laya**. The refusal text stays `Laya is Down. New orders are paused until it's Ready. You can still close positions.` Every chip-Down refusal reads `Laya is Down. New orders are paused until it's Ready. You can still close positions.` That sentence is the same in Practice and in Live. It carries no quantity ceiling. `identity_absent` is not a chip code. When the chip is **Ready** and a single decision carries no proof, the refusal code is `laya_unverified` and the refusal reads `Not placed. Laya's decision couldn't be verified. Try again.` On each managed Laya start FlintTrade hashes `model.safetensors` and every file in `[checkpoint.manifest]` before launch, and writes a runtime record for that run. The record holds the sha256, pid, and start token, and the inode, size, and modification time of the weights file and of each pinned file (`/runtime/laya/verification.json`, with the token and pid also in `run.json`). Every `stop` deletes that record, as does a start that fails after it was written. A record from an earlier run is rejected. A decision without `revision` or `sha256` is checked against that record for both admitted and clamped orders. The decision log is `/runtime/laya/decisions.jsonl`. It records `proof=decision` when the decision carried the pin, or `proof=runtime` when this run's record stood in. An empty note does not call the model. An admitted Practice place still writes one line, `effect=clamp` with `failure=note_absent` and no proof, including when the quantity already fits. When this run's record stood in, each model allow keeps its own `effect=allow` `proof=runtime` line. There is no dedupe. A model decision with no proof is refused with `Not placed. Laya's decision couldn't be verified. Try again.` A health document that omits the digest is **Ready** when that record matches the pin. Laya is not **Ready** by default. The pins live in `packages/services/engine/src/flinttrade_engine/laya_policy.toml`. `[checkpoint]` names `revision` beside `sha256`, and the weights file `model.safetensors`. `[checkpoint.manifest]` pins these files by sha256: `rl_agent_config.json`, `encoder/config.json`, `tokenizer/tokenizer_config.json`, and `tokenizer/tokenizer.json`. FlintTrade hashes each of them before launch. When the files are already on disk and this start is not replacing them, a missing pinned file, a shard index (`model.safetensors.index.json`), or any extra weights file or other file the launcher could read shows **Can't verify the model** and the managed Laya process does not start. A changed byte in a copy that this start is not replacing shows **Wrong model version** and the managed Laya process does not start. A changed byte in the runtime checkpoint starts the download below; the managed Laya process does not start on that tree. Laya does not reach **Ready** in these cases. A verified boot sets `LAYA_WEIGHTS_PATH` to that hashed weights file. A model already in the standard Hugging Face cache is accepted. When that file is the cache symlink (`snapshots//model.safetensors` pointing at `blobs/`), the launch path is the snapshot file, not the blob. The managed Laya process loads that snapshot directory. A blob path is still refused. The boot runs offline (`HF_HUB_OFFLINE=1`, `TRANSFORMERS_OFFLINE=1`). It does not pass a repo id or a revision. When the managed Laya process health document leaves the revision empty, FlintTrade fills the pinned revision from the verified manifest, so the chip leaves **Still loading**. The launch log line is `laya weights path= sha256=`. The recorded inode, size, and modification time are rechecked, without hashing again, when Laya reports Ready and on each watch tick, about every 1.5 seconds. If one changes, the chip shows **Can't verify the model** and the log line is `laya weights path= changed=`, where `` is `inode`, `size`, `mtime`, or a comma-separated list of those. The same watch reads the pid file (the process id file under `runtime/laya`), the key file (`runtime/laya/api.key`), and the runtime record. The desk polls `GET /api/v1/ping` every 1.5 seconds. That ping reconciles those same files the same way an order does, so the chip and the order gate read the same state. A command-line stop or start, or a key rotation, shows on the chip by the next 1.5-second check. After **Start Laya**, until the 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 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 stays that sentence. **Command line.** From the FlintTrade environment (the project `.venv` after setup, or `uv run python`): ```text python -m flinttrade_core.laya_runtime install python -m flinttrade_core.laya_runtime start python -m flinttrade_core.laya_runtime status python -m flinttrade_core.laya_runtime stop ``` `install` creates the managed Laya environment at `/runtime/laya/venv` (on Linux, `~/.flinttrade/runtime/laya/venv`, unless `FLINTTRADE_WORKSPACE_DIR` is set). That environment can share the base interpreter with FlintTrade: its `python` is often a symlink to the same executable, and the packages stay in the managed Laya environment. The install refuses to put them in the FlintTrade environment. The default install is CPU-only torch from `https://download.pytorch.org/whl/cpu`, then `laya[serve]==0.3.21`. The constraints file pins `torch==2.14.0+cpu`, `laya==0.3.21`, `huggingface_hub==1.33.0`, and `tqdm==4.70.1` with no extras (the Laya install constraints file in the backend package). The download progress class subclasses that pinned `tqdm.auto.tqdm`. The `serve` extra stays on the install requirement, because a constraints file cannot name extras. Torch is installed first. The install size is roughly 1.2 GB. `install --accelerator cuda` and `install --accelerator rocm` are opt-in. `start` still uses CPU (`LAYA_DEVICE=cpu`). `start` listens on `127.0.0.1` only. The port comes from `LAYA_PORT` and defaults to 8000. A port clash is **Down** with `port_in_use`. `start` writes a fresh API key to `/runtime/laya/api.key`. When `LAYA_API_KEY_FILE` points at that same path, `start` replaces the 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.** `stop` stops the managed Laya process, deletes that key, and records **Down** with `not_started`. `status` prints the report, including `reason` and the plain-words `detail`, and does not print the API key. If the weights file or any pinned manifest file is not already installed, `start` downloads the pinned commit from `[checkpoint] revision` first. That step does not start the managed Laya process, and it does not use the default branch. The files land in `/runtime/laya/staging`, not in the launch directory and not in the shared Hugging Face cache. The download sets `HF_HOME` to `/runtime/laya/hf-home` and `HF_HUB_DISABLE_XET=1`, so transfer logs stay in that folder and not in the shared cache. The model is about 2.37 GB, and that size is reported once. While that runs, the popover reads **Downloading the model · X of Y GB** (live progress, one decimal, decimal GB). Orders are refused with **Laya is Down. New orders are paused until it's Ready. You can still close positions.** FlintTrade then hashes the weights file and every manifest file in the staging directory. When no checkpoint is already there, a full match renames that directory onto `/runtime/laya/checkpoint` and launches the offline verified boot. Hub access stays off for that launch. When FlintTrade pins a new model version, the next start downloads it the same way. The chip is the same **Downloading the model · X of Y GB** line, with status **Downloading**. There is no Updating label. Your current copy stays in place until the new files match the pin. On a full match, FlintTrade renames `/runtime/laya/checkpoint` aside to `checkpoint.old-` in that same runtime directory, renames staging onto `checkpoint`, then deletes the old copy, and launches the offline verified boot. If that second rename fails, the old checkpoint is renamed back and the chip shows **Can't download the model**, with the tooltip **Check your connection, then Start Laya again.** The managed Laya process does not start. A complete download whose files do not match the pin shows **Wrong model version**. Staging is deleted and a checkpoint already on disk is left in place. A dropped connection, a partial or missing file, or a read error shows **Can't download the model**, with the same tooltip. A failed download or swap is that same chip, not **Wrong model version** and not **Can't verify the model**, whatever older snapshot is on disk. **Can't verify the model** stays when this start did not download. An extra loadable file in a complete download shows **Can't verify the model**. On any of those results the staging directory is deleted and the shared model cache is left alone. The managed Laya process does not start on files that do not match the pin. A copy already on disk is **Wrong model version** only when this start did not download. 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 and one or more `checkpoint.old-*` copies remain, the last `checkpoint.old-*` name is restored onto `checkpoint` and any other aside copies are removed. If that restore fails, the aside copy stays where it is and that cleanup is skipped. A copy that was restored is then checked against the pin. If it does not match, Laya does not start on it; the pinned download runs instead. If that download fails, the chip is **Can't download the model** and the restored copy stays. A missing or extra file in a model that this start is not replacing still shows **Can't verify the model**. The log line for the download step is `laya download repo= revision=`. Start the backend with `LAYA_HOST=127.0.0.1`, the same `LAYA_PORT`, and `LAYA_API_KEY_FILE` set to that `api.key` when the managed Laya process was started separately. The next health probe attaches. The host must stay `127.0.0.1`. A base checkpoint leaves Live-facing status **Down**. Live stays **Down** until that exact revision, weight digest, and policy version are qualified with LIVE_DECISION evidence. Laya is never **Ready** by default. ### Laya on place Desk place surfaces go through Laya admission. Live-facing **Ready** and **Degraded** allow a Live place attempt. Practice **Ready** and **Degraded** allow a Practice place attempt, including while Live itself is still unqualified. The chip then shows the Practice state, with tooltip **Not qualified for Live**. On Live, operator place and automate place run Mode guard → Laya.admit → SafetySystem → gate_order → BrokerRouter. Laya does not place the order and does not replace those layers. A refusal or a quantity clamp stops before SafetySystem. Practice place is admitted before the sandbox and does not enter SafetySystem. A body with `"variety": "gtt"`, in any case or separator spelling, is HTTP 422 `gtt_unsupported` before Laya, SafetySystem, and any broker call, on place, routed place, exit-all, and a bracket. The message is `Not placed. GTT orders aren't supported right now.` On the server, example data is HTTP 403 `mode_blocked` before admission: `Orders are not available for Example. Switch to Practice or Live to trade.` The Order Pad on example data records a sample fill instead (`Example order placed`, id starting `SAMPLE-`). Chat is not an admission source. The model can deny or clamp. It cannot raise a quantity or overturn a hard-rule refusal. **Reason.** On the Order Pad the note is collapsed under Quantity. **Add a reason (optional)** opens a single line. The placeholder is **Optional note for this order**. Once open, the field's accessible name is **Add a reason (optional)**. It is not a required step, and an empty field does not block Place. Quick Trade, Positions square-off, Order Ladder, Scalper, and Option Chain use the same accessible name, **Add a reason (optional)**. A place with no note still gets Laya's policy decision. Practice clamps. Live denies. It is not a hard reject. The server reason for that Practice clamp is **Laya is uncertain. Quantity stays inside the tighter limit.** The server reason for that Live denial is **Laya is uncertain. Live stays closed.** When the requested quantity is greater than the allowed one, the Order Pad clamp notice shows the clamp sentence below, not the Practice reason line. **Deny.** Order Pad and Quick Trade show **Laya denied**, then the server reason. The denial is one alert (`role="alert"`), the only live region. The reason line is named **Laya decision** and is not its own status. When the server sent a quantity ceiling, the next line is **Max quantity N.** A Down refusal does not show that line. The reason is **Laya is Down. New orders are paused until it's Ready. You can still close positions.** Place controls stay off until Laya or the mode changes; you can then retry. Kill All stays reachable. A single decision with no proof, while the chip is **Ready**, is not that Down refusal. The notice reads **Not placed. Laya's decision couldn't be verified. Try again.** The code is `laya_unverified`. A denial is not a Chat outage: the LLM label stays **Not configured** or **Connected (suggest only)**, and the Chat strip stays Info. **Clamp.** A clamp is only when the requested quantity is greater than the allowed one. Nothing is placed until you click. Laya never auto-places the reduced quantity. Order Pad and Quick Trade show **Not placed. Laya allows up to N.** with **Place N** and **Cancel**. **Place N** sends that quantity through the same place path. On Order Pad, **Review Practice order** then shows that placed quantity. When the request is already at the allowed quantity, that place is admitted. Place 1 on **Not placed. Laya allows up to 1.** places. Practice still reaches the sandbox only after that admit. On Live, an admitted quantity continues to SafetySystem and gate_order. **Cancel** places nothing. **Degraded.** Live-facing Degraded leaves Live open. The desk says **Laya Degraded — tighter limits** on the Status menu and under those place controls. That line is not the Blocked strip, and it does not mute Live place. The chip shows Degraded when Live itself is Degraded. Practice Degraded applies the tighter ceiling to a Practice place. **Down.** Laya starts **Down**. The chip label follows the current mode. See [Start Laya](#start-laya). In Practice it shows the managed Laya state and does not read Down while a Practice place can be admitted. **Not qualified for Live** is the chip tooltip. Only Live-facing **Down** opens the Laya Blocked strip and mutes Live place. Practice is not muted by that strip. While that mute is up, the Live place control does not also show **Laya denied**. Kill All stays reachable. Broker may stay **Connected** or **Connected (read)**. A Practice place is refused when Practice itself is Down. The server reason, in every mode, is **Laya is Down. New orders are paused until it's Ready. You can still close positions.** There is no quantity-ceiling line. A Practice refusal never says Live. A Live place while Laya is Ready or Degraded, without a matching qualification record, says **Laya isn't qualified for Live yet. Practice orders are available.** Start the opt-in model before a Down engine can admit. A close the server classifies as reduce-only is still admitted when the books prove it. On Live, an unreadable broker order book refuses a reduce-only close, and that order takes the full check. While Laya is not Ready, Positions, the Practice Order Pad and the Live Order Pad show **Exit requested. Exits are allowed while Laya is Down. Check positions and orders for the outcome.** Exit-all shows **Exit-all requested. Check positions and orders for the outcome.** Cancel-all only cancels and stays reachable. Layer 5 cancels resting orders and then flattens; they are separate from cancel-all. Connected (read) is a broker status, not a Mode. **Chat.** Chat never shows **Admit** or **Approved by Laya**. Chat being offline does not close Live. Order Pad and Quick Trade are the surfaces that show the deny and clamp notices. Scalper, Positions, Order Ladder, and Option Chain may still show a place error as a toast. An automate clamp is a dispatcher error, not a desk confirm. Modify, cancel, smart, multi, and other non-place write verbs are not on this admission. `POST /api/v1/orders/forever` does not place. A valid body is HTTP 501 `Orders are placed through /api/v1/orders/place.` No submit route reaches a broker forever or super-order endpoint. A Live bracket with exactly one stop-loss or one target is `POST /api/v1/orders/bracket`: each leg is admitted, then placed through SafetySystem. Practice is HTTP 403 `practice_unsupported`. A broker-held variety, a stop-loss and a target together, and a trailing stop are refused before that admission. Order Pad keeps GTT visible and disabled, with the tooltip `GTT orders aren't supported right now.` **Feed freshness.** Feed provenance lives on the ticker chip, not in the Mode line. Example data reads **Example**. Practice and Live may read **Live**, **Delayed**, muted **Stale** or **Unknown** plus age when known, or `No live feed (Practice)`. Silent-stale is a fail. This is provenance, not a Mode — Live mode does not imply a Live feed. **Compact / Comfortable.** New installs default to Comfortable (full labels). Compact on a desk Trade viewport (~1280 and wider) keeps chart, order pad, and positions primary; the ticker strip, full tool ribbon, and watchlist / indices / advanced tools start collapsed behind **Watchlist & tools** / **Desk tools**. **Quick Settings** stays on the TopBar so density and theme remain reachable. Selecting Compact again re-collapses that disclosure. Phone layouts are unchanged. **Desk chrome.** The desk uses one TopBar and one scrolling ticker strip. TopBar keeps Mode, session/status, and overflow — it is not a second quote rail, so dual index slots in TopBar are gone. **Tools → Quick Settings** opens density, theme, and similar controls without leaving the desk. **Tools → Settings** opens the full `/settings` route for deep pages (Monitoring, brokers, auth). There is no separate Settings gear. When Compact Trade collapses the tool ribbon, Quick Settings stays on the TopBar; expanding desk tools puts it back in the Tools menu. A desk that offers only full Settings, with Quick Settings removed, fails this bar. The flex shell is TopBar, then the operator status strip when one is showing, then the Mode honesty line, then the ticker strip, then the route body. Trade uses that shell first; the same shell then rolls to Invest, Automate, Learn, and Ditto. This is not a silent widen of Compact-only-on-Trade . Mode and status stay reachable (desk-first; skinny-browser defensive collapse is fine). **Ticker venue badges.** Pinned badges match the venues that feed the marquee: 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 feed chip may read **Example**; it must not hide venue honesty. ### Walkthrough 1. Open Terminal (http://127.0.0.1:5100/terminal on the installed web app; http://localhost:5173/terminal on the Vite dev server). A bookmark of `/trade` opens the same screen. 2. A fresh desk is already in **Practice**. If the chip says **Example**, the session is **Guest**. Choosing **Practice** opens Setup and does not call `POST /v1/auth/mode`. A Live session downgrades with that route and does not ask for a PIN on the downgrade. To look at sample data instead, use **Try with example data** from Welcome or sign-in. 3. Open **+ Widget** (the dialog title is **Add Widget**) and choose **Order Pad**, or pick a preset that contains it. If an Order Pad is already open, choosing it again focuses that pad. It does not add a second one. 4. Type `NIFTY` into the symbol field; FlintTrade autocompletes the current front-month future. Select it. 5. Set Quantity to 1 lot. After you select the future, Order Pad auto-fills Quantity from that instrument's current lot size — do not type a remembered number. Learn Glossary follows the loaded instrument master rather than fixed example figures. A quantity that is not a whole number of lots is refused once, inline: `Quantity must be a multiple of the lot size (N).` `N` is that contract's lot. If the lot is not verified, the pad shows one line: `Not placed. The lot size for isn't verified.` `` is the contract's display name. 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 limit is the broker freeze field when the order carries one, otherwise the exchange figure. Choose **MARKET**. Side = **BUY**. 6. Click **Practice Buy** and confirm the review (**Confirm simulation**). The sandbox order appears in the **Positions** widget immediately; the **Orders** widget shows it as filled (simulated). **Example Buy** on example data opens the example review (`Example only. Nothing is sent to a broker and no order is placed.`). **Continue** records a sample fill on this pad: `Example order placed`, with an id starting `SAMPLE-`. That fill stays on the pad and does not call the order API. 7. Close the position from the Positions widget. Practice square-off posts an opposite order to `POST /api/v1/orders/place`. Confirm your simulated P&L is recorded in the **P&L Monitor** widget. Settings → Practice changes virtual capital and square-off times. It does not place an order. A Practice place is admitted before the sandbox. When Practice itself is Down, a new place is refused with **Laya is Down. New orders are paused until it's Ready. You can still close positions.** and nothing is filled. A close that only reduces an open position is still admitted, and the Practice sandbox can fill it. While Laya is not Ready, Positions and the Practice Order Pad show **Exit requested. Exits are allowed while Laya is Down. Check positions and orders for the outcome.** Exit-all shows **Exit-all requested. Check positions and orders for the outcome.** A second exit on that contract, while one of yours is still unfilled, is refused with `Not placed. An exit for SYMBOL is already pending. Wait for it to fill, or cancel it and try again.` and the row shows **Exit pending**. The label is the symbol, or **this contract** when the symbol is empty. The desk chip follows the current mode. When Practice can admit, the Practice chip shows **Ready** or **Degraded** from the managed Laya process and does not read **Down**. **Not qualified for Live** is the tooltip, not a Down label. Until a stop or a start is confirmed, the chip says **Checking** and the popover says **Checking Laya…**. When admission allows the quantity, the path is front-end → JWT guard → mode guard → Laya.admit → FlintTrade sandbox → simulated fill → REST refresh of Positions and Orders. No real money moved. A refusal or a quantity clamp stops before the sandbox. Example Buy never enters that path. Restoring a Practice backup marks those fills **Restored** (tooltip **Restored from backup. Not sent to a broker or checked by Laya.**). Performance shows **Excludes N restored fills** when N is at least 1, and hides that line when N is 0. **Estimated charges.** Every Practice fill runs through the same Indian statutory calculator the backtest uses: STT, the exchange transaction charge, the SEBI fee, stamp duty, and GST. The shared table records the exchange and the date each rate took effect. Brokerage is not in that table. Practice has no broker contract rate, so a fill never says Brokerage. The fill shows one line, `Charges ₹X (estimated)`, on its own line under the price. It expands to STT, the exchange transaction named from that fill (`NSE transaction` or `BSE transaction`), the SEBI fee, stamp duty, and GST. The `Simulated at last close` tag stays on its own line. Positions and Performance show net P&L. Gross P&L and the charges total sit in the tooltip only. Home and Invest subtract those estimated charges from net worth. In Practice the source line reads `Practice account, after estimated charges`. Net worth is ledger cash, including blocked margin, plus holdings at market value, plus each open position's contribution (options at signed market value, futures at unrealised P&L), minus those charges. Opening a future does not add its notional. ![Trade workspace](screenshots/04-trade.png) *Terminal, with FlexLayout tabs, order pad, positions, and chart.* On example data, Terminal Positions → Heat, **Group by Exchange** and **Group by Sector** draw a labelled band per group (name chip plus exposure when there is room). Flat stays leaf-only. Positions with no exchange metadata show `No exchange groups in these positions` instead of an undifferentiated treemap. On Terminal 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 shows the effective window (for example `Review range · 05 Sep–11 Sep 2026` or `YTD · 01 Jan–11 Sep 2026`). Opening Performance stays on the Review range rather than jumping 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. With example data, Terminal Analysis layout, **OI Chart** shares the Option Chain expiry list for that symbol/exchange. When the list is non-empty, the expiry control is shown and charts/statistics cover only the selected expiry (the chain’s selected expiry when both widgets are open; otherwise the nearest listed). Example-data expiries stay listed. The Mode honesty line is the disclosure — there is no second Example chip on the chain. 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. The widget never pairs “No expiries” with generic or sample bars. With example data, Terminal Watchlist checked LTP and % change columns paint their headers and cells. Those values use the same sample quotes as the ticker tape. A missing quote shows `—` after a brief `…`, never a silent blank. Unchecking a column hides it. Selecting a symbol in Terminal Watchlist retargets Chart, Option Chain, and Scalper to that symbol — no retype. Retarget on example data is allowed. An empty watchlist never silently retargets . With example data, Terminal → Scalper, **Buy CE**, **Sell**, and **1-CLICK** stay disarmed — the same honesty class as Settings → Telegram **Test Send**. They never open Confirm Order and never place. Helper: `Orders are blocked for Example. Switch to Practice or Live with a broker connected to trade.` **1-CLICK** stays OFF and disabled; its title is `One-click is unavailable for Example`. Sample quote preview is allowed; there is no Confirm BUY / Confirm SELL chrome. Practice and Live open Confirm only when the Mode allows it and a gateway is configured. The backend refuses an example-data order if the UI slips. ### Learn → Practice Trading The tab explains: “Practice is built in. Open the Trade desk and place a simulated order, no broker needed.” Choose **Practice** from the Mode menu, then use **Open Trade desk**. That link opens `/trade`, which opens Terminal, and preserves the selected Mode; it does not silently switch to Practice or Live. Use the Order Pad to place a simulated order, then review simulated orders, positions and P&L. Practice uses virtual funds, and simulated fills do not guarantee the same results in Live trading. Broker sandbox setup is not a prerequisite. Under `/learn` → Glossary → Lot Size, the index-lot line follows the loaded instrument master, with contract expiry months where available. When a newer master loads, the line updates with it. The **Verify on NSE** link remains available. Check the selected instrument's current lot size before placing an order; dated example figures are not a substitute for the current master. ### Learn → Resource Hub (local documents) With example data, `/learn` → **Resource Hub** opens project docs from the local FlintTrade backend (`USER_GUIDE.md`, `ORDER_SAFETY.md`, and the other listed cards). First open shows `Loading document…` while that local load settles — it never flashes a red backend error on a cold-start race. A timeout or race is a muted soft fail: `Document isn’t ready yet.` plus a primary **Retry** (one automatic retry is allowed). The red `Couldn’t load this document from the local backend.` line plus **Retry** is reserved for a hard fail after retry is exhausted. Order Safety Notes already loading in the same session does not treat User Guide as permanently broken. --- ## 4. Live-mode safeguard verification Live mode can send real orders through a configured broker path. This guide does not recommend or instruct a live order; use this section to verify the software safeguards, prompts, and recovery controls in a local setup. ### Pre-flight checklist - [ ] Your FlintTrade JWT is fresh — it expires daily at 8 AM IST. - [ ] The authenticator is enrolled. After **Later** on the **Two-factor authentication** card, the Live menu stays locked and **Enrol authenticator in Settings** links to Settings → Security (`/settings#security`). The Live switch does not ask for an authenticator code. Example and Practice stay password-only until enrolment. First-run Setup does not unlock Live. - [ ] An **exactly 6-digit** PIN is set under Settings → Security (`/settings#security`). With no PIN, Live stays locked and shows `Create a PIN in Settings`, linking to that same page. Once Live is eligible, the switch asks for this PIN. Quick Unlock uses the same PIN to reopen the current Mode. - [ ] The 5-layer safety system is active (see [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md#safety-layers)). - [ ] In Live, the Laya chip is **Ready** or **Degraded** if you intend a Live place attempt. The chip follows the current mode and does not invent Ready. In Practice it shows the managed Laya state. **Not qualified for Live** is the tooltip when Live has no matching qualification record. **Down** shows **Laya is Down. New orders are paused until it's Ready. You can still close positions.** and mutes a new Live place. Close and Square off stay available. A base checkpoint leaves Live unqualified. **Degraded** keeps Live open and shows **Laya Degraded — tighter limits**. A Practice place is refused when Practice itself is Down. After Start Laya, until the ping confirms the new state, the chip says **Checking** and the popover says **Checking Laya…**. See [Start Laya](#start-laya). - [ ] Daily P&L pause and hard-stop percentages are configured in Settings → Risk. - [ ] You have read the risk and user-responsibility notes in [disclaimer.md](../disclaimer.md). Operator and automate Live place run Mode guard → Laya.admit → SafetySystem → gate_order → BrokerRouter. Laya does not place the order. A refusal or a quantity clamp stops before SafetySystem. See [Start Laya](#start-laya) and [Laya on place](#laya-on-place). ### Walkthrough 1. Open the Mode menu and choose **Live** when it is enabled. **Switch to Live Trading?** warns that real orders will be placed and asks for your **exactly 6-digit PIN** (`Enter your PIN to confirm`). It does not ask for an authenticator code. After **Later** on the **Two-factor authentication** card, Live stays locked and **Enrol authenticator in Settings** links to Settings → Security. With no PIN, Live stays locked on `Create a PIN in Settings`. `POST /v1/auth/live` refuses 403 `totp_required` until the authenticator is enabled. Quick Unlock reopens the same Mode the session already had, with the correct PIN, and never changes the Mode. Set the PIN under Settings → Security (`/settings#security`) first if you have not already — see [Idle lock and Quick Unlock](#idle-lock-and-quick-unlock). 2. Cancel the modal unless you are deliberately performing your own broker-side test outside this guide. 3. Confirm the UI clearly shows Live mode, the active account, and the configured safety thresholds before any order-capable action is available. 4. Return to **Practice** mode and repeat the order-path walkthrough in the sandbox before continuing development work. If anything looks wrong during live-capable testing, hit the **Kill Switch** on Terminal (Live mode only). A bookmark of `/trade` opens the same workspace. Activate and reset also live under Settings → Risk & Safety. It cancels open orders and then flattens positions through the emergency broker path. It does not use a separate close-position route. **Cancel all** only cancels open orders. The kill switch fires only when you explicitly activate it from the UI, API, or configured Telegram command. Layer 4 daily-loss thresholds block subsequent new orders but do not cancel orders or flatten positions. --- ## 5. Workspace tour FlintTrade's workspace is a [FlexLayout](https://github.com/caplin/FlexLayout) canvas. Every widget is a panel you can drag, tab, stack, float, or pop out into its own window. Layouts persist in `workspace.json` inside your platform-specific workspace directory and sync across sessions: | Platform | Workspace directory | |---|---| | Linux | `~/.flinttrade/` | | macOS | `~/Library/Application Support/flinttrade/` | | Windows | `%APPDATA%\flinttrade\` | | Override | `FLINTTRADE_WORKSPACE_DIR`, then `FLINTTRADE_HOME` | See [Settings reference](#11-settings-reference) for what else lives there. ### Finding your way around - **App bar.** Terminal, Testing, Portfolio, Automation, Reports, Learn, Brokers, Settings, and System. The bar is labelled Apps. Below 768px it collapses behind the menu button at the top left. - **Top bar.** Search (symbols, pages and commands; also Ctrl+K or ⌘K), the Mode menu, one market chip (NSE session and IST clock), the **Status** menu (broker, the decision model labelled Laya, and LLM), **Ask AI**, **Tools** (Quick Settings and Settings), full screen, notifications and your profile. The account switcher appears once a broker account is connected. Feed provenance is the chip at the start of the ticker. - **Terminal toolbar.** The active workspace, **+ Widget** (opens **Add Widget**), **Layouts** and, on a Compact desk, **Watchlist & tools**. - **Tips** appear under the page header and can be dismissed; Settings can bring them back. - **Shortcuts.** Alt+H is labelled **Go to Home** and Alt+T is labelled **Go to Trade**. Both open Terminal. ### The main routes The app bar is the map. An older address opens the matching app and keeps the query string, the hash, and any navigation state. | Route | Purpose | |---|---| | `/welcome` | First-time cinematic introduction; **Skip intro** ends it early. After the first visit it is also the daily login screen; Enter signs in (password only until an authenticator is enrolled; then password + TOTP). An idle lock returns here. The heading is `Practice desk locked`, `Live desk locked`, or `Locked`. **Quick Unlock** is the small label above the PIN field, and the screen reopens the existing Mode — see [Idle lock and Quick Unlock](#idle-lock-and-quick-unlock). Password sign-in also offers **Forgot your password?** — an email OTP reset that sends mail only when SMTP or SES is configured (see [email setup](setup/email.md)). Welcome and sign-in also offer **Try with example data** so Example stays reachable if setup is unfinished. `/login` redirects here and opens sign-in. A signed-in visit leaves Welcome for `/trade`, which opens Terminal. | | `/explore` | Depends on the build and the session. The public site permanently redirects `/explore` (HTTP 308) to the Demo at `/demo-app/welcome`. The public demo bundle replaces `/explore` with Terminal (`/terminal`). An installed build replaces `/explore` with `/welcome`. If that Welcome screen already has a signed-in session, it continues to `/trade`, and the shell then opens Terminal. A signed-out installed visit stays on `/welcome`. **Try with example data** opens `/home`, which the shell replaces with Terminal. | | `/terminal` | Markets, the account overview, and manual trading. FlexLayout canvas, widgets, and presets. Alt+H and Alt+T both finish here. | | `/testing` | Strategy research, Practice forward tests, and local datasets. A bookmark of `/lab` opens this app. | | `/portfolio` | Investments, holdings, allocation, and goals. Sections are Overview, Holdings, Analyse, Discover, and Tax. A leaf hash opens that view inside its section; a section hash opens the section's first view; an unknown hash opens Overview → Dashboard. A bookmark of `/invest`, including `/invest#networth`, opens Portfolio and keeps the hash. | | `/learn` | Learning workspace — courses, glossary, examples, and built-in Practice Trading. **Open Trade desk** links to `/trade`, which opens Terminal. Choose Practice in the Mode menu before placing a simulated order. | | `/automation` | Schedules, monitors, webhooks, logs, and the AI Centre panel. The on-screen title of that panel is **AI Centre**. The catalogue calls the surface an AI workspace. A bookmark of `/automate` opens run control. A bookmark of `/ai` opens Automation with the AI Centre panel. Kill-switch activate and reset live under Settings → Risk & Safety. | | `/reports` | Journal, performance, notes, and coaching. | | `/brokers` | Native broker account metadata and connection status. A bookmark of `/ditto` opens Brokers. Mirroring and aggregate broker risk remain unavailable until native cutover. | | `/settings` | Security, preferences, and integrations (`workspace.json` editor with a form). | | `/system` | Health, network, resources, and operational evidence. A bookmark of `/admin` opens the system console. `/admin/observability` opens operational observability on the same app. The system console needs Developer features turned on in Settings. | First-run Setup finishes on the Practice desk. That desk is **Step 2 or 3**. With no operator yet, Setup starts at Create operator. When an operator already exists and Setup is unfinished, `/setup` resumes at **Step 2 or 3**. The step total is fixed before step 1, from whether this machine's vault is already secured. When the backend has already secured the vault, Setup skips the vault step. The titles stay **Step 1 of 2 - Create operator**, then **Step 2 of 2 - Practice desk**. That path never shows "of 3". An unfinished operator on that path resumes on **Step 2 of 2 - Practice desk**. On that Practice step only, **Your vault is set up and secured on this machine.** appears above **Open Practice desk**. The line under the title on that last step is **1 of 2 completed - last step**. When the vault is not yet secured, Setup keeps three steps: **Step 1 of 3 - Create operator**, **Step 2 of 3 - Vault**, and **Step 3 of 3 - Practice desk**. An unfinished operator resumes on **Step 2 of 3 - Vault** until the vault opens, then on **Step 3 of 3 - Practice desk**. The vault step asks for a master password and **Open vault**. The secured-vault line above **Open Practice desk** stays on the two-step path only. The last-step line is **2 of 3 completed - last step**. Before the create-operator form is shown, and again if a later status read fails, a failed setup-status check stays on that failure. **Retry** is the button. The fresh-install form does not open. - HTTP 429: **FlintTrade is busy**. **FlintTrade is busy right now. Wait a moment, then retry.** - Any other HTTP error, or a response that cannot be read or is incomplete: **Can't check setup status**. **FlintTrade answered, but setup status couldn't be read. Retry in a moment.** - Only when nothing answered: **FlintTrade backend unavailable**, with **Retry**. On the screen before Setup fields mount, the detail is **Start or restart the local FlintTrade backend, then retry. Setup has not advanced and no account, broker, or credential details were submitted from this screen.** **Return to welcome** sits beside **Retry**. If that same network failure is the later status read, the detail is **The FlintTrade backend did not answer. Start or restart the local FlintTrade backend, then retry.** and the only button is **Retry**. A second create while an operator already exists, including two creates that overlap, is refused. The account service raises `Account already set up`. `POST /v1/auth/setup` answers HTTP 409 with code `operator_exists` and message `Request conflicts with the current state`. Setup shows **This machine already has an operator. Sign in to finish setup.** A conflict that is not `operator_exists` still shows `Request conflicts with the current state`. Reloading `/setup` mid-flow resumes the unfinished setup in this browser and keeps the same step title. A run whose vault was not secured at the start still shows **Step 3 of 3 - Practice desk** and **2 of 3 completed - last step** after the vault opens and after a reload. That title does not become "of 2". The same tab restores the setup session and continues the current step. A fresh browser, or a reload on the vault step that needs a setup session, shows **Continue setup** and **This machine already has an operator. Sign in to finish setup.** Enter the password and choose **Continue setup** to carry on. That screen does not open the create-operator form. On a vault-step reload the step title stays put (for example **Step 2 of 3 - Vault**). **Start over (deletes this unfinished operator)** asks once: **Enter your password to delete this unfinished operator.** Confirm with the red **Delete and start over** button, or choose **Cancel**. That deletes the unfinished operator and restarts at step 1. That is the start-over control on the vault step. A workspace data wipe is not required. After Setup completes, opening `/setup` does not restart step 1. A signed-in operator is sent to `/trade`, which opens Terminal. A signed-out operator sees **Setup is complete. Sign in to open the desk.** **Sign in** is the primary button and opens `/welcome`. **Open Settings** is the secondary button. An operator database that already existed before setup completion was recorded is marked setup-complete the next time FlintTrade opens it. That mark covers only the operator rows already in the database. An operator created after that change starts unfinished and still walks through Setup. For an operator marked complete, `/setup` does not restart step 1. A signed-in operator is sent to `/trade`, which opens Terminal. A signed-out operator sees **Setup is complete. Sign in to open the desk.** **Open Practice desk** affirms Practice and opens `/trade`, which opens Terminal. After the affirm, the desk shows a one-line strip. The step total fixed above does not change. With nothing skipped the strip reads `Optional setup · N of 4 done`. N counts finished cards only. Skipping a card does not increase N. When at least one card is skipped, the strip adds the skipped count: `Optional setup · N of 4 done · M skipped`. When M is 0, that `· M skipped` clause is left off. **Show** expands the cards in place. **Hide** collapses them. **Dismiss** on the desk removes the strip there and leaves the same reminder at the top of Settings. The Settings reminder has **Show** and **Hide**. It has no **Dismiss**. The four cards use these titles, with no " — later" suffix: **Two-factor authentication**, **Broker connect**, **LLM**, and **Trading defaults**. A card that is still open offers **Set up** and **Later**, except **Broker connect**, which offers **Continue without a broker** and **Set up** only while that card is neither done nor skipped. A skipped card shows a **Skipped** tag and only **Set up** (no **Later**, and no **Continue without a broker**). A finished card shows a **Done** tag and only **Set up** (no **Later**, and no **Continue without a broker**). **Later** on the card row marks that card **Skipped** and updates the strip. **Later** inside an open authenticator, **LLM**, or trading-defaults panel only closes the panel. It does not mark the card **Skipped** or **Done**, and the strip does not change. The authenticator panel's actions are **Enrol** and **Later**. Card-row **Later** buttons, and **Later** in the open **LLM** and **Trading defaults** panels, are announced as `Later {title}`. `/home` opens Terminal. A signed-in operator who opens it (address bar, refresh, or same-tab bookmark) sees Terminal, the same screen as Alt+H. That visit is never the password Welcome Back gate. That gate stays on `/welcome` for unauthenticated visitors only. ### Home The Home greeting card stays on the dashboard. It uses the saved display name. When that name is missing, or it is the placeholder `Trader`, the greeting uses the username, unless the username is missing or is also `Trader`. Until a name is known, the line is plain `Good morning`, `Good afternoon`, or `Good evening` from the Asia/Kolkata hour. The card never greets the operator as `Trader`. Editing the display name replaces the greeting. There is no greeting toast. Home and Invest share one net-worth figure: ledger cash, plus the market value of holdings, plus open positions. On Invest the label is `Net Worth (Cash + Holdings + Positions)`. Ledger cash includes blocked margin. It is not the available margin, so opening an F&O position does not reduce Net Worth by its margin. A Practice round trip at an unchanged price leaves Net Worth at the starting cash, for example ₹10,00,000. Options add signed market value (last traded price × quantity when that price is positive, otherwise the entry price × quantity). A long is positive and a short is negative, because the premium has already gone through cash. Equity positions that are not already holdings do the same. A flat position adds nothing. Futures add unrealised P&L (last traded price minus a base, times signed quantity): - Dhan's ledger already includes earlier days' mark-to-market. The base is the mark-to-market average (`buyAvg` on a long, `sellAvg` on a short). When that average is absent, the base is `costPrice`, and the figure is approximate. - Kotak Neo's ledger also includes earlier days' mark-to-market, and an open future has no settlement price. The base is the open-leg average, and the figure is approximate. - Practice does not put futures mark-to-market into the ledger and has no settlement price. The base is the entry price. Practice never marks the figure approximate. The positions note, while the figure is exact, reads `Options at market value, futures at unrealised P&L.` When an open future uses an estimated mark, the amount shows `≈` before the rupees, in the same size and colour. Dhan sets that mark when the base is `costPrice`. Kotak Neo sets it on an open future. Practice never sets it. It clears when that position goes flat or its mark is the average. One future names its symbol in the tooltip; two or more say the count, for example `2 futures positions`. Dhan, when the average price was missing, uses `Approximate. Your broker didn't send an average price for NIFTY-JUN2026-FUT, so profit or loss from earlier days may be counted twice.` for one future, and `Approximate. Your broker didn't send an average price for 2 futures positions, so profit or loss from earlier days may be counted twice.` when more than one fall back, with the count in place of `2`. Kotak Neo uses `Approximate. The price for NIFTY25JUNFUT is estimated from the open position's average, so profit or loss from earlier days may be counted twice.` for one future, and `Approximate. The price for 2 futures positions is estimated from the open position's average, so profit or loss from earlier days may be counted twice.` when more than one are estimated, with the count in place of `2`. When both kinds are open, the tooltip joins them: `Approximate. Your broker didn't send an average price for NIFTY-JUN2026-FUT, and the price for NIFTY25JUNFUT is estimated from the open position's average, so profit or loss from earlier days may be counted twice.` Each subject is that future's symbol, or `N futures positions` when several share that kind. While the figure is approximate, this tooltip replaces the positions note on the same labels. The screen-reader name of the amount is `Net Worth, approximately …`, using the same rupee figure and no second `≈`. Allocation percentages are not marked. On Home, `≈` and that tooltip sit on the Net Worth amount. The `Net Worth` label carries the same tooltip and does not itself show `≈`. On Invest Dashboard, the label `Net Worth (Cash + Holdings + Positions)` carries that tooltip, and `≈` with the same tooltip sit on the amount under it. Available Funds on that dashboard uses the same amount format, so it also shows `≈`, and it has no tooltip. On the Net Worth view, the label `Known Total (Cash + Holdings + Positions)` carries that tooltip, and `≈` with the same tooltip sit on the amount under it and on the `Open Positions` value. That line is shown only when the positions contribution is above zero. The donut centre (`tracked`) shows `≈` on the same total and has no tooltip. Cash on that view is not marked. Home allocation shows a labelled Example split (Equity, MF, Gold, F&O) until funds, holdings, and positions have all loaded successfully. If any of those books is still loading or has failed, the split stays on that Example mix. The sample-data Home keeps that mix. After all three succeed on an account, the bar is the account split: Cash, Positions, and Equity. The Example label sits on that split only while it is the example mix. While holdings or funds are still loading, Invest Dashboard shows `Loading portfolio data...` and the Net Worth view shows `—`. After those books settle, both leave the total as `—` until the position book has loaded. A position book that is still loading or has failed does not publish the total. The sample book does not wait on a position book. On an account, Home shows `—` while the position book is pending or has failed, the same check Invest uses. It does not draw a cash-only figure. The amount appears once positions have loaded. A negative Net Worth is drawn as the number, for example `-₹50,000`, or `≈ -₹50,000` when the mark is approximate, the same as on Invest. On Home, each open position's P&L percent is derived from cost: the absolute average price times quantity. Dhan and Neo do not send a percent. When cost is missing or not above zero, or the profit figure is missing, the percent is `—`. ### Invest Invest sections are Overview, Holdings, Analyse, Discover, and Tax. | Section | Views | |---|---| | Overview | Dashboard, Net Worth, Goals | | Holdings | Holdings, Mutual Funds, SIPs, Baskets | | Analyse | Sector, Sector Rotation, Overlap, Benchmark, Shareholding, Risk-Return, Correlation | | Discover | ETF Screener, MF Optimizer, Social, ETFs, Stocks, IPO | | Tax | Tax | A leaf hash opens that view inside its section. `#sip` opens Holdings → SIPs. `#networth` opens Overview → Net Worth. `#mutual-funds` opens Holdings → Mutual Funds. `#mf-optimizer` opens Discover → MF Optimizer. `#basket` opens Holdings → Baskets. `#sector-rotation` opens Analyse → Sector Rotation. A section hash opens that section's first view: `#overview` opens Dashboard, `#analyse` opens Sector, `#discover` opens ETF Screener, and `#tax` opens Tax. `#holdings` is the Holdings view, so it opens Holdings → Holdings. An unknown hash opens Overview → Dashboard. The selected view stays available from that hash when the skill level would otherwise hide it. **Overlap.** With zero or one fund or basket, Overlap shows `No holdings to compare yet. Overlap appears once you hold two or more funds or baskets.` The view opens at two or more. The sample book appears only in the web demo, labelled `Demo (example data)`, or with an Example label before any account snapshot exists. Practice with no holdings always shows the empty state. **Benchmark.** Hard-coded index returns carry the Example chip in every mode, and each index name also shows an Example label. With real holdings, the series legend is `Your holdings (unrealised)`. That legend replaces `Your Portfolio (since first buy)`. Its tooltip reads `Gain or loss on the shares you hold now, compared with what you paid. Sold shares and dividends aren't included.` The comparison's accessible name is `Unrealised return on holdings`. That row carries no Example mark. When the holdings are example data, that row is `Your Portfolio` and keeps an Example label. With no holdings, the row is plain `Your Portfolio`, shows `—`, and `Add holdings to compare against benchmarks.` Benchmarks beaten, alpha, and the other outperformance figures are replaced by `Comparison needs real index data.` That note is shown whenever there are holdings, including on the sample book. The view also reads `Benchmark data is illustrative. Live index data requires a market data subscription.` and `Returns are absolute (not annualised) for periods under 1Y.` **Example chips.** Sector, Sector Rotation, Shareholding, ETF Screener, Social, ETFs, Risk-Return, and Correlation use the Example chip on sample figures. They do not use the Example label. Shareholding omits the chip when the read has failed. In Example, Social shows exactly one Example chip, and only after loading has finished. In Example, ETFs show one Example chip and `Example prices. Connect a broker for live quotes.` Practice and Live keep `live quotes via the native gateway. Refreshes every 30s.` In Example, Sector's header reads `Example sector split. Connect a broker to see yours.` and the footer reads `Example data. Not from your holdings.` That view has one Example chip. In Example, Baskets show one Example chip, including while quotes are loading and after they have loaded. Seeded cards do not add a second marker. Seeded example baskets disable **Edit** and **Delete**, with the title `Example basket — editing unavailable`. Baskets created in Practice or Live keep **Edit** and **Delete**. An empty Baskets view reads `No baskets yet`. **Holdings.** On example data, the Holdings view shows one Example chip when the book is sample data. The Investor Dashboard header badge reads `N holdings` and matches that table. On a sample book the badge has an Example label beside it. The Holdings toolbar reads `N stocks` for the same count. The banner `Example data. Connect a broker to see your own.` is not repeated on the view, and it never shows in Practice. A cold load waits until the holdings query has settled before the sample fallback, so a pending book is not covered by the sample count. Practice waits until that query has settled empty. A failed holdings read shows `Failed to load holdings` and `Refresh`, and does not show `0 holdings`, `No holdings`, or a sample table under that failure. An empty connected book shows `0 holdings` and `No holdings`. An account snapshot replaces the sample book, including a Practice snapshot with cash and an empty holdings list. **Dashboard and Net Worth figures.** Net Worth, Available Funds, Invested Value, and Day P&L on the Dashboard show their final formatted value on the first frame in every mode, with no count-up from zero, including `≈`, `-₹50,000`, and `—`. The sample Dashboard marks the inline sample XIRR with one Example chip (`XIRR` plus that chip). Portfolio Allocation on that sample dashboard omits `Equity + Cash from your connected broker. Debt / MF requires NAV data source.` and does not carry its own Example chip. A connected book keeps that sentence. There is no Portfolio XIRR card. With no holdings, the inline XIRR is omitted. On sample figures, Portfolio (`/portfolio#networth`, also reached by `/invest#networth`) 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 on example data, and those rows do not carry their own Example chip. A connected book keeps `Live equity and cash from your connected broker. Other asset classes require additional data sources.`, the label `Allocation (live assets only)`, and the note `Live from broker`. Those sentences follow the sample-figure flag, including a Practice book that has fallen back to sample holdings. The Example chip paints only on example data, so that Practice fallback shows the example sentences without the chip. Practice does not mark the XIRR figure as Example. The connect banner `Connect a broker in Settings → Brokers to see your real holdings, SIPs, and portfolio value here.` is hidden in Practice. **Mutual funds.** With example data, Portfolio (`/portfolio#mutual-funds`, also reached by `/invest#mutual-funds`) labels the static fixture `Example NAVs · as of 10-Sep-2026` on the view header and again on the disclaimer, and does not claim "Updated daily after market close." The as-of date is the fixture date and does not auto-update. Practice and Live keep `Search Indian mutual funds with live NAV data from AMFI. Updated daily after market close.` when the live feed is in use. **Cash.** Available Funds on Dashboard is the balance left after blocked margin. The Net Worth view's Cash line is the ledger, including that blocked margin, so opening a position does not shrink the total by the margin. Both show full rupees, in Indian grouping, with no paise. When the total is approximate, Available Funds shows `≈` as well. Cash on the Net Worth view does not. ### The widgets (71) Widgets are organised into three categories — Trading / Analysis / Utility — under `packages/apps/terminal/src/widgets/`. The lists below are generated from the widget registry (`packages/apps/terminal/src/layout/widgetFactory.tsx`) and are exhaustive: - **Trading** — Scalper, Positions, Fills, P&L Monitor, Orders, Holdings, Order Pad, Action Centre, Trade Copier, Smart Order, Portfolio Allocation, Quick Trade, Risk, Strategy Monitor, DOM / Ladder, Forever (GTT) Orders, Super Orders, and Conditional Triggers - **Analysis** — Chart, Market Overview, Option Chain, Historical Chain, OI Analytics, Straddle & Implied Move, Greeks, Dealer Gamma, Arbitrage Scanner, Pattern Detection, Tape & Microstructure, Vol Surface, IV Smile & Skew, Straddle P&L, Order Flow, Portfolio Optimiser, Condition Scanner, Pivot Points, Volatility Cone, VWAP Bands, Correlation Pairs, Multi- Timeframe, PCR Trend, Instrument Compare, Greeks Matrix, Gap Analysis, Correlation Matrix, Delivery Data, DOM Heatmap, Portfolio Pivot, and Seasonality - **Utility** — Watchlist, Index Strip, Calculator, News Feed, Ticker, AI Advisor, AI Backends, AI Team, Obsidian Vault, Price Alerts, Reconciliation, Funding Rates, Currency Converter, Earnings Calendar, Strategy Templates, Audit Trail, Economic Calendar, Expiry Countdown, Market Clock, Trade Ideas, Tick Speed, and Journal Entries Every widget is registered in `packages/apps/terminal/src/layout/widgetFactory.tsx`. **+ Widget** on the trade desk opens **Add Widget**. Choosing **Order Pad** when one is already open focuses that pad. It does not add a second Order Pad. A watchlist **Buy** or **Sell** retargets that same open pad. Market Clock uses the same CAS-aware cash timeline as the TopBar (Continuous → CAS → Matching → Post-close → Closed), not a flat 09:15–15:30 "open" window. When F&O still runs after cash continuous ends, the TopBar may show `F&O open · till 15:40` through 6 December 2026, and `F&O open · till 15:45` from 7 December 2026. Non-CAS cash still continuous to 15:30. Feed provenance lives on the ticker chip. Example data reads **Example**. Practice and Live may read **Live**, **Delayed**, muted **Stale** or **Unknown** plus age when known, or `No live feed (Practice)`. The Practice ticker shows `Last close prices · Connect a broker for live prices →`, linking to `/settings#brokers`, only when the market session is not unavailable and neither the WebSocket nor a fresh REST fallback is producing quotes. Unavailable timings read `Market unavailable` and are never labelled last close. Feed provenance is independent of Practice and Live, so silent-stale is a fail. Connected (read) is a broker status, not a feed label. With example data, Terminal Watchlist checked LTP and % change columns use the same sample quotes as the ticker tape. A missing quote shows `—` after a brief `…`, never a silent blank . Selecting a watchlist symbol retargets Chart, Option Chain, and Scalper to that symbol. An empty watchlist never silently retargets . With example data, Terminal Scalper (including the Scalper Zone preset), **Buy CE**, **Sell**, and **1-CLICK** stay disarmed. Helper: `Orders are blocked for Example. Switch to Practice or Live with a broker connected to trade.` There is no Confirm Order path from example data. With example data, Terminal Option Chain, the strip shows OI profile + PCR for the selected expiry/symbol. Example data does not invent live OI. An empty expiry is an honest empty, not zeros-as-data. News Feed loads headlines only through the FlintTrade backend (`GET /api/v1/news`). There is no browser-side RSS or CORS-proxy fallback. If the backend cannot serve articles, the widget reports that news is unavailable from the FlintTrade backend. Publisher profiles are MoneyControl, ET Markets, and LiveMint. ### The 17 workspace presets A preset is a pre-built layout you can apply instantly from the command palette (Ctrl + K → "preset"). Built-in presets include: - **Scalper Zone** — chart, order ladder, order pad, positions, scalper panel. - **Options Desk** — option chain, chart, Greeks, positions, straddle P&L. - **Market Watch** — multi-symbol watchlist, chart, price ticker, indices. - **Analysis** — chart with indicators, OI analytics, positions, news. - **Risk Monitor** — indices, risk, P&L monitor, positions, orders. - **Trading Desk** — indices, positions, risk and orders; the composition that replaced the old Dashboard widget. - **Three Panel** — CE, index and PE charts with synchronised time scales on the nearest-expiry ATM strikes. - **Investor View** — chart, watchlist, holdings, indices (SIPs, net worth and mutual funds live on the Invest page). - … plus nine more. Presets are declarative FlexLayout documents. You can save your own custom preset from Settings → Workspace. --- ## 6. Screener walkthrough The screener lives under the **Analysis** widget category and shares the workspace with everything else — no separate route. Headline tools: ### Option Chain Streaming option-chain widget rendered with [Glide Data Grid](https://github.com/glideapps/glide-data-grid) for 60+ FPS updates even on a 50-strike chain. The header also shows a Max Pain badge derived from the same chain. The Option Chain strip shows **OI profile + PCR** for the selected expiry and symbol. Example data does not invent live OI. The Mode honesty line is the disclosure — there is no second Example chip on the chain strip. An empty expiry is an honest empty — not zeros-as-data. ### OI Analytics One widget (`oichart`) with several views of the same chain read: grouped OI bars, a CE/PE butterfly **OI profile**, a strike heat grid, build-up/unwinding signals, and a **Max Pain** view (the strike at which option writers lose the least if expiry hit right now). There is no standalone Max Pain widget. OI Chart shares Option Chain expiries for the symbol/exchange. The expiry control appears when that list is non-empty; charts and statistics cover only the selected expiry (the chain’s selection when both widgets are open; otherwise the nearest listed). Example-data expiries stay listed. The Mode honesty line is the disclosure — expiries and the OI Chart are not badged Sample. Empty states are honest: `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 or generic bars. ### IV Smile & Skew Implied-volatility curve across strikes, with 25-delta skew and term-structure indicators. Useful for spotting unusual options activity. --- ## 7. Strategy Lab walkthrough Open Testing (`/testing`). A bookmark of `/lab` opens the same screen. Strategy research includes Backtest, Forward Test, Optimise, and Options Builder. ### Backtest 1. **Pick a template.** 94 ready-to-run template modules ship under `packages/services/backtest/src/flinttrade_backtest/strategies/` — ranging from simple EMA crossover to complex options-spreads strategies (`_indicators.py` and `_mixin.py` are shared helpers, not templates). 2. **Configure parameters.** Each template exposes a parameter form (built with `react-hook-form` + `zod`). 3. **Pick a date range.** Historical OHLCV data is sourced from your configured providers (OpenChart, yfinance, or a licensed feed). 4. **Run.** The backtest engine is FlintTrade's native event-driven simulator (pure Python by default). Install the optional VectorBT extra for vectorised runs, or opt in to the Rust `ticks` engine for tick-level precision. 5. **Review.** **Total Return (%)**, **Net trade P&L (₹)** (or **Trade log P&L** when net P&L is missing from the result), equity curve, Sharpe, Sortino, max drawdown, win rate, trade list, Monte Carlo confidence band. After a backtest run on example data in Testing, Review shows labelled dual metrics. **Total Return (%)** is initial capital → final equity (including a forced last-bar close), with subtitle `Initial capital → final equity` — not a sum of trades. **Net trade P&L (₹)** is the Trade Log net-P&L sum when every trade has net P&L. If net P&L is missing, the card is labelled **Trade log P&L** with subtitle `Gross — net P&L not in result` — never a gross sum called net. The trade table column is `Net P&L` when the basis is net, otherwise `P&L`. When the summed trade-log amount matches the equity-curve rupee 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`. The helper is omitted when the trade log or equity curve is empty — nothing to reconcile. Monthly P&L stays labelled `Trade-based · sums Trade Log P&L` so the chart matches the log on the same basis as the table. ### Forward Test Same as Backtest, but runs in Practice mode against live ticks. Use it to validate the software path before any Live-mode use. ### Optimise Walk-forward optimisation across a parameter grid. Outputs a heatmap of performance per parameter combination plus an out-of-sample evaluation. ### Options Builder Payoff needs a premium before it shows numbers. Open Testing → **Options Builder** → **Payoff**. Until every leg has a premium, Max Profit, Max Loss, Net Premium, and BEP(s) show `—`. Helper: `Enter premium to model payoff`. A blank premium is unknown, not zero-risk. With example data, the **Long Call** template seeds a **sample premium** from the sample-chain ATM CE LTP, labelled `Sample premium — edit to model`. Edit the field if you want a different cost. Other templates leave premium blank so Payoff stays on that helper instead of modelling ₹0. Typing an explicit ₹0 is allowed. Payoff then treats cost as free and warns `Premium is ₹0 — payoff treats cost as free`. Debit/credit and max loss share a 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) shown on that same position basis — not premium × lots × lot size on every card. For a long call, Max Loss equals Net Debit. 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. ![Lab](screenshots/06-lab.png) --- ## 8. Automate walkthrough Open Automation (`/automation`). A bookmark of `/automate` opens the same screen. An Automation place is admitted before the safety layers, the same as an operator place. A clamp comes back as a dispatcher error. It does not open a desk confirm, and it does not place the reduced quantity on its own. The hub has three sub-tools: ### Cron Time-based automations on the **Schedules** tab. Jobs use readable names; **Show system jobs** reveals maintenance jobs such as connection health checks, backups and data cleanup. This checkbox only filters the list and never pauses or resumes a job. Custom jobs remain visible by default. Examples: - Run pre-market screener at 9:00 AM IST every weekday. - Snapshot positions to a CSV at 3:30 PM IST. - Write a daily P&L summary to local storage at end-of-day. Cron jobs run inside the FlintTrade backend (`packages/services/automation`). With example data, Automation → Schedules shows one Example marker for the view — seeded jobs do not carry a second chip once Pause is gated. Seeded example jobs show a muted sample status, not a production-looking Active badge. **Pause** on those jobs is disabled, with title helper `Example schedule — control unavailable`. Practice and Live keep Pause/Resume for real jobs. ### Monitors **Live Strategy Monitors** lists running strategies and auto-refreshes about every 5 seconds. You can stop a running strategy from this view. When none are running, the empty state shows "No strategies running" and "Start a strategy from the Strategy Builder tool.", plus an **Open Strategy Builder** outline link that opens Testing. With example data, Settings → Telegram, **Test Send** is disabled (click and Enter do not send). The prefilled message stays visible 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.` There is no confirm-and-send path from example data. Practice and Live arm Test Send only when Telegram is configured; otherwise the helper is "Configure Telegram first". ### Execution Logs **Execution Logs** is the date-paginated history of automated actions. With example data, Automation → Execution Logs, a healthy sample session shows the muted empty state `No execution logs for Example. Switch to Practice or Live to see real run history.` It never shows `Failed to load logs. Backend may be offline.` while the app is live and example data is showing. In Practice or Live, a successful load with no rows for the selected date shows `No execution logs for this date.` — not an outage. The red `Failed to load logs. Backend may be offline.` line (or Retry) is reserved for a real request failure. While logs are loading, the view shows a spinner / `Loading logs…` and never flashes the outage copy. ![Automate](screenshots/07-automate.jpg) --- ## 9. AI Centre walkthrough Open Automation and the AI Centre panel (`/ai` opens that panel; the title stays **AI Centre**). Chat, Signals, Sentiment, and RAG are backed by `packages/services/ai`. Suggest is a local filter UI over an illustrative strategy list — not a live AI fetch. Suggest stays labelled illustrative. ### Chat Local AI analysis and debugging tools. The default local provider is FlintTrade's managed Ollama install on a backend-only loopback port. Settings -> AI requires separate confirmation before downloading the pinned runtime or any model; cloud and custom providers remain optional. Runtime update, rollback and uninstall are offered only while Ollama is stopped. Uninstall preserves models and accepted digests. The model inventory can delete one unselected model name or prune only unused digest-locked aliases created by FlintTrade; configured models are protected. If a timed-out mutation has an outcome FlintTrade cannot prove, Settings blocks later runtime changes and shows the exact operation and admission IDs. Explicit acknowledgement records that the unknown result was reviewed; it does not retry the action or label it successful. Chat is suggest-only. A connected LLM is labelled **Connected (suggest only)**. It does not place Live orders, and it is not Laya. Chat is not an admission source: it never shows **Admit** or **Approved by Laya**. The desk Laya chip is a separate status: **Ready**, **Degraded**, **Down**, **Still loading**, or **Checking**. The chip follows the current mode. In Practice it shows the managed Laya process and does not read Down while Practice orders are being admitted. During the first load the label is **Still loading**. **Not qualified for Live** is the tooltip. Laya starts **Down** and does not invent Ready. How to start it is [Start Laya](#start-laya). A base checkpoint leaves Live unqualified. Only Live-facing **Down** closes Live orders and mirror start. Practice is not muted by that strip. A Practice place is refused when Practice itself is Down. Kill All stays reachable. Chat being offline does not close Live. Automation has one **AI Centre** panel. **Chat with AI** and the Ctrl+K **Ask AI** tab open the same AI Centre conversation. TopBar **Ask AI** opens the contextual tutor using that same conversation history; its route and widget context are retained. The tutor does not float over app content when closed. The Settings tab is named **AI**, with the existing `/settings#llm` link. Chat itself needs a configured LLM via Settings → AI. The badge and composer align with Settings → AI / `#llm` hydration as well as advisor status (including example data, signed in as **Guest**, and Practice), not a leftover local setting. When Settings `#llm` is empty ("No LLM provider configured") or the stored provider is blank, Chat on example data and Practice shows **Not configured** / **LLM not configured** unless `advisor/status` reports an explicit env-backed provider (`LLM_PROVIDER`). **Connected (suggest only)** must not appear from an env-default advisor `configured` (empty provider → ollama). Returning to Chat after you save Settings → AI re-checks readiness (advisor and Settings hydration), so the **Not configured** gate should not stay stuck on an outdated result. When Settings → AI shows Managed Ollama **Not installed**, AI Centre Chat does not show a green **Connected (suggest only)** badge. The badge follows the real LLM status: **Not configured** / **Not installed**, with the primary **Open Settings → AI** CTA to `/settings#llm`. 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. When unconfigured or not installed, the warning badge is **Not configured** or **Not installed**, the empty state is **LLM not configured** (or **Not installed**), the primary CTA **Open Settings → AI** opens `/settings#llm`, and an outline **Retry** re-probes advisor status and Settings `#llm` hydration. Composer input and Send stay disabled; there is no send-then-no-reply path. If leftover transcript messages hide that empty state while Chat is still unconfigured, not installed, disconnected, or in error, the header still offers **Retry** (re-check advisor status and Settings hydration) and **Open Settings → AI**. A configured but broken probe shows **Error** or **Disconnected** with **Retry** — never a green **Connected (suggest only)**. Example data does not show a fake Connected sample advisor. Any later demo replies must be labelled **Sample replies**. Signals **Live** / **Polling** stay separate from Chat LLM readiness. AI Chat live-read context: when an LLM is configured, Chat may use Practice simulated fills and native live-read feeds for analysis. That is analysis context, not a guarantee of profitable alphas, and profitable alphas are not a release criterion. Chat does not place Live orders — Live place stays fail-closed. Chat never shows **Admit** or **Approved by Laya**. This does not lift the native broker HTTP freeze and does not claim every Chat turn already has live ticks. Chat never shows green **Connected** without a real LLM. When Chat is connected, the badge reads **Connected (suggest only)**. Suggest stays labelled illustrative and is not this live-read path. On example data, `/settings#llm`, an unconfigured session shows the empty state "No LLM provider configured" with **Retry** — not a broken load. That Settings empty-state wording stays distinct from Chat's **LLM not configured**; the two are aligned for readiness, so Chat also looks unconfigured when Settings looks empty. Configure a provider in Live or Practice on this machine; see [Settings reference](#11-settings-reference). ### Speaking with the AI assistant The AI Advisor microphone fills an editable conversation draft. Review the transcript and press Send; recognition never submits an order. Your browser may send audio to its speech recognition service. Typed conversation remains available when speech recognition is unsupported or permission is refused. An order request can produce a proposal for review. Approve uses the existing Practice order path and safety admission. Native Live execution remains unavailable until the native cutover; speech does not unlock it. ### Suggest **AI Strategy Suggestions** filters a local illustrative recommendation list by Market Mood chips (**Volatile**, **Trending**, **Sideways**) and your risk profile from persona and experience. Mood is a filter, not a draft and not a live AI fetch. Cards stay labelled illustrative. Suggest is not the Chat live-read path and is not a profitable-alphas product. Changing mood — a chip or **Next mood** — immediately replaces the recommendation cards and clears any previously focused strategy card, so chips, cards, and focus share one mood state. Selected mood chips use `aria-pressed`. When mood and risk match nothing, the empty state offers **Try another mood**, which advances the mood filter. **Deploy to Strategy Lab** opens `/lab?strategy=` for that card. ### Signals Rule-based and ML-derived software outputs. Includes an executor that runs multiple generators in parallel and aggregates diagnostics. These outputs are and are not financial advice. ### Sentiment The Sentiment Analysis view scores submitted text for a symbol through the configured LLM or rule-based fallback. Separate summary and ticker endpoints synchronously analyse the static RSS publisher profiles (MoneyControl, ET Markets, LiveMint). Production composition starts neither a background news scheduler nor a social-media source. ### RAG Retrieval-augmented question answering over local user-provided documents (for example notes, statements, or research PDFs). The runtime is off by default. The local sqlite3/NumPy vector store is built in; embedding libraries are loaded only when installed locally and RAG is enabled, so ordinary launches do not download models, embed documents, or carry optional AI dependencies. Set `FLINTTRADE_RAG_ENABLED=true` when you want the RAG runtime available, or `FLINTTRADE_RAG_AUTO_INDEX=true` when you intentionally want `docs/` indexed at startup. ![AI](screenshots/08-ai.jpg) --- ## 10. Accounts walkthrough ### Native accounts and copy trading Accounts lists the built-in native broker connections. Credentials belong to the native gateway vault, and account selection uses the exact broker and account identity. Native broker HTTP reads and writes remain frozen pending the cutover. Native copy trading has no active runtime. Mirroring, linked-account margin and risk execution are unavailable; their controls stay disabled in every mode. Account linking returns 501, and operations requiring the missing copy runtime return 503 after their authentication and mode checks. No external bridge service is started or contacted. --- ## 11. Settings reference FlintTrade has two layers of configuration: ### Layer 1: `workspace.json` (user preferences and integration settings) Lives in your platform-specific workspace directory: | Platform | Path | |---|---| | Linux | `~/.flinttrade/workspace.json` | | macOS | `~/Library/Application Support/flinttrade/workspace.json` | | Windows | `%APPDATA%\flinttrade\workspace.json` | | Override | `FLINTTRADE_WORKSPACE_DIR`, then `FLINTTRADE_HOME` (in that precedence order) | The Setup and Settings UI write `workspace.json`. If optional setup was dismissed from the Practice desk, Settings opens with that same strip (`Optional setup · N of 4 done`, and `· M skipped` only when M is at least 1). The Settings reminder has **Show** and **Hide**. It has no **Dismiss**. Key Settings panels: | Settings panel | Maps to | Configures | |---|---|---| | **Appearance** | `ui.theme` plus the theme / density stores | Theme (Graphite / Midnight / Ember), light / dark / system, UI density. | | **Data Paths** | `storage.fast`, `storage.archive` | SSD vs HDD paths for tick data vs archive. | | **AI** | `llm.provider`, `llm.host`, `llm.model` | Catalogue-driven LLM profiles generated into the terminal from `llm_provider_profiles.py`: managed Ollama, cloud providers including NVIDIA NIM (intentionally blank unpinned default model), a local OpenAI-compatible host, and custom endpoints. | | **Telegram** | `notifications.telegram_enabled`, `notifications.telegram_chat_id`, `notifications.telegram_bot_token_ref` | Bot enable and chat ID. The token is a hardened file under `/secrets/`; `workspace.json` holds only the `secret://` reference. Enabling the bot applies the saved config to the running Telegram alert / kill-switch bot. A test send lives in Settings → Telegram (**Test Send**); example data keeps that control disarmed. | | **Risk & Safety** | `safety.pnl_pause_pct`, `safety.pnl_kill_pct` | Daily P&L percentages for a reversible new-order pause and a latched new-order hard stop; neither activates Layer 5. `POST /api/v1/safety/config` accepts those same names as `pnl_pause_pct` / `pnl_kill_pct`. The Settings form's TypeScript fields are `daily_loss_pause_pct` / `daily_loss_kill_pct`; `updateSafetyConfig` remaps them to the wire fields before posting. | On `/settings#security`, **Quick-unlock PIN** is the 6-digit PIN that Quick Unlock and the Live switch both ask for. The PIN is optional at account setup. New and Confirm accept digits only (`maxLength` 6). **Set PIN** / **Change PIN** stays disabled until the account password is present, both fields are exactly six digits, and they match. Leaving (blur) a field with 1–5 digits shows `PIN must be exactly 6 digits`; leaving Confirm when both fields are filled and different shows `PINs do not match`. `POST /v1/auth/pin/set` rejects anything that is not `^[0-9]{6}$`. Setting or changing the PIN does not change Mode. ### Idle lock and Quick Unlock Welcome and the idle lock overlay both take their heading from the session JWT `mode` claim: - Practice: `Practice desk locked` - Live: `Live desk locked` - Example data (the `explore` claim), a retired claim, or any other value: `Locked` A Practice session whose broker status is Connected (read) keeps `Practice desk locked`. Connected (read) is a broker status, not a session Mode, so it does not choose the heading. A claim that is itself the words Connected (read) is not Practice or Live, and stays on the plain heading `Locked`. On Welcome, **Quick Unlock** is the small label above the PIN field, not a heading. The PIN field's accessible name is `Enter your 6-digit PIN`. The primary button reads `Unlock Practice desk`, `Unlock Live desk`, or plain `Unlock`, for the same cases as the heading. The idle lock overlay uses that same heading. Its PIN field's accessible name is `Enter your 6-digit PIN`. The PIN submits itself when six digits are entered. The overlay has no Unlock button. Quick Unlock reopens the same Mode the session already had, with the correct PIN. It keeps that Mode. The request is `POST /v1/auth/pin` with body `{ "pin" }`. A successful unlock replaces the session token. The previous token stops working. Live is entered only through `POST /v1/auth/live`, which requires the PIN and authenticator enrolment and also replaces the session token. Until the authenticator is enabled, that call refuses 403 `totp_required`. A session that is already Live keeps that enrolment check when Quick Unlock reopens it. Resetting a finished account needs you to be signed in. Recovery asks for an authenticator code only once an authenticator is enrolled. That ends your other sessions. Signed out, with no authenticator enrolled, the desk says **Sign in to reset this account. You'll need your password.** With an authenticator enrolled it says **Sign in to reset this account. You'll need your password and authenticator code.** With example data, `/settings` → **AI**, an unconfigured session shows the empty state "No LLM provider configured", with **Retry** and guidance `Example uses example data and cannot load or persist LLM secrets.` This is not a broken session; configure a provider in Live or Practice on this machine. Live and Practice still disable editing on a real load failure ("AI settings could not be loaded") to protect a saved configuration, and offer **Retry**. Selecting Managed Ollama while the runtime is absent shows **Not installed** — that is not a Connected advisor. AI Centre `/ai` Chat follows that install state and does not paint green **Connected (suggest only)** until the runtime is installed and configured. `/settings#leverage` always shows real leverage content or an honest empty. When the broker snapshot is available, the tiles show the current margin or leverage figures (read-only — change leverage on the broker platform). When leverage cannot be shown — unsupported broker, missing snapshot, or load failure — the pane shows `Leverage settings unavailable.` plus **Retry**. Selecting the Leverage tab never leaves a highlighted tab over a blank content pane. ### Monitoring Settings → **Monitoring** (`/settings#monitoring`) reads this install. It does not write `workspace.json`. It stays its own Settings section. The TopBar **Broker**, **Laya**, and **LLM** labels are a different cluster . **System Health** keeps service rows and machine rows apart. Signed-out checks use `GET /healthz` and `GET /readyz`, which return status only. `GET /health` needs a session. **Subsystem status** lists **Broker** and **DuckDB** on their own lines. Broker shows its note, otherwise its status, otherwise **unknown** — for example **Broker — Example**. DuckDB reads **DuckDB — Healthy** when the check passes, and otherwise its note (example data can read **DuckDB — Example**) or **Error**. **Example** on a service row is that service's fallback. Monitoring fallback rows are Example. It does not stand in for disk, Memory, CPU, GPU, or network, and it is not merged into the TopBar Broker / Laya / LLM cluster. **This host** is the machine where FlintTrade is installed. The rows are **Disk**, **Memory** (RAM on that machine), **CPU**, **GPU**, and **Network**. A measured row is labelled **This host**. - **Disk** shows used and total gigabytes. - **Memory** shows used and total RAM. - **CPU** shows utilisation against 100%, and the core count when the host reports it. - **GPU** is **GPU**, or **GPU —** the reported name. It shows used and total memory, or utilisation against 100%, when the host reports them. - **Network** shows cumulative **Sent** and **Received**. A missing host figure says **Unavailable**. So does a zero or absent disk or RAM total, a total that is not from this machine, and any sample that looks like real capacity — including an example-data disk or RAM total. The row stays **Unavailable**. It does not show 0/0 or invented gigabytes. When the backend returns a real host reading, including a degraded health response that still carries those totals, **This host** shows that reading in example data, Practice, and Live. When example data has no such reading, the host rows stay **Unavailable** while Broker and DuckDB may still say **Example**. **Process (this app)** appears when FlintTrade's own memory is known. It shows **RSS**, and **VMS** when that figure is known. A missing RSS on that row reads **RSS unknown**. The label is **Process (this app)**. Process RSS and VMS are never labelled as host Memory. **Traffic (this backend session)** shows **Requests / sec**, **Error Rate** (as a percentage), and **Top Endpoints** for this backend session. An empty window reads **No data yet**. **Latency (this backend session)** shows **Order Latency by Broker** with **Avg**, **p50**, **p95**, and **p99** in milliseconds. An empty table reads **No latency data recorded yet**. While a block is still loading it says so (**Loading health…**, and the same form for traffic and latency). If the backend cannot be reached, that block reads **Backend unreachable — health unavailable** (traffic and latency use the same form). Settings → **Report Bug** prepares a GitHub issue without background telemetry. The form keeps runtime/error diagnostics out of the public draft by default; enable the diagnostic-summary switch only after reviewing the displayed metadata. **Download diagnostics** writes a local JSON bundle that excludes raw request bodies, messages, tracebacks, account/user identifiers, entry ids and URL queries. Opening GitHub sends the displayed draft in the URL. Oversized drafts are copied for manual pasting, and security reports open the private GitHub Security Advisory form instead of a public issue. ### Layer 2: `.env` (advanced dev/server fallback) Native desktop users do not need `.env`. The repo-root `.env.example` exists only for Docker/systemd deployments, CI experiments, and contributor fallback testing when a setting cannot be supplied through the app UI. Secrets are stored as `_ref` fields — `secret://` references to hardened files under `/secrets/`. They are never written to `workspace.json` in clear text. ![Settings](screenshots/10-settings.jpg) --- ## 12. Troubleshooting ### Two operator accounts FlintTrade keeps one operator account on this machine. When the database has more than one, startup pauses and leaves every row in place. Nothing is deleted automatically. The screen heading is **FlintTrade couldn't finish updating**. The body reads: "This machine has two operator accounts, and FlintTrade supports one. Your data hasn't been changed. See Troubleshooting → Two operator accounts to choose which one to keep." **Open troubleshooting** opens this section (`USER_GUIDE.md#two-operator-accounts`; on the public site, `/docs/user-guide#two-operator-accounts`). **Retry** checks status again. On Welcome, **FlintTrade couldn't finish updating** stays on screen until that check succeeds and reports that nothing is pending (`migration_blocked` null). A failed check, or a check that still returns `two_operators`, leaves the screen up. A saved sign-in is restored only after that successful check. A failed check does not open the desk. Sign-in does not create a session while the update is paused: `POST /v1/auth/login` returns HTTP 409 with message `FlintTrade couldn't finish updating.` and does not issue a token. `GET /v1/auth/status` returns `migration_blocked` set to `two_operators` (null when the desk may open). When two accounts are present, the log line is `Update paused: this database has 2 operator accounts; FlintTrade supports one. No data was changed.` The number in that line is the operator count. List the accounts. Each row shows the id, username, and created time: ```bash flinttrade operators list ``` The columns are `id`, `username`, and `created`. Keep one account: ```bash flinttrade operators keep ``` The command prints `Keeping `, then one `Removing ` line for each other account, then asks `Remove N other operator account(s)? [y/N]`. Only `y` or `yes` continues. Any other answer, including Enter, prints `No data was changed.` and writes no backup. `--yes` skips that prompt, including for scripts: ```bash flinttrade operators keep --yes ``` With no terminal and no `--yes`, the command prints `No data was changed. A terminal is required, or pass --yes.` and writes no backup. On yes, it writes an owner-only backup beside the database, named `auth.db.bak-YYYYMMDDTHHMMSSZ`. If that name is already present, the stamp includes the fractional seconds (`auth.db.bak-YYYYMMDDTHHMMSSFFFFFFZ`). The file is in the workspace directory, next to `auth.db`: | Platform | Workspace directory | |---|---| | Linux | `~/.flinttrade/` | | macOS | `~/Library/Application Support/flinttrade/` | | Windows | `%APPDATA%\flinttrade\` | | Override | `FLINTTRADE_WORKSPACE_DIR`, then `FLINTTRADE_HOME` | On Windows, if `%APPDATA%` is unset, the directory is `%USERPROFILE%\AppData\Roaming\flinttrade\`. It then removes the other operator accounts and the rows in `auth.db` that name them, renumbers the kept account to id 1, and rewrites rows that named that account (`account_id`, `operator_id`, or `user_id`) to id 1, in one transaction. A failure rolls that transaction back, so the accounts are unchanged. Keeping the stored id 1 again leaves that account's sessions, settings, and data in place. The single-operator update continues. The success lines name the id you passed. They do not print the stored id: `Kept operator .` `Backup: /auth.db.bak-YYYYMMDDTHHMMSSZ` `One operator account remains. Open FlintTrade and choose Retry.` `This backup contains login secrets. Keep it private and delete it once FlintTrade works again.` `` in `Kept operator .` is the id you passed. The kept account is stored as id 1, so that stored id can differ from the id you passed. A later `flinttrade operators list` shows the kept account as id `1`. Passing `2` prints `Keeping 2 `, then one `Removing ` line for each other account, then `Remove N other operator account(s)? [y/N]`. After the transaction commits it prints `Kept operator 2.`, then `Backup: /auth.db.bak-YYYYMMDDTHHMMSSZ`, then `One operator account remains. Open FlintTrade and choose Retry.`, then `This backup contains login secrets. Keep it private and delete it once FlintTrade works again.` The next `flinttrade operators list` shows that account as id `1`, not `2`. If that transaction does not commit, the backup file is removed and the command prints `The operator update could not be finished. No data was changed.` The command edits `auth.db` only. In that transaction it removes the other operator rows and rows in the same file that name those operators through `account_id`, `operator_id`, or `user_id`, then stores the kept operator as id 1 and points that operator's rows at id 1. Login-attempt rows are not stored against an operator, so they stay. It does not open `workspace.json` or any other database. Open FlintTrade and choose **Retry**. On Welcome, **FlintTrade couldn't finish updating** stays up until the status check succeeds and reports that nothing is pending. ### "Port 5100 already in use" ```bash # Linux / macOS lsof -i :5100 kill # Windows (PowerShell) Get-NetTCPConnection -LocalPort 5100 | Select-Object OwningProcess Stop-Process -Id ``` ### "No LLM provider configured" or "AI settings could not be loaded" With example data, `/settings` → AI, the empty state "No LLM provider configured" is expected for an unconfigured session. Example data cannot load or persist LLM secrets. Use **Retry**, or configure a provider in Live or Practice on this machine. On Live or Practice, "AI settings could not be loaded" disables editing to protect a saved configuration. Use **Retry**. On AI Centre Chat, an unconfigured LLM shows **LLM not configured** (badge **Not configured**) with **Open Settings → AI** and an outline **Retry** that re-probes advisor status and Settings `#llm` hydration. Composer input and Send stay disabled. Example-data and Practice Chat both look unconfigured when Settings `#llm` is empty or the stored provider is blank — **Connected (suggest only)** must not appear from an env-default advisor `configured`. If leftover transcript messages hide that empty state, the header still offers **Retry** and **Open Settings → AI**. The Settings empty-state wording stays distinct from Chat's **LLM not configured**; they are aligned for readiness. When Settings → AI shows Managed Ollama **Not installed**, AI Centre does not show a green **Connected (suggest only)** badge. The badge is **Not configured** / **Not installed**, the Settings CTA stays visible, and the composer stays gated until the runtime is installed and configured. A provider string of ollama is not Connected while the managed runtime is absent. Chat never shows green **Connected (suggest only)** without a real LLM. When that LLM is configured, Chat may use Practice fills and native live-read feeds for analysis; Suggest stays labelled illustrative, and profitable alphas are not a release criterion. A configured but broken probe shows **Error** or **Disconnected** with **Retry**. Returning to Chat after saving Settings → AI re-checks readiness (advisor and Settings hydration), so the gate should not stay stuck on an outdated **Not configured**. ### "Token expired" when placing an order FlintTrade JWTs expire daily at 8 AM IST. Refresh the token by signing in again — the front-end will redirect you to `/welcome` automatically when it detects the 401. ### Place refused as Laya denied, or quantity reduced Order Pad and Quick Trade show this under the place controls. Scalper, Positions, Order Ladder, and Option Chain may still show a place error as a toast. 1. **Laya denied** — read the server reason under the headline. The denial is one alert (`role="alert"`), the only live region. The reason line is named **Laya decision** and is not its own status. Place controls stay off until Laya or the mode changes. **Max quantity N.** is the ceiling the server sent. The strip reads **Laya is Down. New orders are paused until it's Ready. You can still close positions.** when Live-facing status is Down. It mutes Live place. Practice is not muted by that strip. A Practice place is refused when Practice itself is Down, with **Laya is Down. New orders are paused until it's Ready. You can still close positions.** A non-exit order is HTTP 403. Start the opt-in model before a Down engine can admit. A close that only reduces an open position can still be sent when the books prove it. On Live, an unreadable broker order book refuses a reduce-only close, and that order takes the full check. While Laya is not Ready, Positions, the Practice Order Pad and the Live Order Pad show **Exit requested. Exits are allowed while Laya is Down. Check positions and orders for the outcome.** Exit-all shows **Exit-all requested. Check positions and orders for the outcome.** A close larger than the position is refused with that Down line. A second exit while one is already unfilled, and the broker order book can be read, is `Not placed. An exit for SYMBOL is already pending. Wait for it to fill, or cancel it and try again.` and the row shows **Exit pending**. When a retained exit exists and the broker's orders cannot be read, that refusal is `Not placed. Broker orders for SYMBOL are unavailable. Reconcile them before another exit.` If the position flips after the broker book loads, that row is tagged **Unexpected** and stays on screen with `Position changed after your broker's orders loaded. You're now SIDE QUANTITY SYMBOL. Close it if that wasn't intended.` SIDE is long or short. The Laya chip follows the current mode. Practice shows the managed Laya process. When Practice can admit, the chip shows **Ready** or **Degraded**, not **Down**. During the first load it says **Still loading**. In Live the chip shows Live-facing status. **Not qualified for Live** is the chip tooltip and the popover line when the managed Laya process is up and Live is not qualified. A Live place while Ready or Degraded, without a matching qualification record, says **Laya isn't qualified for Live yet. Practice orders are available.** A base checkpoint leaves Live unqualified. Laya starts **Down** and does not invent Ready. Start it from [Start Laya](#start-laya). Chat cannot place instead. 2. **Not placed. Laya allows up to N.** Nothing was placed. A clamp is only when the requested quantity is greater than the allowed one. Laya does not auto-place. **Place N** sends that quantity through admit again. On Order Pad, **Review Practice order** then shows that placed quantity. Place 1 on **Not placed. Laya allows up to 1.** places. SafetySystem and gate_order run when that quantity is allowed. **Cancel** places nothing. The model does not raise quantity. A Down refusal does not show **Max quantity** and does not say to start the model. **Laya Degraded — tighter limits** means Live-facing Degraded: Live is open with a tighter ceiling. It is not a Blocked strip. 3. On the Order Pad, example data records a sample fill. The success line is `Example order placed` and the id starts with `SAMPLE-`. The server refusal `Orders are not available for Example. Switch to Practice or Live to trade.` is the HTTP path. A safety-layer rejection names the layer and is a separate message. ### Orders not arriving / silently dropped 1. Check the Mode chip in the TopBar. **Example** is sample data, signed in as **Guest**. The Mode line reads `Example data. No broker is connected and no orders are sent.` Order Pad **Example Buy** on Terminal opens a review that reads `Example only. Nothing is sent to a broker and no order is placed.` **Continue** then records a sample fill (`Example order placed`, id starting `SAMPLE-`). Open the Mode menu and choose **Practice** for simulated fills, or unlock **Live** for a real broker order. 2. Open the **Orders** widget and look at the rejection reason column. A Laya denial or clamp stops before the safety layers. Order Pad and Quick Trade show it under the place control; see [Place refused as Laya denied, or quantity reduced](#place-refused-as-laya-denied-or-quantity-reduced). 3. Check the FlintTrade backend logs — the console where you ran `python scripts/ft.py start` (or `make start`). A safety-layer refusal is logged with the layer that blocked it. A Laya refusal is not that layer. ### Front-end shows stale prices ### "Cannot find module '@/...'" The path alias `@` → `packages/apps/terminal/src/` is configured in `tsconfig.json` and `vite.config.ts`. If your editor's TypeScript server disagrees, restart it. If `vitest` complains, the Vitest config lives inside `vite.config.ts` and inherits that alias — there is no separate `vitest.config.ts`. ### Settings page won't save `workspace.json` may be read-only or in a folder the process cannot write to. Check the path printed in the FlintTrade backend startup log and ensure the running user has write access. ### "Leverage settings unavailable." or a blank Leverage pane A highlighted Leverage tab on `/settings#leverage` must never sit over a blank pane. The pane shows real leverage content, or the honest empty `Leverage settings unavailable.` plus **Retry**. Leverage is a read-only broker snapshot — change it on the broker platform, not in FlintTrade. ### Where to get help - **Settings → Report Bug** — prepare a bounded issue draft and optional local diagnostic bundle from inside FlintTrade. - **Question issue template** — for focused usage questions and setup help. - **GitHub Issues** — for bugs and feature requests (use the templates in `.github/ISSUE_TEMPLATE/`). - **security.md** — for security issues (private disclosure via GitHub Security Advisories). If your issue requires a backend log, attach the relevant lines from the console where you ran `python scripts/ft.py start`, or from the persistent log at `/logs/flinttrade.log` (redact any broker account IDs or tokens first). --- ## 13. Uninstall Uninstalling is one command on every OS, and it is the same command for a web-app install and a desktop install. The plain uninstall removes only the installed application and its launcher integration; your workspace, managed source, toolchain and data are retained so a reinstall can recover them. ```bash # macOS / Linux curl -fsSL https://flinttrade.vercel.app/uninstall.sh | bash ``` ```powershell # Windows 10/11 irm https://flinttrade.vercel.app/uninstall.ps1 | iex ``` ### Removing your data as well Add the purge flag — `--purge` on macOS/Linux, `-Purge` on Windows — to also delete recognised FlintTrade data (workspace, managed source and tools). Purge is irreversible and asks for typed or explicit scripted confirmation. ```bash # macOS / Linux curl -fsSL https://flinttrade.vercel.app/uninstall.sh | bash -s -- --purge ``` ```powershell # Windows 10/11 & ([scriptblock]::Create((irm https://flinttrade.vercel.app/uninstall.ps1))) -Purge ``` Ordinary backup archives bhavcopy CSVs only. It does not capture workspace settings or credentials — see [Backup and restore](setup/backup.md). ### If the site is unreachable Use this whenever an uninstall command above fails: the hosted URLs are only a redirect to the scripts in this repository, and a site outage answers `503` rather than the script. Fetch the same uninstaller straight from GitHub: ```bash # macOS / Linux curl -fsSL https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-uninstall.sh | bash ``` ```powershell # Windows 10/11 irm https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-uninstall.ps1 | iex ``` Or, from a clone or from the managed source checkout (`~/.flinttrade/web-src/FlintTrade` for the web app, `~/.flinttrade/src/FlintTrade` for the desktop shell), run them directly: ```bash # macOS / Linux bash scripts/install/flinttrade-uninstall.sh ``` ```powershell # Windows 10/11 powershell -ExecutionPolicy Bypass -File scripts\install\flinttrade-uninstall.ps1 ``` Both accept the same purge flag (`--purge` / `-Purge`). ### Where settings live Open Settings from the app bar, your avatar, or Tools. Ctrl+, (Command+, on macOS) opens Quick Settings for common display preferences. Appearance has one colour-mode selector and one glass-effect toggle. Broker shows native account status and availability restrictions. Connection alone does not enable live orders. Native connection and HTTP account reads remain frozen on this unreleased line. Leverage appears only with a connected broker account, and not on example data. Existing `/settings#api` links open the native Broker page. Risk & Safety owns daily-loss thresholds, global safety caps, the confirmed kill switch and account-scoped opening-capital/reset controls. Automate and the Trade desk link to it. Telegram setup and test messages live in Settings → Telegram. Manage workspaces on the Trade desk includes built-in templates and Saved presets. Saved presets retains create, edit, fork, import, export and delete controls; existing `/settings#presets` links open desk management. AI save status appears only on the AI page. Fork creates a named copy with the same description and widgets. Open Edit on the copy to change its contents. In the public demo, custom presets are saved only in the current browser and are not synced to an installed account. Export a preset as JSON to keep a portable copy. Installed builds continue to save presets through their local FlintTrade backend.