Risk controls

The hard risk limits — drawdown, daily loss, per-trade, position caps — and the kill-switch that enforce them before a strategy can lose real capital.

Risk controls are the fixed limits that decide whether a trade is allowed to open and when all trading must stop. They are checked in code at order-submit time and in the portfolio-risk loop, not left to a strategy's discretion. This page is the operator reference for what each limit is, its default, the range you can set it to, and — most importantly — how the limits interact so a single bad run cannot become a catastrophic one.

The live risk dashboard shows these controls as gauges; this page documents the controls themselves. Most of them only bite in live trading. In beta the app hard-locks to paper — publicly the "candidate" stage — so the limits still evaluate, but no real order is ever placed.

Forven is a research tool. Nothing here is a prediction, a recommendation, or financial advice. Risk controls cap downside; they do not create an edge, and no setting guarantees against loss. Every number below is an illustrative default, not a suggested value — tune it to your own risk budget.

The two enforcement points

Every risk control runs at one of two moments:

  1. At order submitcan_open() is the gate each new trade must pass. It checks the kill-switch state, the daily-loss halt, per-trade risk, the position cap, and the per-strategy cooldown gate. The check fails closed: if a limit is breached or cannot be evaluated, the order is rejected with a reason explaining which limit it hit (for example, that the requested risk exceeds the per-trade max). The optional risk/reward gate is applied earlier, when the signal is evaluated, rather than inside can_open().
  2. In the portfolio-risk loop — the equity update that runs every daemon tick (update_equity()) compares portfolio drawdown against max_drawdown_pct and same-day realized PnL against max_daily_loss_pct, tripping the kill-switch or the daily-loss halt when a threshold is crossed.

The first stops a bad trade before it opens. The second stops all trading after the portfolio as a whole has fallen too far.

The hard limits

These are the configurable caps. Set them in Settings → Trading; the next portfolio-risk check reads the new values through _get_risk_limits(). They are persisted in the KV store under forven:settings and also live in config.json.

ControlConfig keyDefaultRangeEnforced
Max drawdownmax_drawdown_pct101–30Portfolio loop → kill-switch
Max daily lossmax_daily_loss_pct50.5–10Portfolio loop → daily-loss halt
Max risk per trademax_risk_per_trade_pct20.5–10can_open() at submit
Max concurrent positions (live)max_concurrent_positionsunlimitedcan_open() against shared wallet
Max concurrent positions (paper)paper_max_concurrent_positionsunlimitedcan_open() per paper session
Min risk/rewardmin_risk_reward_ratio0 (off)Signal evaluation (scanner)
Cooldown after losscooldown_after_loss_hours0 (off)0–24can_open() per strategy

Defaults shown are illustrative. unlimited means the key is unset (null) and no cap applies — set an explicit number to enforce one.

Max drawdown — the kill-switch threshold

max_drawdown_pct is the deepest portfolio drawdown allowed before the kill-switch fires and force-closes everything. Drawdown is measured against the high-water mark (HWM) — the peak equity since the last reset — as:

drawdown = 1 - current_equity / high_water_mark

When that figure reaches max_drawdown_pct (default 10%, illustrative), the kill-switch trips. See The kill-switch below for what happens next.

Max daily loss — the daily halt

max_daily_loss_pct caps same-day realized loss as a percentage of initial_capital (default starting capital 10000, illustrative). When the day's realized PnL falls past the cap, daily_loss_halt is set and new trades are blocked for the rest of the current UTC day. Existing positions are not force-closed — this is a brake on opening more, not an emergency exit.

The halt is scoped to the current UTC day only. At UTC midnight the day's start-of-day equity snapshot resets and trading is allowed again, even if yesterday hit the limit. It also clears on a manual trading-halt reset (POST /api/system/trading/reset) or when the kill-switch is reset.

A legacy USD form, max_daily_loss, is still honoured but is overridden by max_daily_loss_pct when both are set.

Max risk per trade

max_risk_per_trade_pct (default 2%, illustrative) caps the risk any single order may carry, as a percentage of the account. can_open() enforces it at submit time; an order whose risk exceeds the cap is rejected with a reason noting the requested risk exceeds the per-trade max. On the risk dashboard, current_per_trade_risk reports the risk of your largest open position — when that gauge sits near the limit, you have little headroom for the next signal.

A legacy max_position_size_pct exists and is overridden by max_risk_per_trade_pct when both are set.

Position caps — and why live and paper differ

There are two separate caps because live and paper account for positions differently:

  • max_concurrent_positions applies to the shared live wallet. Live execution pools every active strategy's positions into one HyperLiquid wallet, so this cap is global — it counts all strategies together, not per-strategy.
  • paper_max_concurrent_positions applies per paper session. Paper sessions are isolated sandboxes with their own capital; the cap counts only that session's positions.

The risk dashboard reflects this split with two non-additive counts: open_positions (live) and open_positions_paper (paper).

Optional entry gates

Two gates are off by default and tighten which trades may open rather than how many:

  • min_risk_reward_ratio — rejects an entry whose risk/reward is below the threshold (for example 1.5 requires at least 1:1.5). Default 0 disables it. A tight value reduces trade frequency, so set it deliberately.
  • cooldown_after_loss_hours — blocks a strategy from reopening for N hours after it posts a loss. This is per-strategy: a different strategy can still trade while one is cooling down. Default 0 disables it.

Two more keys feed assumed costs into the risk math: risk_fee_bps and risk_slippage_bps (both default 0). They let position sizing account for fees and slippage when computing per-trade risk.

The kill-switch

The kill-switch is the drawdown circuit breaker — the control that converts "the portfolio is down too far" into "stop everything now." It is governed by kill_switch_enabled (default true); when false, drawdown is still computed but the switch will not trigger.

When portfolio drawdown reaches max_drawdown_pct, the switch fires automatically:

  1. The portfolio-risk loop (update_equity(), run each daemon tick) detects the breach and sets kill_switch_active = true.
  2. An emergency-close loop iterates over every open live position and submits a market-close for each.
  3. Each close attempt uses an escalating slippage cap so it still fills in a fast market — 300 bps on the first attempt, 600 bps, then 1000 bps on retries.
  4. If a position fails to close after three attempts, it is marked pending-close-reconcile (with the failure recorded under kill_switch_close_error in the trade's signal data) and left open for human review.
  5. Once the loop finishes, is_trading_allowed() returns false and no new positions open.
  6. Trading stays halted until an operator manually resets the switch.

A 429 rate-limit during the emergency close does not trip the trade circuit breaker; signed submits retry with bounded backoff (0.5s up to 4s) so the close path keeps working through rate-limit bursts. Only true outages (5xx, connection errors) open a breaker.

Resetting the kill-switch

Reset is an explicit operator action — there is no auto-reset. From the risk page button, the CLI, or the API:

# Reset the kill-switch (operator-only; the request must confirm the action)
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:8003/api/kill-switch/reset" `
  -ContentType "application/json" -Body '{"confirm": true}'

Resetting is destructive to drawdown tracking. The reset clears kill_switch_active and re-baselines the HWM to current equity. The next kill-switch threshold is then measured from the reset point, not the original peak. Reset after a 20% drawdown and a further 10% fall from there is roughly a 28% fall from the original peak before the switch fires again. Reset only once you have addressed the cause — never just to silence the alarm.

If the HWM is unset or zero, drawdown cannot be computed and the kill-switch will not fire. The HWM is synced from the account value at startup; if it is missing, that sync needs investigating before you rely on the switch.

The decay kill-switch (per-strategy)

Separate from the portfolio kill-switch, the risk-manager agent watches each live strategy's recent performance. If a strategy's 72-hour live Sharpe degrades past decay_kill_switch_pct (default 30%, illustrative) versus its walk-forward baseline, that single strategy is autonomously demoted back to the Test stage and its trading halts — the rest of the portfolio is untouched. This is how a strategy that decays out-of-sample retires itself instead of bleeding capital.

How the limits interact

The controls are layered, narrowest first, so a problem is caught at the smallest scope that can contain it:

  • Per tradecan_open() rejects an oversized or low-quality order before it ever opens.
  • Per strategy — cooldown-after-loss and the decay kill-switch pause or demote one misbehaving strategy without touching the others.
  • Per day — the daily-loss halt stops new trades once the day's realized loss budget is spent, then clears at UTC midnight.
  • Whole portfolio — the kill-switch force-closes everything when total drawdown breaches the cap, and stays latched until you reset it.

A trade must clear every applicable layer to open. Any one layer can halt activity on its own. The daily-loss halt is reversible by the clock; the kill-switch is latched and requires a deliberate, documented reset.

Configure the limits

Steps

  1. Open Settings and go to the Trading (Risk) section. The current values are shown.
  2. Set max_drawdown_pct (default 10, range 1–30) — the kill-switch threshold.
  3. Set max_daily_loss_pct (default 5, range 0.5–10), or the legacy max_daily_loss in USD.
  4. Set max_risk_per_trade_pct (default 2, range 0.5–10).
  5. Set max_concurrent_positions for the live wallet and paper_max_concurrent_positions for each paper session.
  6. Optionally enable min_risk_reward_ratio and cooldown_after_loss_hours.
  7. Save. Settings persist to the KV store (forven:settings); the next portfolio-risk check applies them.

What you'll see

After saving, the risk dashboard reflects the new caps under Risk limits (these are the limits, not current usage). The kill-switch threshold and daily-loss limit are active immediately on the next risk check — no restart needed.

Caveats

  • In beta, execution is paper-locked. Live-only controls (real-wallet drawdown, the kill-switch close loop, the position cap) populate but never place a real order until live trading is unlocked. See execution modes.
  • Paper PnL is not a proxy for live PnL. Paper trades fill at local candle prices, carry no exchange order ID, and are never reconciled against the exchange — paper drawdown is informative, not predictive.
  • The kill-switch reset re-baselines the HWM and is destructive to drawdown tracking; understand the consequence before resetting.
  • A failed emergency close (recorded as kill_switch_close_error and marked pending-close-reconcile) leaves a position open and needs manual intervention — it is not silently retried forever.
  • Stale pending-open trades have their risk slot freed after 180s (illustrative) so the position cap cannot deadlock on a never-filled order; if such a trade is genuinely open, manual review may be needed.