docs/ORDER_SAFETY.md
Rama main · réplica de lectura del repositorio original.
# Order Safety Notes
FlintTrade is single-operator, self-hosted software. It is not a broker, an
investment adviser, a managed strategy product, a compliance product, or a
service that accepts funds or account access from other people. Nothing in this
repository is financial, investment, tax, legal, or regulatory advice.
This page describes the technical safety controls FlintTrade applies when an
operator enables an order-capable integration. Broker, exchange, tax, and
regulatory responsibilities remain with the operator and their broker.
## Order-Gating Model
**Placement.** Every order FlintTrade submits goes through admission
when it's placed. The HTTP routes that submit an order are
`POST /api/v1/orders/place`, `POST /api/v1/orders/<broker>/place`,
`POST /api/v1/positions/exit-all`, and `POST /api/v1/orders/bracket`
when the body has exactly one stop-loss or one target. Each bracket leg
is admitted, then placed through SafetySystem, `gate_order`, and
`BrokerRouter`. Practice on that route is HTTP 403 `practice_unsupported`.
GTT, a broker-held variety, a stop-loss and a target together, and a
trailing stop are refused before that admission. Operator and automate
place (core `/orders/place`, strategy dispatch, and webhook place) share
the admission below. `place-smart`, `open-position`, and `close-position`
are not mounted. The Practice sandbox has no place or square-off route.
Settings → Practice does not place.
1. The mode guard runs first. Example data stays HTTP 403 `mode_blocked`
(`Orders are not available for Example. Switch to Practice or Live to trade.`)
and does not enter `Laya.admit`. The Terminal Order Pad records a sample
fill on the client for example data (`Example order placed`, id starting
`SAMPLE-`). This guard is the server path.
2. `Laya.admit` then admits or refuses the proposal. A refusal
(`laya_denied`) or a quantity clamp (`laya_clamp`) stops before
SafetySystem on Live and before the native sandbox on Practice.
Neither quantity is placed.
3. An allowed Live place runs `SafetySystem.check_order` in runtime order
L5 → L4 → L1 → L2 → L3 (kill switch, daily P&L, field validation,
position limits, portfolio risk).
4. `gate_order` then mints the `SafetyContext` bound to that order and the
selector-bound principal. It does not re-run the layer checks.
`gate_order` remains the only mint after an allowed Live place.
5. `BrokerRouter.place_order` re-HMACs, matches fields, applies the account
ACL, and consumes the one-shot gate.
An allowed Practice place is admitted, then goes to the native sandbox. It
does not enter SafetySystem, `gate_order`, or `BrokerRouter`. Before Laya,
place refuses a quantity that is not a positive multiple of the lot, or
that is above the freeze limit, with HTTP 400 `quantity_admission`. The
Order Pad shows the lot sentence once, inline:
`Quantity must be a multiple of the lot size (N).`
`N` is that contract's lot. It is not a published constant. A missing
verified lot is a different sentence, also shown once, inline:
`Not placed. The lot size for <name> isn't verified.`
`<name>` is the contract's display name. The Practice sandbox returns
that sentence as HTTP 400 with `status: error` and no `code` field, and
it does not fill the order. 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. The routed place route is Live only and uses the same
Live admission. Modify and cancel are not this admission. `POST /api/v1/orders/cancel-all` only
cancels. A body with `"variety": "gtt"` 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.`
No submit route reaches a broker forever or super-order endpoint. The
Kotak Neo adapter refuses a `gtt` place. `POST /api/v1/orders/forever` returns
HTTP 501 `Orders are placed through /api/v1/orders/place.` and does not
call a broker. Practice and Live both refuse that variety before the
sandbox or a broker. The Order Pad GTT option stays visible and disabled,
with the tooltip `GTT orders aren't supported right now.`
**Regular modify and cancel:**
1. Ordinary modify and cancel are blocked while Layer 5 is latched. A
cancellation can remove a protective exit, so it is not exempt.
2. A risk-increasing modify also runs `SafetySystem.check_order` on the
proposed order. A proven no-increase modify and a cancel skip that chain.
3. `gate_order` mints the `SafetyContext` over the canonical fingerprint
(`_op` is `modify` or `cancel`).
4. `BrokerRouter.modify_order` / `cancel_order` re-verify and consume the
gate.
**Extended verbs** (forever modify/cancel, super-order, conditional
trigger modify/cancel, convert, exit-all, reducing, multi, cancel-all,
smart-cancel). Exit-all is a submit route: the server records a
reduce-only proof for each open contract before `exit_all_positions`.
Forever, basket, split, and trigger *place* are not submit routes.
1. Risk-increasing legs still run `SafetySystem.check_order` where the
route admits exposure.
2. `gate_broker_write` mints the `SafetyContext` (it delegates to
`gate_order`, which remains the sole mint). The verb must be listed in
`GATED_WRITE_VERBS` and `payload["_op"]` must equal the verb.
3. `BrokerRouter.execute_gated` re-verifies and dispatches.
New integrations must preserve this split. Do not invent a third path, and
do not send an extended verb through `gate_order` alone.
## Laya admission
An operator or automate proposal is admitted or refused as a typed verdict
with `allow`, `reason`, and `limits`. Laya does not place an order and does
not mint the one-shot write ticket. `gate_order` remains the only mint.
Chat may suggest and explain only: bring your own API key, use managed
Ollama, or point **Custom (OpenAI-compatible)** at another local runtime.
Chat is not an admission source, and Chat downtime does not close Live.
Operator and automate place run the mode guard, then `Laya.admit`.
**Live** place then runs SafetySystem L1–L5, `gate_order`, and
`BrokerRouter`. Laya does not replace those layers, and `gate_order`
remains the only mint after an allowed Live place. **Practice** place is
admitted before the sandbox and does not enter SafetySystem, `gate_order`,
or `BrokerRouter`. Example data stays `mode_blocked` before admit. A refusal
(`laya_denied`) or a quantity clamp (`laya_clamp`) stops before
SafetySystem on Live and before the sandbox on Practice. A clamp names
the reduced quantity. Neither size is placed. Order Pad and Quick Trade
require the operator to place that reduced quantity. An automate clamp is
a dispatcher error and does not place the reduced quantity on its own.
Chat is not an admission source. Modify, cancel, and cancel-all are not
admitted as place. Forever place, basket, split, and conditional-trigger
place do not submit. A Live bracket with exactly one stop-loss or one
target does submit. A GTT body is refused on every submit route
before Laya admission and SafetySystem. No submit route reaches a broker
forever or super-order endpoint. The Kotak Neo adapter refuses a `gtt` place.
Admission order is fixed. Down is checked first. The existing hard rules
then run unchanged (source, mode, symbol, side, quantity, price, trigger).
Only after those rules pass does Laya ask typed A/B questions about free
text: whether a note states a reason, whether it shows tilt or revenge, and
whether the stated plan contradicts the order side. Expiry, quantity, price,
and symbol are not sent to the model. Thresholds sit on option probabilities
in the versioned `laya_policy.toml`. There is no confidence field in the
gate. The model can deny or clamp. It cannot raise a quantity or overturn a
rule refusal. An unreachable host, a timeout, a malformed response, or a
revision or digest mismatch is Down. Down refuses Practice as well as Live.
Down copy is the same in every mode: "Laya is Down. New orders are paused until it's Ready. You can still close positions." A non-exit order is HTTP 403. That refusal carries no quantity ceiling. A Practice
refusal never says Live. When Laya is Ready
or Degraded and a Live place is refused only because Live is not qualified,
the reason is "Laya isn't qualified for Live yet. Practice orders are
available." An uncertain answer, including an empty note, clamps in
Practice and denies in Live. It is not a hard reject. The Practice server
reason is "Laya is uncertain. Quantity stays inside the tighter limit."
The Live server reason is "Laya is uncertain. Live stays closed." On a
denial, Order Pad and Quick Trade show that server reason inside one
alert (`role="alert"`), the only live region. The reason line is named
"Laya decision" and is not its own status. The desk
does not auto-place. A clamp is only when the requested quantity is
greater than the allowed one. Order Pad and Quick Trade show "Not placed.
Laya allows up to N." with Place N and Cancel. Place N sends that
quantity. On Order Pad, "Review Practice order" then shows the placed
quantity. Place 1 on "Not placed. Laya allows up to 1." places, because
that request is already at the allowed quantity.
Chip reason codes are `not_started` (Not started), `stopped` (Stopped),
`port_in_use` (`Port <n> in use`), `still_loading` (Still loading),
`downloading` (`Downloading the model · X of Y GB`), `download_failed`
(Can't download the model), `unreachable` (Unreachable), `unverified`
(Can't verify the model), `wrong_revision` (Wrong model version),
`key_rejected` (Can't reach Laya), and `key_missing` (The Laya API key file is missing.). A health check does not replace `key_missing` with Not started. `<n>` is the managed Laya port. For
`not_started`, `stopped`, `port_in_use`, `still_loading`, and
`unreachable`, the tooltip is the label followed by
`. Next: python -m flinttrade_core.laya_runtime start`. `downloading`
has no tooltip and no Next line, and it is not Still loading. The
`download_failed` tooltip is "Check your connection, then Start Laya again."
`downloading` and `download_failed` use the status word Down. Orders are
refused with "Laya is Down. New orders are paused until it's Ready. You can still close positions."
The `unverified` tooltip is "The installed model couldn't be checked
against the pinned version. Restart Laya. If it keeps happening, reinstall
it." That code applies when this start did not download. A failed download,
including one over an older unverified snapshot, is `download_failed`
("Can't download the model"). The `wrong_revision` tooltip is "Laya is running a different model
than FlintTrade expects." That code is only a real mismatch: a complete
download whose files do not match the pin, a snapshot already on disk
that this start is not replacing, or a running managed Laya process that reports
another revision or digest. A dropped connection, a partial download, or
a failed download or swap, including one that puts the previous
checkpoint back, is not this code. The
`key_rejected` tooltip is "Laya restarted with a new key. Reconnecting…"
The chip stays Down and 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: Unreachable, or 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 that sentence.
`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. The runtime record holds
the sha256, pid, and start token, and the inode, size, and modification
time of the weights file and of each pinned file
(`<workspace>/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
`<workspace>/runtime/laya/decisions.jsonl`. It records `proof=decision`
or `proof=runtime`. An admitted Practice place with an empty note skips
the model and 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. If the
record cannot be checked, the chip reason is `unverified`. Stopping the
managed Laya process records Down before an in-flight probe can publish Ready. Desk
place surfaces go through this admission. 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 (`unverified`) and the managed Laya process does not start. A
changed byte in a snapshot that this start is not replacing shows Wrong
model version (`wrong_revision`) 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. `start`
downloads the commit in `[checkpoint] revision`, not the model repository's
default branch, into `<workspace>/runtime/laya/staging` when the weights
file or a manifest file is not on disk, and when the runtime checkpoint
is on disk but its hashes are not the pin. The download does not start
the managed Laya process. That download sets `HF_HOME` to
`<workspace>/runtime/laya/hf-home` and `HF_HUB_DISABLE_XET=1`, so transfer
logs stay out of the shared cache. The model is about 2.37 GB, and that
size is reported once. While it runs, including a pin change, the chip is
`Downloading the model · X of Y GB` (for example `Downloading the model · 1.2 of 3.4 GB`) and the status word is Down. There
is no Updating label. When no checkpoint is already there, a full match
renames staging onto `<workspace>/runtime/laya/checkpoint`. When a
checkpoint is already there, the current copy stays in place until the
new files match. On a full match that checkpoint is renamed aside to
`checkpoint.old-<random>` in the same runtime directory, staging is
renamed onto `checkpoint`, then the old copy is deleted. Hub access stays
off for that launch. If that second rename fails, the old checkpoint is
renamed back and the chip is `download_failed` ("Can't download the
model"), not `wrong_revision`. A complete download whose files do not
match the pin is `wrong_revision`, staging is deleted, and the checkpoint
already on disk stays. An extra loadable file in a complete download is
`unverified`. A dropped connection, a partial or missing file, or a read
error is also `download_failed`. The managed Laya process does not start on files that
do not match the pin. If the download does not finish, the chip is `download_failed`, not
`wrong_revision` and not `unverified`, whatever older snapshot is on
disk. `unverified` stays when this start did not download. A snapshot
already on disk is `wrong_revision` only when this start did not
download. Those failures delete the staging directory and leave the shared
model cache alone. 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, the managed Laya process does not start on it; the pinned download runs
instead, and a failed download leaves `download_failed` with that copy
still on disk.
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/<revision>/model.safetensors` into `blobs/`), the
launch path is the snapshot file, not the blob. 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=<path> sha256=<digest>`. 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=<path> changed=<field>`, where `<field>` 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, so a command-line
stop or start, or a key rotation, is reconciled by that watch. The desk
polls `GET /api/v1/ping` every 1.5 seconds. That ping reconciles the
pid, the key, and the runtime record the same way an order does, so the
chip and the order gate read the same state. A stop or a start 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 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 stays that sentence.
New orders stay paused. A reduce-only close is unchanged.
When decision status is Down, the desk opens incident class `laya` ("Laya is
Down. New orders are paused until it's Ready. You can still close positions.").
That class closes a new Live place on the shared
client place path. The server decides reduce-only. A client flag is ignored.
A close qualifies inside either place route when it reduces the same contract without changing its sign. Its opposite-side quantity cannot exceed the open quantity minus pending exits. On Live, available position and broker-order books must establish that bound; this desk’s pending exits also count. Missing, malformed or contradictory books cannot establish a reduce-only close or known flatness. There is no unreadable-book fallback.
Laya records a qualifying close with proof kind `reduce_only` and does not deny or clamp it. Down and Degraded do not block that verified close. Live still runs SafetySystem after the record. A second exit on the same exact broker account and contract, while this desk’s earlier exit remains unfilled, returns HTTP 409 `exit_pending`: `"Not placed. An exit for <symbol> is already pending. Wait for it to fill, or cancel it and try again."`
Practice uses that code on the Practice book. On Live, readable broker evidence and this desk’s retained reservations determine the refusal. If a retained exit exists but broker orders are unavailable, the response is HTTP 409 `exit_orders_unreadable`: `"Not placed. Broker orders for <symbol> are unavailable. Reconcile them before another exit."` The response’s `message` and `reason` contain the same text. The label is the symbol, or `this contract` when the symbol is empty.
The Positions row shows **Exit pending** for the unfilled-exit case. A position whose sign flips after the broker book has
loaded keeps that row, tagged **Unexpected**, and the book shows
`Position changed after your broker's orders loaded. You're now <long or short> <quantity> <symbol>. Close it if that wasn't intended.`
until dismissed.
Anything that would flip or add to a position takes the full admit.
`POST /api/v1/positions/exit-all` requires a server-side reducing proof before requesting exits. Unavailable position evidence returns HTTP 503; ambiguous evidence returns HTTP 409. Neither response sends a square-off. An accepted exit-all or reducing place means requested, not filled or flat: check broker positions and orders for the outcome.
`POST /api/v1/orders/cancel-all` requests cancellation only. Cancellation acknowledgement does not establish terminal cancellation or closed exposure. Layer 5 also requests exits after resting-order cancellation; those emergency paths remain reachable while Laya is Down. Only independently confirmed reducing execution may be described as closed.
Broker may stay **Connected** or **Connected (read)**. Laya starts Down.
`GET /health` records Ready, Degraded, or Down from the opt-in managed Laya process when
one is registered. The desk polls `GET /api/v1/ping` every 1.5 seconds. That ping reconciles the watched pid, key, and runtime record the same way an order does, then
publishes Live-facing `laya`, sidecar `laya_practice`,
`laya_live_qualified`, `laya_reason`, and `laya_port`. It does not invent Ready. The Laya chip label follows
the current mode, so Practice shows the managed Laya process and does not read Down while
Practice orders are being admitted. During the first load the chip says
Still loading. "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 base checkpoint is not
qualified for Live, so Live stays Down until a qualification record exists
for the exact model revision, weight digest, and policy version
(`EvidenceUseScope.LIVE_DECISION`). Practice can be Ready or Degraded from
the same probe. Degraded does not open that class and does not mute Live.
Degraded enforces the tighter quantity ceiling and the desk says so. Down
does not add a second deny under a Live control that is already muted.
Other Live write verbs still reach SafetySystem without this admission.
Other Practice verbs go straight to the sandbox. The managed Laya process install is in
[DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md#laya-decision-process).
Operator steps are in [Start Laya](USER_GUIDE.md#start-laya).
| Concern | Automate risk note | SafetySystem | Ticket guards | Laya |
| --- | --- | --- | --- | --- |
| Allow or deny | `allowed` and `reason` | L1–L5 pass or fail | refusal string | verdict `allow` and `reason` |
| Size | `position_qty` | L1 quantity, L2 limits | lot multiple | `limits.max_quantity` |
| Price | stop and target | L1 price band | limit and trigger present | price and trigger present |
| Mode | not modelled | mode guard | Example data refused | Example data stays `mode_blocked` before admit; Down refuses the proposal |
| Book, margin, Greeks, daily loss, kill | daily loss is agent config | L2–L5 | not present | not owned |
The automate risk note is not this admission. Lot size, the price band,
margin, Greeks, daily loss, and the kill switch stay where they are.
## Unknown Broker Outcomes
If adapter entry occurred but FlintTrade could not persist or receive the
broker acknowledgement, the lifecycle ledger records `OUTCOME_UNKNOWN` and
blocks later normal writes. A clean broker snapshot never clears that state:
the order may have filled and disappeared or may use an identifier FlintTrade
did not receive.
Recovery is an operator decision in the Reconciliation widget. The operator
must inspect the broker, use an authenticated PIN-unlocked Live session, own
the exact broker/account selector, and type the decision, selector and attempt
ID exactly. Before accepting a new decision, FlintTrade forces reconciliation
for only that selector and requires the resulting snapshot generation to be
durably adopted. Finalisation revalidates the same evidence under the outcome
lease so a newer conflicting snapshot cannot be ignored. Snapshot timestamps
are monotonic: older snapshots and same-time snapshots with different content
fail closed. Every broker-observed material order identity is retained as an
immutable, generation-scoped observation, so a later changed or empty snapshot
cannot erase earlier matching evidence. The public diff is cryptographically
bound to recursively frozen private broker/local snapshots and rebuilt before
persistence and again before adoption. Malformed, mutated, or mismatched
reconciliation reports are rejected rather than interpreted as clean.
For applied placements, every supplied broker ID must have been first observed
after the attempt was invoked and must match all material persisted identity
fields. Basket IDs are bound to explicit child indexes rather than inferred
from list order; a partial decision must partition every child into applied and
not-applied sets. Modify and cancel decisions use operation-specific state
evidence. A cancel-pending state, a mixed modify state, missing broker fields,
an `UNKNOWN` material marker, or an operation for which the ledger did not
persist enough evidence remains blocked. Broker normalisers do not substitute
zero for an omitted numeric order field; an explicit zero remains valid and an
omitted or malformed value remains unavailable.
FlintTrade first persists the immutable decision as `PENDING_AUDIT`, fsyncs a
hash-chained audit event and independently verifies its receipt. The ledger
then commits `CONFIRMED_APPLIED`, `CONFIRMED_NOT_APPLIED`, or
`CONFIRMED_PARTIAL` together with the local order-state update, but retains a
durable `PENDING_ROUTER_CLEAR` record until the current router removes only
that attempt's fault. Completion requires a verifiable receipt bound to the
resolution, attempt, selector, and exact current-router generation; a boolean,
missing, stale, or mismatched proof fails closed. A `PENDING_AUDIT` retry must
obtain a newer exact-selector
generation; the ledger archives the prior pending revision and creates a new
resolution ID. A `PENDING_ROUTER_CLEAR` retry resumes the already committed
decision without another broker read. Other
unknown attempts and unrelated critical ledger health, including conservatively
migrated legacy faults, continue to block normal writes. Recovery performs no
broker mutation and emergency reducing writes remain on the gated path.
The terminal rejects malformed or contradictory status, report, success and
structured-error envelopes before changing cached state or showing a healthy
result. Attempt identity, canonical outcome, status, booleans and authorised
remaining-outcome counts are runtime-validated. Authentication transitions
retire the entire QueryClient generation, so callbacks from an older principal
can finish only against the retired client and cannot repopulate the next
principal's cache.
If task cancellation or another `BaseException` crosses a broker-write boundary
after adapter invocation, the attempt is durably marked `OUTCOME_UNKNOWN` before
the exception is re-raised. Audit appends repair and fsync a torn tail in the
newest plain chain segment, including across a date rollover, before extending
the chain; archive compression uses a same-directory,
fsynced atomic replacement so a crash preserves either the source or a complete
archive.
## Safety Layers
| Layer | Purpose | Examples |
|---|---|---|
| L5 Kill switch | Stop order-capable workflows, cancel open orders, and request position flattening where supported. | Explicit UI button, API endpoint, or Telegram command. Checked first. |
| L4 Daily P&L | Pause or hard-stop subsequent new orders when daily loss thresholds are hit. | Default pause at 3 % and hard stop at 15 %; no broker-side cancel or flatten. |
| L1 Order validation | Reject malformed or disallowed orders before routing. | Symbol, exchange, side, quantity, order type, price, and market-session checks. |
| L2 Position limits | Prevent local workflows from exceeding configured exposure. | Default max five open positions and 60 % margin-use guard. |
| L3 Portfolio risk | Cap book-level Greek exposure. | Net delta and net vega guards. |
## Kill Switch
Kill switch triggers:
- Telegram bot: `/kill` command when the operator enables the integration.
- Terminal UI: the Kill Switch control on Terminal (Live mode
only), and Activate / Reset on `/automate` → Settings.
- API: `POST /api/v1/safety/kill-switch` to latch Layer 5 (JSON body
`{ "reason": "…" }`). `DELETE /api/v1/safety/kill-switch` resets it after
emergency actions complete. From the Vite dev proxy the same routes are
`/ft-api/api/v1/safety/kill-switch`. There is no `/activate` suffix.
The percentage-based Layer 4 daily-loss thresholds do not activate this kill
switch. They latch new-order admission until manually reset. Automatic
account-level flattening, when enabled, is owned separately by the authoritative
rupee MTM circuit breaker.
The kill switch is a local software control. It should be paired with broker-side
limits, broker-side position checks, and manual review of live-mode settings.
## Audit Logging
FlintTrade writes local audit events for order and safety activity. The audit
directory is resolved by `audit_log_dir()`: `AUDIT_LOG_DIR` (direct directory),
then `<storage.archive>/audit` when `workspace.json` sets `storage.archive`,
then `archive/audit/` inside the workspace directory
(`FLINTTRADE_WORKSPACE_DIR`, then `FLINTTRADE_HOME`, then the OS default):
| Platform | Default audit archive |
|---|---|
| Linux | `~/.flinttrade/archive/audit/` |
| macOS | `~/Library/Application Support/flinttrade/archive/audit/` |
| Windows | `%APPDATA%\flinttrade\archive\audit\` |
| Override | `AUDIT_LOG_DIR`, then `<storage.archive>/audit`, then the workspace `archive/audit/` path above |
| Event | Typical fields |
|---|---|
| `ORDER_PLACED` | Strategy or source, symbol, exchange, side, quantity, order type, timestamp. |
| `ORDER_MODIFIED` | Original fields, modified fields, reason, timestamp. |
| `ORDER_CANCELLED` | Source, symbol, reason, timestamp. |
| `SAFETY_CHECK` | Layer, verdict, reason, and order summary. |
| `LOGIN` / `LOGOUT` | Session start or end, broker label, timestamp. |
| `KILL_SWITCH_ACTIVATED` | Trigger, local P&L snapshot, affected positions or orders where available. |
Audit retention is a local configuration choice. These logs are useful for
debugging and personal records; they are not a legal or regulatory attestation.
## Rate Limits
| Category | Default local limit | Enforced by |
|---|---|---|
| Smart orders | 2 per second | Same split: proxy 429 plus client-side throttle. |
Limits apply across configured exchanges for the running FlintTrade instance.
Operators should also configure any limits available in their broker dashboard.
## Sessions And Credentials
- Broker login, OAuth, TOTP, and exchange access remain broker-side concerns.
- Native-adapter broker credentials live in the encrypted gateway vault.
- The account transaction foundation retains unknown authentication outcomes
rather than retrying them. Its vault and workspace evidence must remain
coherent before a read generation can be exposed. Redacted terminal audit
events have durable pending delivery and stable IDs; audit export never
repeats authentication or order work. Production native account HTTP
mutations remain `503`, and native HTTP reads remain `409` until their
separate cutovers land.
- Secrets should be file-backed under your platform workspace directory
(`~/.flinttrade/` on Linux, `~/Library/Application Support/flinttrade/` on
macOS, `%APPDATA%\flinttrade\` on Windows; overridden by
`FLINTTRADE_WORKSPACE_DIR`, then `FLINTTRADE_HOME`). Telegram and LLM
credentials live as hardened files under `<workspace>/secrets/` with a
`secret://` reference in `workspace.json`. Do not commit credentials or
personal network details.
## Market Metadata
FlintTrade includes market-hours, expiry, fee, and cost-model metadata so local
screens, calculators, and backtests can behave consistently. Treat this metadata
as software configuration, not advice or a guarantee that an order is suitable,
permitted, or profitable.
## Operator Checklist
Before enabling live-mode order routing:
1. Review the source code for the order path you plan to use.
2. Run in sandbox mode first and inspect the audit log output.
3. Configure broker-side safeguards such as order limits, account limits, and
manual approval controls where available.
4. Keep broker credentials, API keys, and personal network details out of Git.
5. Confirm that every enabled automation still routes through `gate_order`
or `gate_broker_write`.
6. Treat FlintTrade as local software for your own account, not as an investment
service for others.