Loss is budgeted before it is taken
T3 Trade enforces risk limits in code rather than relying on model instructions. Each mission has a fixed cumulative-loss ceiling, a 14-item checklist before any position increase, a persistent block when that budget is exhausted, and protective stops on the exchange. Seven user controls can reduce or remove exposure even when the provider process is unavailable.
The ceiling is arithmetic, not opinion
A mission's mandate includes its cumulative-loss ceiling in
maximumCumulativeLossUsd. Everything the mission has lost, is
losing, and has queued to lose is measured against it by one pure function,
evaluateLossBudget, in
packages/trading-contracts/src/lossAccounting.ts:106-138.
Property tests pin every equation, and the functions take already-reconciled inputs:
they never read the exchange
packages/trading-contracts/src/lossAccounting.ts:15-17.
One reader gathers those inputs: realised PnL and paid fees from the reconciled
fills table, open-position risk from the position snapshots, reserved risk from the
reservations table, plus the notional of accepted, unfilled resting entries for the
aggregate caps
apps/server/src/trading/TradingBudgetReader.ts:4-12. The
closedPnl in those rows is the exchange's own attribution per fill, not a number T3
computes packages/contracts/src/trading.ts:132. Net
funding reads as zero until a funding source is wired
apps/server/src/trading/TradingBudgetReader.ts:115-118.
Two rules materially affect the calculation. First,
profits never raise the ceiling. Equation 2 clamps realised loss
used to max(0, -result), so a winning stretch drives used
toward zero and stops there
packages/trading-contracts/src/lossAccounting.ts:111-112.
The risk policy encodes the same rule in the schema itself:
positivePnlExpandsLossBudget: Schema.Literal(false). Not a
boolean that happens to be false; a type that cannot hold true
packages/trading-contracts/src/authority.ts:41.
Second,
this budget calculation assigns zero directional risk when the stop price is
missing. Separate validation still refuses a position increase without a valid stop. When
the stop price is unknown, the loss-to-stop term contributes 0, because substituting
a $0 stop would book the full notional of a long against the budget and exhaust it
instantly
packages/trading-contracts/src/lossAccounting.ts:39-51.
Paid fees are never double-counted either: fees already paid live in the realised
result and are never also reserved as unpaid open-position fees
packages/trading-contracts/src/lossAccounting.ts:8-11.
Eq 4 is also the reservation the checklist holds against the budget and the
execution service persists before signing
apps/server/src/trading/TradingPreviewService.ts:379-393.
Fourteen questions, in an order that matters
Before any position-increasing action is signed, it runs the §16.3 checklist: 14 items
in their listed order, each with its own rejection reason, plus two exit-only items
apps/server/src/trading/TradingPreviewService.ts:40-70. The
service is pure validation over already-loaded state: it reads no exchange and mutates
no tables
apps/server/src/trading/TradingPreviewService.ts:10-15. The
checklist order affects safety
apps/server/src/trading/TradingPreviewService.ts:444.
TradingPreviewService.ts:193-199
TradingPreviewService.ts:201-207
TradingPreviewService.ts:209-217
TradingPreviewService.ts:239-272
TradingPreviewService.ts:286-298
TradingPreviewService.ts:300-310
TradingPreviewService.ts:312-315
TradingPreviewService.ts:317-325
TradingPreviewService.ts:339-351
TradingPreviewService.ts:353-361
TradingPreviewService.ts:371-377
TradingPreviewService.ts:395-411
TradingPreviewService.ts:413-421
TradingPreviewService.ts:423-442
TradingPreviewService.ts:54-63
TradingPreviewService.ts:64-69
An exit runs the nine checks about executing it correctly and drops every check about
whether more exposure should be permitted. Those entry rules once blocked exits when
risk was already elevated. A blocked mission, disabled entries, or an exhausted budget
could each prevent the action needed to reduce exposure
apps/server/src/trading/TradingPreviewService.ts:462-483. The
exit list also drops the exchange minimum, because refusing to close a dust position is
the minimum inverted
apps/server/src/trading/TradingPreviewService.ts:515-523.
The stop gate runs twice
Preview checks the stop against the harness's limit price. The execution service then
runs the identical checkStopInformation again against the price
actually going on the wire, the BBO-derived limit for a marketable IOC, before
anything is persisted and before a nonce is spent
apps/server/src/trading/HyperliquidExecutionService.ts:605-624. A position increase is submitted only if both stop checks pass.
When the budget is gone, the mission stops
Exhaustion is a computed flag, not a judgement: exhausted when
remaining budget is at or below zero
packages/trading-contracts/src/lossAccounting.ts:135. What
happens next is a fixed sequence run by the execution guard, and it runs to the end
regardless of who is watching.
Block increases at the gate
guardAction refuses every position-increasing action while
the budget is exhausted. Cancel, reduce, close, and modify_stop still pass, because
§16.4 blocks taking on risk, not managing the risk already open
apps/server/src/trading/TradingExecutionGuard.ts:172-184,
packages/trading-contracts/src/lossAccounting.ts:145-158.
Cancel resting increasing orders
blockForExhaustion reads the mission's resting
position-increasing orders and attempts to cancel each one. The cancellation report
is returned, never swallowed (RC03), but an unconfirmed cancel does not stop the
block: abandoning the safety block because one cancel would not confirm would trade
a bounded uncertainty for a mission that keeps increasing exposure
apps/server/src/trading/TradingExecutionGuard.ts:196-219.
Mark the mission blocked
Transition to blocked with reason
cumulative_loss_limit
apps/server/src/trading/TradingExecutionGuard.ts:221-227.
Reconcile every held market
So local order rows reflect the cancels before the blocked status is announced;
every market the mission holds, because the exhaustion cancelled orders on all of
them
apps/server/src/trading/TradingExecutionGuard.ts:240-255.
Stay blocked until the user says otherwise
guardResume rejects a harness resume: only an explicit user
resume after revalidation clears it, and the harness's own resume request is refused
while blocked
apps/server/src/trading/TradingExecutionGuard.ts:259-271,
apps/server/src/trading/TradingExecutionGuard.ts:5-9. No
path lets the model spend its way back into a budget it already spent.
An error is not a verdict
The guard keeps budget_exhausted and
infrastructure_error apart because a stuck execution record was
once reported to the harness as budget_exhausted inside a payload that also said
exhausted: false, sending it down a recovery path that could
never work
apps/server/src/trading/TradingExecutionGuard.ts:61-71. A
SQL read that fails is not the budget saying no.
Ceilings in dollars, and they only tighten
The mandate's four ceilings are maximumLeverage, maximumGrossNotionalUsd,
maximumCumulativeLossUsd, and maximumPlannedRiskPerPositionUsd. Testnet defaults for
allocated capital C: 20x leverage, 8x C notional, 35% of C cumulative loss, 7% of C
per position, with the percentages written as integer math so a mandate never
carries a float artifact like 35.000000000000004
packages/trading-contracts/src/authority.ts:179-189.
An operator can move the four numbers without a rebuild through environment
variables: T3_TRADES_AUTHORITY_MAX_LEVERAGE,
T3_TRADES_AUTHORITY_MAX_GROSS_NOTIONAL_USD,
T3_TRADES_AUTHORITY_MAX_CUMULATIVE_LOSS_USD, and
T3_TRADES_AUTHORITY_MAX_PLANNED_RISK_USD
apps/server/src/trading/TestnetAuthority.ts:13-16. All
four are absolute values, not multiples of capital: an operator raising a ceiling is
thinking in dollars about a specific account, not in ratios
apps/server/src/trading/TestnetAuthority.ts:18-19. An
invalid value falls back to the default instead of preventing mission creation
apps/server/src/trading/TestnetAuthority.ts:30-35.
The knobs shape size and nothing else. Nothing here can grant direction reversal,
switch margin mode, or touch the risk policy: those are authority the user grants
explicitly, not a number in the environment
apps/server/src/trading/TestnetAuthority.ts:21-23. The
risk-policy page of this atlas covers where the mandate itself comes from; see
Signer & authority.
The gross cap is deliberately sized to what the loss budget can protect, not to what
the margin allows
packages/trading-contracts/src/authority.ts:163-167.
Stops that live on the exchange
The invariant
No acknowledged position increase may remain without confirmed exchange-native
reduce-only protection beyond the bounded reconciliation window
apps/server/src/trading/TradingProtectionService.ts:4-9. A
stop in T3 Trade is a real reduce-only trigger order resting on Hyperliquid, not a
local promise to act later.
Linked at birth
An increase and its stop go out in one normalTpsl action. The
child is sized to the requested size; reconciliation resizes it to the canonical
position
apps/server/src/trading/HyperliquidExecutionService.ts:626-634.
Independent replacement stops
When coverage is short after a partial fill, a scale-in, or a parent cancellation
that took its children, submitProtectiveStop places an
explicitly sized reduce-only stop with na grouping, so it is
nobody's child and outlives the parent
apps/server/src/trading/HyperliquidExecutionService.ts:143-160.
Place, then cancel
The replacement stop is confirmed before the old one is dropped. Overlapping
reduce-only protection is harmless because reduce-only orders cannot open exposure;
a gap between the cancel and the placement is exactly the window the invariant
forbids
apps/server/src/trading/TradingProtectionService.ts:23-28.
Coverage is read
A grouped response is not proof: the submitted child may be untriggered, rejected,
or already cancelled, so coverage is read back from
frontendOpenOrders every time
apps/server/src/trading/TradingProtectionService.ts:18-22.
Take-profit is a wake
The target is deliberately not an exchange-native order anymore;
reconcileTakeProtection withdraws take-profit orders that
earlier builds rested
apps/server/src/trading/TradingProtectionService.ts:33-40.
The watchdog
Every five seconds the reactor's protection guard re-reads coverage. When the window
closes uncovered it escalates straight to emergency close and wakes the harness
apps/server/src/trading/TradingMissionReactor.ts:2542-2563, cadence
apps/server/src/trading/TradingMissionReactor.ts:2888.
Emergency close follows a bounded sequence
The emergency close runs when full protection cannot be confirmed inside the
reconciliation window. §17.5 calls it a deterministic safety action, not a strategy
decision: it runs without the harness, in a fixed order, a fixed number of times, and
never waits for anyone to wake up and help
apps/server/src/trading/TradingEmergencyCloseService.ts:4-7.
The order is the specification, and each step exists because of the failure it prevents
apps/server/src/trading/TradingEmergencyCloseService.ts:9-32.
Block increases
Mark the mission blocked and block every position increase first; otherwise the thing being unwound can grow while it is being unwound.
Cancel non-reduce-only orders
A resting entry that fills mid-close re-opens the exposure just closed. Reduce-only orders are left alone: they are the protection.
Read fresh canonical position and BBO
The size closed must be the size that exists now, not the size that existed when the trouble started.
Submit a reduce-only marketable IOC
For exactly that size, priced off the side it will cross.
Reconcile fills and the remaining position
Canonical state answers what the attempt actually did.
Retry with a fresh read and the remaining size
At most three attempts in total
TradingEmergencyCloseService.ts:50-51. An IOC fills what
it can and cancels the rest, so a partial close is the expected case, not an error.
Still open: say exactly what is left
Keep the mission blocked, preserve whatever protection can be placed, and report the exact remaining size and reason.
The outcome is a narrow three-way union: flat, open, or unknown, with
remainingSize: null whenever the canonical size could not be
confirmed
apps/server/src/trading/TradingEmergencyCloseService.ts:63-117. One rendering, describeEmergencyCloseOutcome, is shared by
every caller so the three reactor call sites cannot disagree (RC04)
apps/server/src/trading/TradingEmergencyCloseService.ts:119-133. The full anatomy of an order's path to the exchange is on the
Execution page.
Seven controls that do not ask the model
None of the §14.7 controls may require a harness turn: a user who wants out of a
position must be able to get out while the provider process is dead, the session is
unreachable, or the model is mid-thought
apps/server/src/trading/TradingControlService.ts:2-8. The
risk-reducing controls therefore take the preview-free reduce-only path, which is safe
for a different reason: the exchange itself will not let a reduce-only order open or
extend a position
apps/server/src/trading/TradingControlService.ts:10-17.
pause
Block new entries, scale-ins, reversals, and re-entry. Protection stays live
TradingControlService.ts:144-145.
resume
Re-enable a paused mission. A mission blocked for cumulative loss is not resumable
here; that is §16.4 TradingControlService.ts:147-148.
cancelEntries
Cancel every resting position-increasing order, protecting any filled slice first.
The position itself is left alone
TradingControlService.ts:150-156.
reducePosition
25 / 50 / 75 / 100 percent of the canonical position, via a reduce-only IOC
TradingControlService.ts:158-161.
closePosition
Close the canonical position entirely
TradingControlService.ts:163-166.
revoke
End autonomous authority permanently, preserving any valid protection
TradingControlService.ts:168-169.
closeAndRevokeMission
Close every held market, then revoke: the one-click way out. Authority ends exactly
once, only after canonical flat is confirmed across the whole held set (RC01)
TradingControlService.ts:171-181.
What is not bypassed
The buttons bypass discretionary harness reasoning, never T3's safety boundary.
Every control still goes through the signer, the nonce lane, canonical
reconciliation, and, where it matters most, the protection reconciliation, so
cancelling entries cannot strip a partial fill of its stop
apps/server/src/trading/TradingControlService.ts:19-24.
An unconfirmed close says unknown
A submitted-but-unconfirmed close must report
CLOSE_OUTCOME_UNKNOWN: "Close outcome unknown: an order may
have executed; position could not be confirmed." Never a numeric size, never "Already
flat", never "Position closed"
apps/server/src/trading/TradingControlService.ts:78-84. A
reduce makes at most two bounded attempts before reporting back
apps/server/src/trading/TradingControlService.ts:227-228.
Each control action leaves a persistent result
Before RC06, the outcome of a risk-control press lived only in server logs: a dispatched
command proved the request was accepted, and the reactor's exchange work finished later
where nobody could see it
apps/server/src/persistence/Migrations/094_TradingControlResults.ts:9-12. The fix is the append-only trading_control_results table, one
row per applied control with the control name, a completed, failed, or unknown status,
the composed summary, per-market facts as JSON, and the sequence of the request event it
answers
apps/server/src/persistence/Migrations/094_TradingControlResults.ts:15-23. A trading.mission.control-result event rides the same stream
as the doorbell that tells the client to refetch.
apps/web/src/components/trading/useMissionControls.ts:136-145
apps/web/src/components/trading/useMissionControls.ts:68-69,
apps/web/src/components/trading/useMissionControls.ts:120-121
apps/web/src/components/trading/MissionStripBar.tsx:140
apps/server/src/persistence/Migrations/094_TradingControlResults.ts:19-23
The same honesty rule appears at every layer of this page: the budget states what is left, the guard states which side of the gate an action fell on, the emergency close states flat, open, or unknown, and the control strip states that no final result means the outcome is unknown. Where the screens live is covered in Web & desktop; the stories behind these rules are in Failure stories, and the invariants they protect are collected in Safety invariants.
Go deeper
Server, risk services
-
apps/server/src/trading/TradingPreviewService.tsthe §16.3 checklist and both orderings -
apps/server/src/trading/TradingExecutionGuard.tsexhaustion enforcement and the reduce-only exits -
apps/server/src/trading/TradingBudgetReader.tsassembles the §16.2 snapshot from reconciled tables -
apps/server/src/trading/TradingProtectionService.tsthe stop invariant and its repairs -
apps/server/src/trading/TradingEmergencyCloseService.tsthe bounded §17.5 unwinding -
apps/server/src/trading/TestnetAuthority.tsthe operator ceiling knobs
Contracts and clients
-
packages/trading-contracts/src/lossAccounting.tsthe six pure equations and the exhaustion permission set -
packages/trading-contracts/src/authority.tsmandate ceilings, defaults, and the risk policy schema -
apps/server/src/trading/TradingControlService.tsthe seven §14.7 controls -
apps/server/src/persistence/Migrations/094_TradingControlResults.tsdurable control outcomes (RC06) -
apps/web/src/components/trading/useMissionControls.tspress-to-result correlation and the 30-second timeout -
apps/server/src/trading/TradingMissionReactor.tsthe five-second watchdog family and the escalation
Neighbouring pages: where the reconciled budget inputs come from is Reconciliation; what the guards wrap is Execution; the wire types that carry all of it are Contracts; the missions these budgets bound are Missions watches wakes.