Strict Risk Configuration
Tight stops, stacked drawdown guards, proof that risk enforcement works
The clearest demonstration that Horizon’s risk layer actively bounds losses: run the same strategy twice, once with loose risk and once with strict risk, and compare max drawdown.
The file
# strict_risk.py
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticGBM
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.portfolio import KellyOptimizer
from horizon.quant import BollingerMeanRev, TSMomentum
from horizon.risk import (
DrawdownGuard,
OrderRisk,
RiskConfig,
StopLoss,
)
from horizon.risk.drawdown import DrawdownAction
SEED = 20250412
BASE_STRATEGIES = [
BollingerMeanRev(window=20, entry_z=2.0, edge_bps=60, horizon_days=3),
TSMomentum(lookback=20, edge_bps=50, horizon_days=5),
]
UNIVERSE = StaticUniverse([
Market(id=t, asset_class=AssetClass.Equity)
for t in ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"]
])
DATA = SyntheticGBM(
market_ids=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
n_bars=504,
mu=0.08,
sigma=0.22,
seed=SEED,
)
def run_loose():
risk = RiskConfig(
per_order=OrderRisk(max_order_notional_usd=100_000),
max_gross_leverage=2.0,
)
return hz.run(
mode="backtest",
strategies=BASE_STRATEGIES,
asset_classes=[Equity],
universe=UNIVERSE,
portfolio=KellyOptimizer(kelly_fraction=0.30, max_gross_leverage=2.0),
risk=risk,
data_source=DATA,
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
def run_strict():
risk = RiskConfig(
per_order=OrderRisk(max_order_notional_usd=25_000),
max_gross_leverage=1.0,
stop_loss=StopLoss(per_position_pct=0.04, trailing_pct=0.03),
drawdown=[
DrawdownGuard(daily_pct=0.03, action=DrawdownAction.HaltNew),
DrawdownGuard(weekly_pct=0.06, action=DrawdownAction.ReduceHalf),
DrawdownGuard(monthly_pct=0.12, action=DrawdownAction.Flatten),
],
)
return hz.run(
mode="backtest",
strategies=BASE_STRATEGIES,
asset_classes=[Equity],
universe=UNIVERSE,
portfolio=KellyOptimizer(kelly_fraction=0.20, max_gross_leverage=1.0),
risk=risk,
data_source=DATA,
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
def main():
loose = run_loose()
strict = run_strict()
print("=" * 60)
print("STRICT vs LOOSE RISK. same strategies, same data")
print("=" * 60)
print(f"{'':20} {'Loose':>15} {'Strict':>15}")
print("-" * 60)
print(f"{'Trades':20} {loose.n_trades:>15} {strict.n_trades:>15}")
print(f"{'Total return':20} {loose.total_return:>14.2%} {strict.total_return:>14.2%}")
print(f"{'Sharpe':20} {loose.sharpe:>14.3f} {strict.sharpe:>14.3f}")
print(f"{'Max drawdown':20} {loose.max_drawdown:>14.2%} {strict.max_drawdown:>14.2%}")
assert strict.max_drawdown < loose.max_drawdown, (
"Strict risk did NOT reduce max drawdown. risk layer is broken"
)
reduction_pct = (loose.max_drawdown - strict.max_drawdown) / loose.max_drawdown
print()
print(f"✓ Strict config reduced max DD by {reduction_pct:.1%}")
if __name__ == "__main__":
main()
Expected output
============================================================
STRICT vs LOOSE RISK. same strategies, same data
============================================================
Loose Strict
------------------------------------------------------------
Trades 1464 777
Total return -35.94% -34.73%
Sharpe -0.458 -3.848
Max drawdown 53.48% 34.73%
✓ Strict config reduced max DD by 35.1%
(Numbers may vary slightly; the key invariant is strict.max_drawdown < loose.max_drawdown.)
Why the strict config wins on drawdown
Smaller positions
kelly_fraction=0.20 and max_gross_leverage=1.0 instead of 0.30 and 2.0. Every position is smaller, so every loss is smaller.Tighter stops
Stacked drawdown guards
Smaller per-order notional
The assertion
assert strict.max_drawdown < loose.max_drawdown
This is the invariant: stricter risk config must produce a lower max drawdown on the same data. If it doesn’t, the risk engine has a bug. This is tested explicitly in tests/test_behavioral_audit.py.
Why Sharpe gets worse
Stricter risk often lowers Sharpe because:
- Stops close winning trades that would have recovered
- Drawdown guards halt entries during productive periods
- Smaller sizes mean smaller absolute gains
This is the risk/return tradeoff in action. Strict risk gives you lower drawdown and lower Sharpe. Loose risk gives you higher Sharpe and higher drawdown. Pick based on your risk tolerance.
For production, the typical answer is: use moderate risk, not either extreme. RiskProfile.moderate() is a reasonable default.
Run it
PYTHONPATH=. python3 strict_risk.py