Risk Management¶
The risk and state tutorial runs a stop, two portfolio reductions, and the recovery between breaches from one declared price panel.
ml4t-backtest has two levels of risk management: position rules (per-position exits) and portfolio limits (portfolio-wide constraints). Position rules are the primary tool -- they automatically evaluate on every bar and generate exit orders when triggered.
Rule snippets below assume a strategy or broker with current positions and prices. The linked risk tutorial runs the full event sequence and inspects reduction fills.
Position Rules¶
StopLoss¶
Exit when loss exceeds a threshold:
- Long positions: triggers if
bar_low <= entry_price * (1 - pct) - Short positions: triggers if
bar_high >= entry_price * (1 + pct) - With the default
STOP_PRICEfill mode, a gap through the stop fills at the bar open; other modes use their configured fill rule
TakeProfit¶
Exit when profit reaches a target:
- Long positions: triggers if
bar_high >= entry_price * (1 + pct) - Short positions: triggers if
bar_low <= entry_price * (1 - pct)
TrailingStop¶
Exit when price retraces from the high water mark:
- Tracks the highest price since entry (longs) or lowest price since entry (shorts)
- Triggers when price retraces by
pctfrom the water mark - Water mark behavior is configurable via
trail_hwm_source,initial_hwm_source, andtrail_stop_timingin BacktestConfig
See Execution Semantics for the full details on trailing stop timing modes.
TimeExit¶
Exit after holding for a maximum number of bars:
VolatilityStop¶
Exit when price moves against a position by a multiple of Average True Range (ATR).
After a position exists, supply a positive atr value through
broker.update_position_context(asset, {"atr": current_atr}). Without it, the rule holds:
from ml4t.backtest.risk.position import VolatilityStop
rule = VolatilityStop(
multiplier=2.0, # Stop distance is twice the supplied ATR
)
TighteningTrailingStop¶
Trail that tightens as profit increases:
from ml4t.backtest.risk.position import TighteningTrailingStop
rule = TighteningTrailingStop(
schedule=[
(0.00, 0.05), # Before +5% profit, trail 5%
(0.05, 0.03), # At +5% profit, trail 3%
(0.10, 0.02), # At +10% profit, trail 2%
(0.20, 0.01), # At +20% profit, trail 1%
],
)
ScaledExit¶
Take partial profits at predefined levels:
from ml4t.backtest.risk.position import ScaledExit
rule = ScaledExit(
targets=[
(0.10, 0.5), # At +10%, exit 50% of current position
(0.20, 1.0), # At +20%, exit the remaining position
],
)
ScaledExit tracks triggered targets on the rule instance. Use a separate instance for each
position or reset it when a position closes.
SignalExit¶
Exit based on a signal value in the position's context:
from ml4t.backtest.risk.position import SignalExit
rule = SignalExit(threshold=0.3) # Exit when signal drops below 0.3
Composing Rules¶
RuleChain (First Trigger Wins)¶
The most common pattern -- rules evaluate in order, first non-HOLD action triggers:
from ml4t.backtest import RuleChain, StopLoss, TakeProfit, TrailingStop
rules = RuleChain([
StopLoss(pct=0.05), # Highest priority: hard stop at -5%
TakeProfit(pct=0.20), # Take profit at +20%
TrailingStop(pct=0.03), # Trail 3% from peak
])
AllOf (All Must Agree)¶
Exit only when multiple conditions are true simultaneously:
from ml4t.backtest.risk.position import AllOf, TakeProfit, TimeExit
# Exit at or above breakeven after at least five bars
rule = AllOf([
TakeProfit(pct=0.0), # At or above breakeven
TimeExit(max_bars=5), # Must have held 5+ bars
])
AnyOf (Any Trigger Wins)¶
Semantically equivalent to RuleChain, but named for clarity when composing:
from ml4t.backtest.risk.position import AnyOf
rule = AnyOf([
StopLoss(pct=0.05),
SignalExit(threshold=0.3),
])
Nested Composition¶
Combine composition patterns for complex logic:
rules = RuleChain([
StopLoss(pct=0.08), # Hard stop always applies
AllOf([TakeProfit(pct=0.0), TimeExit(max_bars=5)]), # Breakeven + held 5 bars
TrailingStop(pct=0.03), # Trail from peak
TimeExit(max_bars=60), # Max hold 60 bars
])
Setting Rules¶
Global Rules¶
Apply to all positions:
class MyStrategy(Strategy):
def on_start(self, broker):
broker.set_position_rules(RuleChain([
StopLoss(pct=0.05),
TrailingStop(pct=0.03),
]))
Per-Asset Rules¶
Override rules for specific assets:
def on_start(self, broker):
# Global default
broker.set_position_rules(StopLoss(pct=0.05))
# Override for volatile assets
broker.set_position_rules(
RuleChain([StopLoss(pct=0.10), TrailingStop(pct=0.05)]),
asset="TSLA",
)
Per-asset rules take precedence over global rules for that asset.
Portfolio Limits¶
Portfolio limits operate at the portfolio level, not per-position. They check aggregate metrics (drawdown, exposure, position count) and can warn, reduce positions, or halt trading.
Import from ml4t.backtest.risk.portfolio.limits:
MaxDrawdownLimit¶
from ml4t.backtest.risk.portfolio.limits import MaxDrawdownLimit
limit = MaxDrawdownLimit(
max_drawdown=0.20, # Liquidate at -20% drawdown
warn_threshold=0.15, # Warn at -15%
)
MaxPositionsLimit¶
from ml4t.backtest.risk.portfolio.limits import MaxPositionsLimit
limit = MaxPositionsLimit(max_positions=10)
MaxExposureLimit¶
from ml4t.backtest.risk.portfolio.limits import MaxExposureLimit
limit = MaxExposureLimit(max_exposure_pct=0.10) # Warn above 10% in one asset
DailyLossLimit¶
from ml4t.backtest.risk.portfolio.limits import DailyLossLimit
limit = DailyLossLimit(max_daily_loss_pct=0.03) # Liquidate above 3% daily loss
GrossExposureLimit / NetExposureLimit¶
from ml4t.backtest.risk.portfolio.limits import GrossExposureLimit, NetExposureLimit
gross = GrossExposureLimit(max_gross_exposure=1.5) # Halt above 150% gross
net = NetExposureLimit(min_net_exposure=-0.2, max_net_exposure=1.2) # Warn outside range
VaRLimit / CVaRLimit¶
These checks require at least lookback_days of portfolio returns in the risk manager's
context["historical_returns"]. Without that input they report no breach.
from ml4t.backtest.risk.portfolio.limits import VaRLimit, CVaRLimit
var_limit = VaRLimit(threshold=0.05, confidence_level=0.95)
cvar_limit = CVaRLimit(threshold=0.08, confidence_level=0.95)
BetaLimit¶
SectorExposureLimit / FactorExposureLimit¶
Sector checks require an asset-to-sector mapping in context["asset_sectors"].
Factor checks require an asset-to-loading mapping in context["factor_loadings"];
the example names that factor momentum. Pass these mappings as the context argument to
RiskManager.update(...). Without them the checks report no breach.
from ml4t.backtest.risk.portfolio.limits import SectorExposureLimit, FactorExposureLimit
sector = SectorExposureLimit(max_sector_exposure=0.30)
factor = FactorExposureLimit(factor_name="momentum", max_exposure=0.50)
Limit Actions¶
Each limit check returns a LimitResult with an action:
| Action | Meaning |
|---|---|
none |
No breach |
warn |
Log warning, continue trading |
reduce |
Reduce open positions by the configured fraction; currently supported by MaxDrawdownLimit |
halt |
Stop opening new positions |
liquidate |
Flatten open positions and stop new trading |
For a drawdown reduction, set both action="reduce" and an explicit
reduction_pct in (0, 1]. The manager cancels pending orders and submits one
risk-tagged reduction order per open position. It acts once while that limit
remains breached and can act again after the breach clears. Other built-in
limits reject action="reduce" at construction until they have a defined
reduction policy.
For example, MaxDrawdownLimit(max_drawdown=0.10, action="reduce",
reduction_pct=0.50) cuts each open position by half after a 10% drawdown.
Pass the broker into RiskManager.update(...) so reduction and liquidation
actions are applied through normal order execution:
results = risk_manager.update(
equity=broker.get_account_value(),
positions={asset: pos.market_value for asset, pos in broker.positions.items()},
timestamp=timestamp,
broker=broker,
)
if risk_manager.is_halted:
return
In the book¶
Chapter 19, Section 19.4, ml4t-backtest risk demo applies position rules and portfolio limits. The exit strategies notebook compares stop choices.
Next Steps¶
- Book Guide -- risk-management chapter and case-study mapping
- Execution Semantics -- stop fill modes and trailing stop timing
- Strategies -- integrating risk rules into strategies
- Configuration -- stop-related config parameters