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.

6 min readUpdated 3 August 2026risk, limits, kill-switch, emergency-stop, drawdown

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:

  1. 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_REACHED bot never ticks again.

  2. 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 of allocation/remove as well.

  3. Write LIMIT_REACHED and 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 startedAt rather than lastTickAt, 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 RUNNING row ERROR would 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

Flatten and stop every one of the caller's running or paused bots

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

Stop the entire fleet, then sweep every bot still holding anything

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:

  1. 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.
  2. 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.
  3. 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:

Force-stop one bot, closing its positions on any process

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.

Close every open position on one bot

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