Why Position Sizing Is the Most Critical Variable
Most agent developers spend 90% of their time on signal generation — the machine learning model, the market microstructure analysis, the pattern recognition engine — and less than 10% on position sizing. This is a catastrophic allocation of attention. The math is unambiguous: a mediocre signal with excellent sizing will outperform a superior signal with poor sizing over any sufficiently long time horizon.
Consider two agents. Agent A has a win rate of 55% with a 1:1 reward-to-risk ratio. Agent B has a win rate of 65% with the same ratio. If Agent A uses optimal Kelly sizing and Agent B bets a flat 25% of capital on every trade, Agent B will go broke while Agent A compounds steadily. The edge in the signal matters far less than the discipline in the sizing.
The three canonical sizing frameworks every trading agent should understand are:
- Kelly Criterion: Maximizes the long-run growth rate of capital. Mathematically optimal under certain assumptions, but aggressive and highly sensitive to edge estimation errors.
- Fixed Fractional: Bets a constant percentage of current equity per trade. Simple, robust, and forgiving of edge estimation errors. The workhorse for most production agents.
- Volatility-Adjusted (ATR-based): Scales position size inversely with market volatility. Ensures consistent dollar risk per trade regardless of how turbulent the instrument is behaving at any given moment.
Key Principle: Position sizing is not just about maximizing returns — it is primarily about surviving drawdowns long enough for your edge to manifest. A strategy with a positive expected value can still produce ruin if position sizes are not controlled.
The Gambler's Ruin Problem
Even with a positive edge, an agent betting too large relative to its bankroll will eventually hit a losing streak long enough to wipe out capital entirely. This is mathematically guaranteed — not probable, guaranteed — for any agent that bets above optimal Kelly. The only question is how many trades it takes. An agent risking 50% of capital per trade with a 60% win rate has roughly a 17% chance of hitting a 90% drawdown within 20 trades. An agent risking 2% faces roughly a 0.001% chance of the same outcome.
The practical implication: every sizing algorithm is in the business of buying time for the edge to work. The longer you survive, the more trials you accumulate, and the more certainly your positive expectation converts into realized profit.
Kelly Criterion: Formula and Implementation
The Kelly criterion was derived by John Kelly at Bell Labs in 1956, originally in the context of information theory and signal transmission. Its application to gambling and investing was recognized almost immediately, and it remains the gold standard for growth-optimal betting.
The Core Formula
For a bet with a binary outcome — win or lose — the Kelly fraction is:
For a trading context with variable win/loss amounts, the more general form is:
A Worked Example
Suppose your agent's strategy has the following historical statistics:
- Win rate: 58% (p = 0.58, q = 0.42)
- Average winning trade: $120 (W = 120)
- Average losing trade: $80 (L = 80)
- Reward-to-risk ratio: 120/80 = 1.5
Kelly fraction: f* = 0.58 - (0.42 / 1.5) = 0.58 - 0.28 = 0.30
The Kelly criterion says to bet 30% of your capital on this trade. Most practitioners apply a fractional Kelly of 0.25x to 0.5x Kelly to reduce variance — so 7.5% to 15% of capital per trade. This acknowledges that real-world edge estimates are noisy, and the cost of overestimating Kelly is asymmetric: overestimating Kelly causes ruin faster than underestimating Kelly costs returns.
Warning: Kelly sizing is only optimal when your edge estimate is accurate. In practice, run Kelly on your most conservative edge estimate and apply a fractional multiple (0.25 to 0.5). A half-Kelly strategy achieves about 75% of the growth rate of full Kelly with much smaller drawdowns.
Kelly for Continuous Returns
For strategies with continuous return distributions — not binary win/lose — the Kelly fraction generalizes to:
This formulation is more appropriate for agents trading in continuous markets. The mean and variance should be computed over a rolling window that reflects the agent's current performance regime, not a static historical average.
Fixed Fractional Sizing
Fixed fractional is the simplest and most robust sizing method. You define a maximum risk percentage per trade — typically 1% to 3% of current equity — and size the position so that hitting your stop-loss results in exactly that dollar loss.
The Mechanics
If your agent has $10,000 in equity and wants to risk 2% per trade ($200), and the trade has a stop-loss 5% away from entry, the position size is:
The elegance of fixed fractional is that it scales naturally with your equity. As you win, position sizes grow proportionally. As you lose, they shrink, providing natural protection against catastrophic drawdowns. You can never mathematically reach zero — you always retain a remaining fraction — assuming you respect the sizing rules exactly.
Choosing the Right Fraction
| Risk Fraction | Growth Expectation | Max Drawdown Risk | Best Suited For |
|---|---|---|---|
| 0.5% | Slow, stable | Very low | Conservative production agents |
| 1% | Moderate | Low | Standard production agents |
| 2% | Good | Moderate | Confident edge with solid data |
| 5% | High growth | High | Aggressive / well-backtested only |
| 10%+ | Speculative | Very high | Casino-style or single-bet scenarios |
For Purple Flea trading agents, the 1–2% range is recommended for algorithmic strategies. If your agent also uses the casino API for separate bets, treat those as entirely separate capital allocations and never let casino losses affect trading position sizing.
Volatility-Adjusted Sizing: ATR-Based
Fixed fractional treats all trades as equivalent in volatility terms — a trade with a 2% stop is sized the same as a trade with a 10% stop if you are targeting the same dollar risk. But in practice, markets have different volatility regimes, and a stop that is appropriate in a calm market may be too tight in a volatile one, leading to premature stop-outs and unnecessary losses.
ATR (Average True Range) provides a volatility-normalized measure of how much an instrument typically moves in a single period. By using ATR to set stops and size positions, your agent automatically adapts its exposure to market conditions without requiring manual parameter updates.
True Range and ATR Calculation
The True Range for a single period is the greatest of three values:
- Current high minus current low
- Absolute value of (current high minus previous close)
- Absolute value of (current low minus previous close)
ATR is the exponential moving average of True Range over a lookback window, typically 14 periods. For an ATR-based stop, multiply ATR by a multiplier (1.5 to 3.0) to get your stop distance:
Position size then follows fixed-fractional logic, but with an ATR-derived stop distance:
Why ATR-Based Sizing Outperforms Fixed Stop Sizes
During high-volatility periods, ATR widens and your position size automatically decreases — you take on fewer units when the market is more turbulent. During calm periods, ATR narrows and your position size increases, deploying more capital when risk is lower. This naturally produces a smoother equity curve than fixed-stop-size approaches because your dollar risk exposure stays consistent regardless of market regime.
Historical Context: The Turtle Traders pioneered ATR-based position sizing in the 1980s. Their N-based sizing produced remarkably consistent risk exposure across very different markets — crude oil, gold, currencies, and bonds — by normalizing for each market's inherent volatility. The same principle applies directly to agent trading across multiple crypto markets with wildly different volatility profiles.
Portfolio Heat and Maximum Concentration Limits
Individual trade sizing is only half the problem. Even if every trade is sized correctly in isolation, an agent that runs 20 correlated trades simultaneously may be taking on far more aggregate risk than intended. Portfolio heat measures the total open risk as a percentage of equity across all positions simultaneously.
Calculating Portfolio Heat
A typical constraint is to cap total portfolio heat at 15–25% of equity. This means that if every open stop is hit simultaneously — a worst-case scenario — you lose at most 15–25% of your account in a single wave. Combined with correlation limits, this prevents simultaneous loss cascades from correlated positions.
Correlation-Adjusted Heat
Two long positions in BTC and ETH are not independent. They typically have a correlation above 0.8 during normal markets and even higher during stress events. An agent should track not just raw heat but correlation-adjusted heat, which weights correlated positions more heavily in the aggregate risk calculation. The simplest practical approach: treat groups of correlated assets as a single position for heat calculation purposes.
| Portfolio Heat | Risk Profile | Worst-Case Loss | Recommendation |
|---|---|---|---|
| <10% | Conservative | <10% | Good for new or untested agents |
| 10–20% | Moderate | 10–20% | Standard production range |
| 20–35% | Aggressive | 20–35% | Requires strong edge confidence |
| >35% | Dangerous | Potential ruin event | Avoid in all production settings |
Maximum Concentration Limits
Beyond aggregate heat, impose per-asset and per-sector concentration limits. Even if total heat is within bounds, having 80% of heat concentrated in a single asset exposes you to idiosyncratic risk that diversification could have eliminated at no cost. Typical limits for a well-designed agent:
- Per-asset max: No more than 30–40% of total portfolio heat in a single instrument
- Per-sector max: No more than 50–60% of heat in correlated assets (e.g., all L1 chains)
- Direction max: Limit net long or net short bias to prevent catastrophic directional blowups
Python: PositionSizer Class
The following Python class implements all three sizing methods with portfolio heat tracking, maximum heat enforcement, and win-rate-based Kelly adaptation. It is designed to integrate directly with the Purple Flea trading API.
import numpy as np from dataclasses import dataclass from typing import Optional, List, Dict from collections import deque import logging logger = logging.getLogger('PositionSizer') @dataclass class TradeRecord: """Single completed trade result.""" pnl: float equity_before: float symbol: str side: str # 'long' or 'short' @dataclass class OpenPosition: """Currently open position for heat tracking.""" symbol: str side: str entry_price: float stop_price: float units: float dollar_risk: float # always positive class PositionSizer: """ Calculates position sizes using Kelly, fixed fractional, and volatility-adjusted (ATR) methods. Tracks portfolio heat, enforces concentration limits, and adapts Kelly fraction based on rolling win-rate. """ def __init__( self, equity: float, max_risk_pct: float = 0.02, max_portfolio_heat: float = 0.20, kelly_fraction: float = 0.25, max_concentration: float = 0.35, trade_history_len: int = 100, ): self.equity = equity self.max_risk_pct = max_risk_pct self.max_portfolio_heat = max_portfolio_heat self.kelly_fraction = kelly_fraction self.max_concentration = max_concentration self.trade_history: deque = deque(maxlen=trade_history_len) self.open_positions: Dict[str, OpenPosition] = {} # ── Core sizing methods ─────────────────────────────────────────── # def kelly( self, win_rate: Optional[float] = None, avg_win: Optional[float] = None, avg_loss: Optional[float] = None, ) -> float: """ Compute fractional Kelly position size as fraction of equity. Derives stats from trade_history if not provided. Returns 0.0 if insufficient data or no edge detected. """ if win_rate is None or avg_win is None or avg_loss is None: stats = self._compute_trade_stats() if stats is None: logger.warning('Insufficient trade history for Kelly') return 0.0 win_rate, avg_win, avg_loss = stats if avg_loss == 0: return 0.0 rr = avg_win / avg_loss q = 1.0 - win_rate full_kelly = win_rate - (q / rr) if full_kelly <= 0: logger.info(f'No edge detected (Kelly={full_kelly:.4f})') return 0.0 fractional = full_kelly * self.kelly_fraction fractional = min(fractional, self.max_risk_pct * 5) logger.info( f'Kelly: WR={win_rate:.2%} RR={rr:.2f} ' f'Full={full_kelly:.4f} Frac={fractional:.4f}' ) return fractional def fixed_fractional( self, entry_price: float, stop_price: float, custom_risk_pct: Optional[float] = None, ) -> Dict: """ Size position so stop hit = risk_pct loss of equity. Returns dict with units, dollar_risk, heat_after. Returns units=0 if position would breach heat limits. """ risk_pct = custom_risk_pct or self.max_risk_pct dollar_risk = self.equity * risk_pct stop_dist = abs(entry_price - stop_price) if stop_dist == 0: return {'units': 0, 'dollar_risk': 0, 'reason': 'zero stop distance'} units = dollar_risk / stop_dist heat_added = dollar_risk / self.equity current_heat = self.portfolio_heat() if current_heat + heat_added > self.max_portfolio_heat: remaining = self.max_portfolio_heat - current_heat if remaining <= 0: logger.warning('Portfolio at max heat; rejecting position') return {'units': 0, 'dollar_risk': 0, 'reason': 'max heat reached'} dollar_risk = self.equity * remaining units = dollar_risk / stop_dist heat_added = remaining return { 'units': units, 'dollar_risk': dollar_risk, 'heat_pct': heat_added, 'heat_after': current_heat + heat_added, 'position_value': units * entry_price, } def vol_adjusted( self, entry_price: float, atr: float, atr_multiplier: float = 2.0, custom_risk_pct: Optional[float] = None, ) -> Dict: """ATR-based volatility-adjusted position sizing.""" stop_price = entry_price - (atr * atr_multiplier) return self.fixed_fractional( entry_price=entry_price, stop_price=stop_price, custom_risk_pct=custom_risk_pct, ) # ── ATR helper ──────────────────────────────────────────────────── # @staticmethod def compute_atr( highs: List[float], lows: List[float], closes: List[float], period: int = 14, ) -> float: """Compute ATR using Wilder's smoothing (EMA alpha=1/period).""" if len(closes) < period + 1: raise ValueError(f'Need at least {period+1} candles') true_ranges = [] for i in range(1, len(closes)): tr = max( highs[i] - lows[i], abs(highs[i] - closes[i - 1]), abs(lows[i] - closes[i - 1]), ) true_ranges.append(tr) atr = sum(true_ranges[:period]) / period alpha = 1.0 / period for tr in true_ranges[period:]: atr = alpha * tr + (1 - alpha) * atr return atr # ── Portfolio heat tracking ──────────────────────────────────────── # def portfolio_heat(self) -> float: total = sum(p.dollar_risk for p in self.open_positions.values()) return total / self.equity if self.equity > 0 else 0.0 def add_position(self, pos: OpenPosition) -> None: self.open_positions[pos.symbol] = pos def close_position(self, symbol: str, pnl: float) -> None: if symbol in self.open_positions: pos = self.open_positions.pop(symbol) self.trade_history.append(TradeRecord( pnl=pnl, equity_before=self.equity, symbol=symbol, side=pos.side, )) self.equity += pnl # ── Statistics helpers ───────────────────────────────────────────── # def _compute_trade_stats(self): """Returns (win_rate, avg_win, avg_loss) or None if <10 trades.""" if len(self.trade_history) < 10: return None wins = [t.pnl for t in self.trade_history if t.pnl > 0] losses = [abs(t.pnl) for t in self.trade_history if t.pnl < 0] if not wins or not losses: return None return ( len(wins) / len(self.trade_history), sum(wins) / len(wins), sum(losses) / len(losses), ) def rolling_win_rate(self, window: int = 20) -> float: recent = list(self.trade_history)[-window:] if not recent: return 0.5 return sum(1 for t in recent if t.pnl > 0) / len(recent) def summary(self) -> Dict: stats = self._compute_trade_stats() return { 'equity': self.equity, 'portfolio_heat': self.portfolio_heat(), 'open_positions': len(self.open_positions), 'total_trades': len(self.trade_history), 'rolling_win_rate': self.rolling_win_rate(), 'kelly_recommended': self.kelly() if stats else None, }
Integration with Purple Flea Trading API
The Purple Flea trading API exposes a /v1/orders endpoint that accepts position size as a unit count. The cleanest integration pattern is to compute units via your PositionSizer and pass them directly as quantity in the order payload. The wallet API provides real-time equity data to keep the sizer synchronized with actual account balances.
import httpx import asyncio from position_sizer import PositionSizer, OpenPosition BASE_URL = 'https://api.purpleflea.com' API_KEY = 'pf_live_your_key_here' HEADERS = { 'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json', } async def get_wallet_equity(client: httpx.AsyncClient) -> float: resp = await client.get(f'{BASE_URL}/v1/wallet/balance', headers=HEADERS) resp.raise_for_status() return float(resp.json()['balance']['usd']) async def get_ohlcv(client: httpx.AsyncClient, symbol: str, limit: int = 30): resp = await client.get( f'{BASE_URL}/v1/market/ohlcv', params={'symbol': symbol, 'interval': '1h', 'limit': limit}, headers=HEADERS, ) resp.raise_for_status() return resp.json()['candles'] async def place_sized_order( client: httpx.AsyncClient, sizer: PositionSizer, symbol: str, side: str, entry_price: float, method: str = 'vol_adjusted', ) -> dict: """ Compute position size and place order on Purple Flea. Supported methods: 'vol_adjusted', 'kelly', 'fixed' """ sizer.equity = await get_wallet_equity(client) if method == 'vol_adjusted': candles = await get_ohlcv(client, symbol) highs = [c['high'] for c in candles] lows = [c['low'] for c in candles] closes = [c['close'] for c in candles] atr = PositionSizer.compute_atr(highs, lows, closes) sizing = sizer.vol_adjusted(entry_price=entry_price, atr=atr) elif method == 'kelly': kelly_f = sizer.kelly() dollar_pos = sizer.equity * kelly_f stop_pct = 0.05 # 5% stop for Kelly positions stop_price = entry_price * (1 - stop_pct if side == 'buy' else 1 + stop_pct) sizing = { 'units': dollar_pos / entry_price, 'dollar_risk': dollar_pos * stop_pct, 'stop_price': stop_price, } else: # fixed fractional stop_pct = 0.03 stop_price = entry_price * (1 - stop_pct if side == 'buy' else 1 + stop_pct) sizing = sizer.fixed_fractional(entry_price, stop_price) if sizing.get('units', 0) == 0: return {'status': 'skipped', 'reason': sizing.get('reason')} resp = await client.post( f'{BASE_URL}/v1/orders', json={ 'symbol': symbol, 'side': side, 'quantity': round(sizing['units'], 6), 'type': 'market', 'stop_loss': sizing.get('stop_price'), }, headers=HEADERS, ) resp.raise_for_status() stop_p = sizing.get('stop_price', entry_price * 0.97) sizer.add_position(OpenPosition( symbol=symbol, side='long' if side == 'buy' else 'short', entry_price=entry_price, stop_price=stop_p, units=sizing['units'], dollar_risk=sizing['dollar_risk'], )) return resp.json() async def main(): async with httpx.AsyncClient() as client: equity = await get_wallet_equity(client) sizer = PositionSizer(equity=equity, max_risk_pct=0.02) result = await place_sized_order( client, sizer, symbol='BTC-USD', side='buy', entry_price=85_000.0, method='vol_adjusted', ) print(f'Order: {result}') print(f'Portfolio heat: {sizer.portfolio_heat():.2%}') print(f'Summary: {sizer.summary()}') if __name__ == '__main__': asyncio.run(main())
Adapting Size Based on Win Rate via Wallet API
One of the most powerful features of a live trading agent is the ability to continuously adapt position sizing based on recent performance. A strategy performing at 65% win rate last month may have degraded to 48% today due to changed market conditions — and an agent that does not detect and respond to this degradation will overbet a deteriorating edge until it is ruined.
Rolling Performance Monitoring
By fetching closed trade history from the Purple Flea wallet API and loading it into the PositionSizer's trade history, the system automatically recalibrates Kelly recommendations and adaptive risk percentages based on recent real performance.
class AdaptivePositionSizer(PositionSizer): """ Extends PositionSizer with: - Dynamic risk % based on rolling win rate - Drawdown-triggered risk reduction - Performance-based Kelly multiplier scaling """ def __init__(self, equity, peak_equity=None, **kwargs): super().__init__(equity, **kwargs) self.peak_equity = peak_equity or equity self.base_risk_pct = kwargs.get('max_risk_pct', 0.02) def adaptive_risk_pct(self) -> float: """ Compute risk % dynamically from rolling win rate and drawdown. Floor: 0.25x base. Ceiling: 2x base. """ wr = self.rolling_win_rate(window=20) dd = (1.0 - self.equity / self.peak_equity) if self.peak_equity > 0 else 0 # Win rate scale: linear from 0.5x at WR=40% to 1.5x at WR=70% wr_mult = max(0.5, min(1.5, 1.0 + (wr - 0.55) * 3.33)) # Drawdown scale: ratchet down risk when underwater dd_mult = ( 0.25 if dd > 0.20 else 0.50 if dd > 0.10 else 0.75 if dd > 0.05 else 1.00 ) adjusted = self.base_risk_pct * wr_mult * dd_mult return max( self.base_risk_pct * 0.25, min(self.base_risk_pct * 2.0, adjusted) ) def update_peak(self): if self.equity > self.peak_equity: self.peak_equity = self.equity def fixed_fractional(self, entry_price, stop_price, custom_risk_pct=None): risk = custom_risk_pct or self.adaptive_risk_pct() return super().fixed_fractional(entry_price, stop_price, risk) async def sync_trade_history( client: httpx.AsyncClient, sizer: AdaptivePositionSizer, limit: int = 50, ) -> None: """Load recent closed trades from wallet API into sizer.""" resp = await client.get( f'{BASE_URL}/v1/wallet/transactions', params={'type': 'trade_pnl', 'limit': limit}, headers=HEADERS, ) resp.raise_for_status() for tx in resp.json()['transactions']: sizer.trade_history.append(TradeRecord( pnl=float(tx['amount']), equity_before=float(tx['balance_before']), symbol=tx.get('symbol', 'UNKNOWN'), side=tx.get('side', 'long'), )) sizer.equity = await get_wallet_equity(client) sizer.update_peak() print(f'WR={sizer.rolling_win_rate():.2%} | Adaptive risk={sizer.adaptive_risk_pct():.3%}')
Connect Your Agent to the Trading API
Register your agent to get API credentials, access real-time market data, and start placing sized orders against Purple Flea's trading infrastructure.
Register Your Agent Trading API DocsThe Risk of Overbetting: Why Agents Get Ruined
The mathematics of overbetting are brutal and non-intuitive. Most developers think in terms of expected value per trade, but the relevant quantity for long-term survival is the geometric mean return — the compound growth rate. Overbetting reduces the geometric mean even when it increases the arithmetic mean, and this distinction is where most trading agents make a fatal error.
The Arithmetic vs. Geometric Mean Trap
Consider a strategy with outcomes of +50% and -40% with equal probability. The arithmetic mean is +5%, which sounds profitable. But the geometric mean is sqrt(1.5 * 0.6) - 1 = sqrt(0.9) - 1 = approximately -5.1%. You are actually losing money in expected compound terms despite the positive arithmetic expectation. This is the core reason aggressive bettors go broke over time even with strategies that look profitable on a per-trade basis.
Kelly sizing prevents this by finding the bet size that maximizes the geometric mean. Betting above Kelly increases arithmetic expectation but destroys geometric growth. The agent appears to do better on individual trades while systematically destroying its compound wealth.
Consecutive Loss Probability and Drawdown Tables
With a 60% win rate (40% loss rate), consecutive losing streaks follow a geometric distribution. The table below shows what happens at different risk levels:
| Consecutive Losses | Probability | Drawdown at 2% risk | Drawdown at 25% risk |
|---|---|---|---|
| 3 in a row | 6.4% | 5.9% | 57.8% |
| 5 in a row | 1.0% | 9.6% | 76.3% |
| 8 in a row | 0.07% | 15.0% | 90.0% |
| 10 in a row | 0.01% | 18.3% | 94.4% |
A 1% probability event sounds rare. But if your agent makes 500 trades, it will encounter a 5-in-a-row losing streak roughly 5 times on average. At 25% risk per trade, each such streak destroys 76% of remaining capital. An agent that experiences two or three such streaks has effectively zero capital left — ruined — despite having a genuine 60% win rate edge.
Estimation Error Amplification
The problem is compounded by the reality that edge estimates are noisy. If you estimate your win rate as 60% when it is actually 52%, your Kelly calculation recommends dramatically more risk than is optimal for the true edge. The conservative rule: assume your measured edge is half the observed edge when setting live sizing. This provides a safety margin against estimation error and edge degradation over time.
Critical Warning: Never deploy an agent to live trading with position sizes derived from simulated or backtested win rates alone. Paper trade for a minimum of 50–100 live trades to obtain a credible win rate estimate before scaling up. The cost of this patience is a few weeks of smaller returns. The cost of skipping it can be permanent capital loss.
Circuit Breakers: Automated Protection
Production trading agents must implement hard circuit breakers that halt trading when drawdown thresholds are crossed. These are not optional guardrails — they are core risk management infrastructure that prevents a bad week from becoming a catastrophic month.
class CircuitBreaker: """Hard stops that override all position sizer decisions.""" def __init__( self, daily_loss_limit: float = 0.05, session_loss_limit: float = 0.10, consecutive_loss_limit: int = 6, max_drawdown_halt: float = 0.20, ): self.daily_loss_limit = daily_loss_limit self.session_loss_limit = session_loss_limit self.consecutive_loss_limit = consecutive_loss_limit self.max_drawdown_halt = max_drawdown_halt self.session_start = None self.day_start = None self.consecutive_losses = 0 self.halted = False self.halt_reason = None def check(self, equity: float, peak: float) -> bool: """Returns True if trading allowed, False if halted.""" if self.halted: return False if self.session_start is None: self.session_start = equity self.day_start = equity return True session_loss = (self.session_start - equity) / self.session_start day_loss = (self.day_start - equity) / self.day_start drawdown = (peak - equity) / peak if peak > 0 else 0 if day_loss >= self.daily_loss_limit: return self._halt(f'Daily loss limit: {day_loss:.2%}') if session_loss >= self.session_loss_limit: return self._halt(f'Session loss limit: {session_loss:.2%}') if drawdown >= self.max_drawdown_halt: return self._halt(f'Max drawdown: {drawdown:.2%}') if self.consecutive_losses >= self.consecutive_loss_limit: return self._halt(f'{self.consecutive_losses} consecutive losses') return True def record_trade(self, won: bool) -> None: self.consecutive_losses = 0 if won else self.consecutive_losses + 1 def _halt(self, reason: str) -> bool: self.halted = True self.halt_reason = reason logger.critical(f'CIRCUIT BREAKER: {reason}') return False
Putting It All Together
The full production position sizing stack for a Purple Flea trading agent connects all components in a coherent workflow:
- Bootstrap: On agent startup, fetch wallet balance and recent trade history via the wallet API. Initialize AdaptivePositionSizer and CircuitBreaker. Load real trade data so the rolling win rate is meaningful from the first live trade.
- Pre-signal gate: Call
circuit_breaker.check()before running signal computation. No need to analyze markets if trading is halted — this also prevents wasted inference compute. - Signal generation: Run entry logic. If a signal fires, determine entry price and select sizing method based on strategy type: vol_adjusted for standard trend trades, kelly for high-conviction breakouts, fixed for stable range trades.
- Size computation: Call the appropriate sizer method. Check returned
unitsandheat_after. Reject the trade if heat_after exceeds your limit or units is zero. - Order placement: Submit to
/v1/orderswith computed quantity and stop_loss. Register the OpenPosition in the sizer for heat tracking. - Position monitoring: Poll
/v1/positionsperiodically. When positions close, callsizer.close_position()with actual P&L to update trade history and rolling statistics. Callcircuit_breaker.record_trade()to update consecutive loss counter. - Adaptation loop: Every N trades, log
sizer.summary()and observe how Kelly recommendations and adaptive risk percentages shift. This creates a self-correcting feedback loop where sizing automatically responds to regime changes without human intervention.
Key Insight: Position sizing is a living system, not a static parameter. An agent that treats risk percentage as a fixed constant will be miscalibrated for most of its trading life. Adaptive sizing, tied to real performance data from the wallet API, is the difference between an agent that slowly grinds down and one that compounds steadily for years.
The Purple Flea trading API provides all the data primitives needed to run this stack: wallet balance for equity synchronization, transaction history for win rate calculation, OHLCV data for ATR computation, and a robust order endpoint that accepts quantity-based sizing. Agents that wire these together with a proper PositionSizer class have a structural advantage over agents using naive flat-bet or percentage-of-balance approaches.
Test Position Sizing with Zero-Risk Faucet Funds
New agents can claim free funds from the Purple Flea faucet and test position sizing algorithms in the live casino environment before risking real capital on trading strategies.
Claim Faucet Funds View Trading Docs