Risk management and the kill switch
The per-bot risk envelope, the platform-wide limits every bot is measured against, what happens when a limit trips, and the two emergency stops — the user's and the operator's.
Risk is enforced in two places that are easy to confuse. Every bot carries its own envelope, chosen by its owner. Over the top of that sit platform limits, chosen by you, that no user can exceed. A signal has to clear both before it becomes an order.
The per-bot envelope
Set at creation, editable afterwards (with the bot stopped for most of them).
| Field | What it caps | Applies to |
|---|---|---|
maxPositionSize |
Notional of a single entry | Entries only |
maxConcurrentTrades |
How many positions may be open at once | Entries only |
stopLossPercent |
Loss on one position before it is closed | Each trade |
takeProfitPercent |
Gain on one position before it is closed | Each trade |
dailyLossLimit |
Absolute loss in a UTC day | The whole bot |
dailyLossLimitPercent |
Loss in a UTC day as a share of the allocation | The whole bot |
maxDrawdownPercent |
Fall from peak equity | The whole bot |
cooldownSeconds |
Minimum gap between trades | Each trade |
A bot created without an explicit value inherits the operator's defaults — Default Stop Loss, Default Take Profit, Default Max Daily Loss, Default Max Drawdown, Default Cooldown and Max Concurrent Trades from the settings screen.
The daily-loss check runs only when dailyLossLimit or dailyLossLimitPercent is
set, and the drawdown check only when maxDrawdownPercent is set. A NULL skips
the check entirely, so a bot with empty limits can never reach LIMIT_REACHED
however much it loses.
Bots created before those defaults were applied were saved with both empty. If an older bot is losing money and never stopping, check its risk settings before assuming the check is broken.
An explicit 0 is honoured as a deliberate "no limit" — that is what the truthiness gate reads it as — so a bot you genuinely meant to run uncapped stays uncapped.
Platform limits
These are read fresh on every risk check, from one settings snapshot per check so the tests cannot disagree with each other mid-evaluation.
| Setting | Refuses |
|---|---|
| Minimum Trade Amount | Any order whose notional is below it — entry or exit |
| Maximum Trade Amount | Any entry whose notional is above it |
| Enable Market Orders | Every MARKET signal when off |
| Enable Limit Orders | Every LIMIT signal when off |
| Max Bots per User | Creating another bot |
| Max Active Bots per User | Starting or resuming another bot |
| Minimum Bot Allocation | An allocation below the floor (zero is still legal) |
A bot that legally accumulated ten entries of 3,000 each emits one exit worth
30,000. Testing that against a per-order ceiling of 10,000 would refuse it every
tick, forever: the position could never be closed by its own strategy, used
would stay pinned at the full notional so no new entry fits either, and the bot
would sit stalled holding inventory it cannot sell.
Reducing exposure is always allowed. Capping how much a bot may take on is a
different decision from trapping what it already holds. The same reasoning exempts
exits from maxConcurrentTrades.
Order of checks
Per tick, per bot, the engine asks two questions.
Should this bot be trading at all? Daily loss, then drawdown, then the
consecutive-error count (5 failures moves it to ERROR). A failure here trips the
bot, not just the trade.
Should this specific signal become an order? Order type, position size, concurrency, allocation headroom, the minimum notional, then the platform ceiling. A failure here refuses the trade and the bot keeps running — it is not an error and does not count towards the error limit.
What happens when a limit trips
A daily-loss or drawdown breach does three things, in this order:
-
Cancel every working order. A working sell rung is holding base inventory at the matching engine, so the exit below cannot be sized for the whole position while it stands. Nothing else would ever cancel them — a
LIMIT_REACHEDbot never ticks again. -
Flatten every open position. This happens before the status changes, and that ordering is the whole point: stop-loss and take-profit are only evaluated inside a tick, and a tick only runs while the bot is
RUNNING. Changing the status first would leave the position open with no stop enforcement at exactly the moment the limit exists to protect it, and its capital locked out ofallocation/removeas well. -
Write
LIMIT_REACHEDand notify the owner.
A market exit that is submitted is not yet closed — the trade rows close when the order fills, which may be a tick or two later.
To bring a bot back: review the strategy, adjust the limits if they were too tight, and start it again.
The stale-tick watchdog
A RUNNING bot that has not reported a tick for 5 minutes is moved to ERROR
with Bot stopped responding (stale tick) and its owner is notified.
Two details matter operationally:
- A bot that has never ticked is caught too. It is aged from
startedAtrather thanlastTickAt, because a bot the engine never adopted has no tick time at all — that is precisely the bot this detector exists for. - The watchdog stands down while the engine is not running here. During a
maintenance window or an engine outage no bot is expected to tick, so marking
every
RUNNINGrowERRORwould turn a ten-minute window into a fleet-wide incident. The consequence is that during an outage nothing marks anything, and the admin dashboard's stalled count is the only signal.
The user's kill switch
For each bot it flattens first, then stops — for the same reason a risk violation does. Stopping first would leave any open position with no stop enforcement and its capital reserved indefinitely: an emergency stop that abandons the position it was called to protect.
The stop itself goes through the real path, so working orders are cancelled on the
book for real and the committed capital is re-derived afterwards. That matters: an
earlier version marked the order rows cancelled locally while the ecosystem orders
stayed live, still holding the user's funds, and used stayed pinned at its
pre-kill figure so allocation/remove would pay out nothing.
The response says what actually happened:
| Field | Meaning |
|---|---|
requested |
Bots that were running or paused when the switch was pulled |
stoppedCount |
Bots now STOPPED |
positionsClosed |
Positions flattened across all of them |
warnings |
Bots that stopped but are still holding inventory that could not be closed |
failures |
Bots that could not be stopped at all |
If nothing could be stopped, the call fails rather than returning a cheerful zero.
Stopping is the one thing that must work when the platform is otherwise closed. A user who cannot get out of the market is worse off than one who cannot get in.
The operator's kill switch
A written reason of at least 10 characters is required, and it is recorded against every bot the switch stopped, so opening any one bot's history later shows why it stopped.
Three steps, in this order:
- The status update runs first, on whatever process answered the request, so an engine still ticking on another process stops re-adopting these bots at its next sync.
- The unwind is driven from the database, in batches, using the resident instance where this process has one and a transient instance built from the row where it does not. This is what makes the result independent of which worker served the request.
- The engine's own teardown runs last, so a resident engine still clears its tick loop and drops its instances — and anything a still-resident bot opened while the sweep was in flight is caught.
The sweep is deliberately not filtered by status. The damage it exists to
repair is a bot whose row says STOPPED while its position is still open, so
filtering on RUNNING/PAUSED would skip exactly the bots that need unwinding.
The response reports stoppedCount, flattenedCount, failedCount and a sample
of the failures.
An install-wide price outage can leave bots that were stopped but not flattened.
failedCount is the number that tells you the fleet is not actually flat. A
success message with a non-zero failure count means you have bots still holding
inventory, and you will need to chase them by hand.
Individual bots can be force-stopped one at a time:
Stopping is not liquidating
An ordinary stop cancels the bot's working orders. It does not liquidate
inventory unless the strategy is configured to (sellAllOnStop on a grid). A
stopped bot showing an open position is expected, not a fault.
To close it, use Close position in the terminal's dock — it works on a stopped bot — or an admin force-stop, which closes positions automatically and therefore never strands capital.
Next
- Admin console — the fleet health view and the stranded-capital figure
- Settings — where every platform limit above is set
- Troubleshooting — "insufficient allocated funds",
LIMIT_REACHED,ERROR