Skip to content

Risk management

AlphaProve's engine owns sizing, leverage, and margin. Your strategy declares intent; the risk manager decides the actual position size and enforces limits. You never place raw orders.

Leverage & margin

  • Leverage is capped per subscription tier (Free 10×, Trader 50×, Pro 100×) and further by each instrument's exchange ceiling. If you request more than your tier allows, the run is clamped and the UI shows "requested N×, capped to M×".
  • Margin mode:
    • cross: all positions share account equity as collateral (liquidation when total equity ≤ total maintenance margin).
    • isolated: each position is backed by its own allocated margin; a loss on one position can't liquidate the rest.

Position sizing

Choose a sizing_method:

method how it sizes
risk_per_trade risk a fixed fraction of equity per trade, derived from the stop distance (falls back to percent_equity when no stop)
percent_equity a fixed notional fraction of equity per trade
fixed_notional a fixed USDT notional per trade
fixed_contracts a fixed quantity per trade
leverage_notional notional = equity × leverage × fraction × confidence: leverage drives size (margin/liquidation still bind)
leverage_risk risk-per-trade scaled by leverage: qty = (equity × risk% × leverage) / stop distance (falls back to leverage_notional with no stop)

Dynamic / adaptive risk

Risk per trade can adapt instead of staying a fixed constant. Three opt-in ways, in precedence order (most specific wins):

  1. Per-signal override: Signal(direction="LONG", risk_pct=0.02). Compute it however you like in evaluate(). The adaptive helpers make volatility-targeting one line: risk = adaptive.volatility_target_risk(ctx, base_pct=0.01, target_vol=0.02).
  2. Engine-declared in RISK: dynamic_risk: "drawdown_scaled" (cut risk as drawdown deepens) or "streak_scaled" (halve risk per consecutive loss). Optional dynamic_risk_floor_pct sets a hard floor (default: 25% of base). These use data only the engine sees (peak equity, closed trades). No evaluate() code needed.
  3. Fixed: the default risk_per_trade_pct.

risk_pct and dynamic_risk apply to the stop-based methods (risk_per_trade, leverage_risk); the notional methods ignore them.

Dynamic stops & take-profits

Stop/TP placement lives in your strategy (the engine doesn't place stops). The adaptive helpers cover the common cases so you never hand-roll the math, and the dispatchers let you write one default and switch modes without rewriting branches:

# one-line dispatchers — write the default mode; switch it with a one-token edit
stop = adaptive.resolve_stop(ctx, mode="atr", side="LONG", entry=entry)      # percent | atr
tp   = adaptive.rr_take_profit(entry, stop, rr=2.0, side="LONG")
risk = adaptive.resolve_risk(ctx, mode="volatility", base_pct=0.01)          # fixed | volatility | atr
return Signal("LONG", stop_loss=stop, take_profit=tp, risk_pct=risk)

You write the default; the catalog is what you (or the AI) pick from. All four axes are now dropdown-selectable from the Risk & sizing panel at submit time: the engine axes (sizing_method, dynamic_risk) via RISK/the run config, and the strategy axes (stop_mode, risk_mode) when the strategy declares them as enum PARAMS (see Editing risk per run below).

Stop management (already in the engine)

These run after entry and are managed by the engine. Set them on the Signal:

field what it does
trailing_stop_distance ratchets the stop toward price (never away). Use adaptive.atr_trail_distance(ctx, mult=2.0) for a volatility-adaptive trail.
breakeven_at_profit_pct moves the stop to entry once N% in profit (fires once).
max_hold_bars time-exits the position after N bars.

They compose freely with the sizing, risk-fraction, and stop-placement choices above.

Stops are optional

Signal(direction="LONG") with no stop_loss/take_profit is valid. Use it for exit-on-signal strategies. The risk layer still bounds the position via leverage caps, liquidation, and exposure/concentration limits. Supply stops only when you exit at a fixed level.

Kill-switches

  • Drawdown: set max_drawdown_pct (0 = off). Action is halt (block new entries) or flatten (force-close everything).
  • Daily loss: set daily_loss_limit_pct (calendar-day, UTC).

Declaring risk in a strategy

Python: an optional module-level RISK literal:

HISTORY = {"primary": 1}

RISK = {
    "account_leverage": 10,
    "margin_mode": "isolated",
    "sizing_method": "risk_per_trade",
    "risk_per_trade_pct": 0.01,
    "dynamic_risk": "drawdown_scaled",   # optional; "off" (default) | "drawdown_scaled" | "streak_scaled"
    "dynamic_risk_floor_pct": 0.0,       # optional hard floor (0.0 => implicit 25%-of-base floor)
}

def evaluate(ctx):
    # ctx.risk exposes the RESOLVED values (after run config + tier clamping)
    if ctx.risk is not None and ctx.risk.buying_power <= 0:
        return None
    ...

No-code: an optional top-level risk block in the JSON config:

{ "risk": { "account_leverage": 10, "margin_mode": "isolated", "sizing_method": "risk_per_trade", "risk_per_trade_pct": 0.01 } }

Anything you declare is a default: the run config you pick at submit time, then your tier's caps, take precedence. Reading the resolved values back is what ctx.risk is for.

Editing risk per run

Everything in RISK/PARAMS is a default. At submit time (in both chat and the library), a single Risk & sizing panel lets you review and override it for one backtest, no code changes:

  • Sizing & dynamic risk: pick a sizing_method or dynamic_risk from a dropdown and exactly that method's knobs appear (risk %, equity fraction, leverage, floor, …), pre-filled from the strategy's declared RISK.
  • Strategy parameters: any PARAMS the strategy declares (periods, multipliers, thresholds) are editable for this run only. Numeric params get an input; enum params get a dropdown.
  • Market, window, capital, leverage, fills: set here too, coverage-clamped to available data.

The panel reads a self-describing method catalog from the engine, so new methods and modes appear automatically with their knobs.

Switchable stop / risk modes

The stop and risk dispatchers become user-switchable when the strategy exposes their mode as an enum PARAM: a str PARAM with a choices list. The panel then renders a dropdown and you flip atr ↔ percent (or fixed ↔ volatility ↔ atr) per run, with no rewrite or regeneration:

HISTORY = {"primary": 100}  # covers the selectable ATR(14) stop mode

PARAMS = {
    "stop_mode": {"default": "atr", "choices": ["percent", "atr"]},
    "atr_mult":  {"default": 1.5},
}

def evaluate(ctx):
    stop = adaptive.resolve_stop(ctx, mode=ctx.params["stop_mode"], side="LONG",
                                 entry=entry, atr_mult=ctx.params["atr_mult"])
    return Signal("LONG", stop_loss=stop)

The same per-run override applies to any declared numeric PARAM: tune atr_mult for a single backtest without editing the strategy.

AI-set defaults

When you build a strategy in chat, the assistant proposes the run settings (symbol, timeframe, lookback, capital, leverage), which pre-fill the panel so you can adjust anything before running, and authors the switchable stop_mode/risk_mode PARAMS into the code whenever the spec names a parametric stop or risk, so the panel is useful immediately on a freshly generated strategy. A hand-written strategy that hardcodes a single mode isn't switchable until you expose that mode as a PARAM (as above).