Risk Management Rules We Hard-Coded (and Why)
Our trading bot has 12 hard-coded risk rules that can't be overridden. No config flags. No "just this once" exceptions. Here's each rule, why it exists, and the incident that taught us to code it.
Uncomfortable truth: Risk management isn't about making more money. It's about surviving long enough to not blow up. These rules exist because we learned the hard way.
Why Hard-Coded?
We could make these configurable. A nice dashboard with sliders. "Adjust your risk tolerance!"
We won't. Here's why:
- Humans are bad at risk: When you're down 3% in a day, you want to "make it back." That's when you take stupid risks.
- Configurability = overrideable: If it's a setting, you'll change it. If it's code, you can't.
- Survival > optimization: These rules will cost us profitable trades. That's the point. We'd rather miss opportunities than blow up.
With that out of the way, here are the 12 rules.
Rule 1: Max 5% Per Trade
POSITION_SIZE = min(5% of portfolio, calculated_size)
Why: Even the highest conviction trades can go to zero. Crypto exchanges get hacked. Prediction markets resolve ambiguously. Black swans happen.
Incident: Day 3 of development, we tested with 25% positions. A single Polymarket trade went against us. Paper loss: -$250. That's 2.5% of the portfolio on one trade. We coded the 5% limit that afternoon.
Exception: None. Even 95% confidence LLM scores get 5% max.
Rule 2: Max 7% Total Exposure Per Asset
if total_exposure[asset] + new_position > 7%: reject()
Why: You can have multiple strategies trading the same asset. Mean reversion, momentum, RSI bounce — all firing on BTC at once. Without this rule, you're accidentally concentrated.
Incident: Week 2, the clustering incident. Three ETH positions opened within 90 minutes. Individually: 5% each. Combined: 15% exposure to ETH. When ETH dropped, we lost 3× what we should have. This rule prevents it from ever happening again.
Rule 3: Circuit Breaker After 3 Consecutive Losses
if consecutive_losses >= 3: pause_trading(), notify_human()
Why: Three consecutive losses usually means one of two things: (1) the regime changed and our strategies don't work anymore, or (2) there's a bug. Either way, we should stop and figure out what's wrong.
Incident: We had a bug in our RSI calculation that went unnoticed for 4 days. The bot took 7 losing trades in a row before we noticed. Would've been caught after 3 losses if this rule existed.
Reset: Requires manual review. Human must check logs, confirm no bugs, and explicitly resume trading.
Rule 4: Daily Drawdown Limit (4%)
if daily_drawdown >= 4%: halt_all_strategies(), cooldown_24h()
Why: If you're down 4% in a day, something is wrong. The market, your strategies, or your infrastructure. Keep trading and you'll make it worse.
Incident: During a volatility spike, our bot took 5 losing trades in 2 hours. Each trade was sized correctly. But the regime was chaotic — mean reversion got stopped out repeatedly. By the time we noticed, we were down 8% for the day. This rule would've stopped us at 4%.
Rule 5: 45-Minute Cooldown Per Asset
if last_trade[asset] < 45 minutes ago: reject()
Why: Prevents overtrading on the same asset. After you exit a position, wait. Let the market settle. Don't jump right back in.
Incident: We had a mean reversion bot that traded BTC 6 times in 2 hours. Each trade was small losses. The bot was essentially whipsawing — buying dips in a downtrend. The cooldown rule forces it to slow down.
Rule 6: LLM Confidence Threshold (65% Minimum)
if llm_confidence < 0.65: reject()
Why: The LLM can veto any trade. If confidence is below 65%, it should. Low confidence = the model sees contradictions it can't resolve.
Incident: Early version didn't have this threshold. The LLM scored a trade at 52% confidence. We took it anyway. Lost money. Now 65% is the hard floor.
Rule 7: Auto-Execute Only at 85%+ Confidence
if llm_confidence >= 0.85: auto_execute() else: queue_for_human_review()
Why: High confidence trades execute automatically. Medium confidence trades (65-84%) require human approval. This balances speed with oversight.
Incident: We had a 72% confidence trade that looked good but had a subtle flaw the LLM spotted. It queued for review. We caught the issue and rejected it. Without the queue, it would've executed automatically.
Rule 8: Regime-Based Strategy Suspension
if regime == "trending": suspend(mean_reversion_strategies) if regime == "ranging": suspend(momentum_strategies)
Why: Mean reversion loses in trending markets. Momentum loses in ranging markets. The regime detector suspends strategies that don't match current conditions.
Incident: Week 3, the market flipped to trending. Mean reversion would've taken 3 losing trades. The regime detector suspended it automatically. This rule saved us from bleeding.
Rule 9: Max 10% Crypto Exposure
if crypto_exposure > 10%: reject_new_crypto_positions()
Why: Crypto is correlated. When BTC drops, everything drops. We cap total crypto exposure to prevent accidental concentration.
Incident: We had 5% in BTC, 5% in ETH, 5% in SOL. Looked diversified. Then crypto crashed 20% in a day. All three positions moved together. Now we cap crypto at 10% total.
Rule 10: OI Squeeze Veto (Score ≥ 80)
if open_interest_squeeze_score >= 80: veto_long_positions()
Why: When open interest is extremely high and funding rates are elevated, the risk of a long squeeze (forced liquidations cascading) is too high. We don't add longs in these conditions.
Incident: We saw a setup that looked perfect. Then we checked OI: 95th percentile. Funding rates: extreme. We would've entered right before a 15% long squeeze. This rule would've vetoed it.
Rule 11: Polymarket Liquidity Minimum ($10K Volume)
if market_24h_volume < $10,000: reject()
Why: Thin Polymarket markets have wide bid-ask spreads. You lose 5-10% on entry and exit. Not worth it.
Incident: We tested a market with $3K volume. Entry slippage: 4%. Exit slippage: 3%. Even though we picked the right outcome, we lost money after costs. $10K minimum volume prevents this.
Rule 12: Ambiguous Resolution Veto (Polymarket)
if contract_wording_ambiguous: reject()
Why: Some Polymarket contracts have unclear resolution criteria. "Will the Fed announce a rate hike?" Announce how? FOMC statement? A speech? A leak? We reject these.
Incident: We've rejected 6 markets in 3 weeks due to ambiguous wording. One: "Will ETH hit $5K before May?" Which exchange? What if one exchange wicks but others don't? Our LLM classifier flags these automatically.
The Pattern
Look at these 12 rules. Every single one exists because something went wrong.
- Rule 1-2: Position sizing bugs
- Rule 3-4: Loss streaks and drawdowns
- Rule 5: Overtrading
- Rule 6-7: LLM confidence gaps
- Rule 8: Regime mismatch
- Rule 9: Correlated exposure
- Rule 10: OI squeeze risk
- Rule 11-12: Polymarket-specific gotchas
We didn't design these rules upfront. We earned them. Each one is a scar from a mistake we made in paper trading.
What We'll Add
These rules aren't final. We'll add more as we find new failure modes. Planned additions:
- Correlation veto: If new position is >0.7 correlated with existing positions, reduce size or reject.
- Event risk halt: Auto-pause trading during major events (FOMC, CPI, earnings) when volatility spikes.
- LLM disagreement veto: If primary LLM says "buy" and critic says "veto" twice in a row on the same asset, pause and review.
Risk management is iterative. You find bugs. You patch them. You survive.
The Bottom Line
These 12 rules will cost us profitable trades. We're okay with that.
Missing a profitable trade is annoying. Blowing up is permanent.
We're optimizing for survival. Everything else is secondary.
Follow the 90-Day Challenge
These 12 rules are active in our 90-day paper trading challenge. Every veto, every circuit breaker trigger, every rejection — logged publicly. See risk management in action.