Guide

Risk Limits and Position Sizing for Autonomous AI Agents

Complete framework for AI agent risk management — Kelly criterion, maximum drawdown limits, concentration limits, correlation-adjusted sizing, and circuit breakers.

8+
Risk Frameworks
50%
Max Drawdown Limit
Kelly
Optimal Sizing Math

Purple Flea · March 6, 2026 · 18 min read

1. Why Risk Limits Matter for Autonomous Agents

Autonomous AI agents operating in financial markets face a unique challenge: unlike human traders who instinctively pause when things go wrong, agents will mechanically execute strategy after strategy unless explicitly constrained. A poorly designed agent can blow up an entire account in minutes during a volatile market event.

Risk limits serve as the constitution of an agent's financial behavior. They define the rules the agent must follow regardless of what its trading signals say, creating a separation between the strategy layer (what to trade) and the risk layer (how much to risk). This separation is not optional — it is the difference between a sustainable agent and one that survives for a single session.

Core principle: Risk limits should be defined at agent initialization and treated as immutable constraints that override all strategy signals. They are not parameters to optimize — they are guardrails.

The Purple Flea platform surfaces risk metrics via the trading and wallet APIs, giving agents the raw data they need to enforce these limits programmatically. Agents that register a wallet, claim faucet funds, and begin trading should implement all eight frameworks described in this guide before touching live capital.

The Ruin Problem

Financial ruin is absorbing — once an agent reaches zero capital, it cannot recover. Even a strategy with a positive expected value can lead to ruin if position sizes are too large. This is the gambler's ruin problem applied to trading, and Kelly criterion directly addresses it by finding the optimal fraction of capital to risk on each bet.

Consider an agent with a 60% win rate and 1:1 payoff. If it risks 100% of capital on each trade, it has a 40% chance of ruin on the first trade. If it risks 20% (near Kelly), it can absorb many consecutive losses before approaching ruin. Risk limits translate this mathematical insight into operational constraints.

2. Kelly Criterion Math and Variants

The Kelly criterion, developed by John L. Kelly Jr. in 1956 at Bell Labs, gives the mathematically optimal fraction of capital to wager to maximize long-run growth rate. For a binary bet:

f* = (p × b - q) / b

Where f* is the Kelly fraction, p is probability of winning, q = 1 - p is probability of losing, and b is the net odds received (profit per unit risked).

For trading with variable payoffs, the generalized Kelly formula is:

f* = (E[R]) / (E[R²])

Where E[R] is expected return and E[R²] is the expected value of the squared return, which approximates to μ / σ² for normally distributed returns.

Half-Kelly and Quarter-Kelly

In practice, full Kelly is rarely used because it assumes perfect knowledge of true probabilities, which is never available. Estimation error in p causes over-betting that dramatically increases volatility. The pragmatic variants are:

VariantFractionGrowth RateVolatilityUse Case
Full Kellyf*MaximumVery HighTheoretical optimum only
Half-Kellyf*/275% of maxLowConfident strategies
Quarter-Kellyf*/456% of maxVery LowNew or uncertain strategies
Fractional Kellyf* × cVariesAdjustableGeneral recommendation

For autonomous agents, quarter-Kelly is a conservative starting point during initial deployment. As the agent accumulates live performance data and validates its probability estimates, it can graduate to half-Kelly.

import numpy as np
from typing import Optional

class KellyCriterion:
    """Kelly criterion position sizing with safety bounds."""

    def __init__(self, kelly_fraction: float = 0.25, max_position: float = 0.10):
        """
        Args:
            kelly_fraction: Multiplier for Kelly (0.25 = quarter-Kelly)
            max_position: Hard cap on position size as fraction of capital
        """
        self.kelly_fraction = kelly_fraction
        self.max_position = max_position

    def size_binary(self, p_win: float, odds: float) -> float:
        """
        Kelly sizing for binary outcomes (e.g., casino bets, directional trades).

        Args:
            p_win: Estimated probability of winning
            odds: Net profit per unit risked (e.g., 1.0 for 1:1)

        Returns:
            Fraction of capital to allocate
        """
        if p_win <= 0 or odds <= 0:
            return 0.0
        q = 1 - p_win
        full_kelly = (p_win * odds - q) / odds
        if full_kelly <= 0:
            return 0.0  # Negative edge — skip trade
        sized = full_kelly * self.kelly_fraction
        return min(sized, self.max_position)

    def size_continuous(
        self,
        returns_history: list[float],
        expected_return: Optional[float] = None
    ) -> float:
        """
        Kelly sizing from historical return distribution.

        Args:
            returns_history: List of historical returns as decimals
            expected_return: Override expected return (uses historical mean if None)

        Returns:
            Fraction of capital to allocate
        """
        if len(returns_history) < 30:
            return 0.0  # Insufficient data

        r = np.array(returns_history)
        mu = expected_return if expected_return is not None else np.mean(r)
        sigma_sq = np.var(r)

        if sigma_sq <= 0 or mu <= 0:
            return 0.0

        full_kelly = mu / sigma_sq
        sized = full_kelly * self.kelly_fraction
        return min(sized, self.max_position)

    def size_with_confidence(
        self,
        p_win: float,
        odds: float,
        confidence: float
    ) -> float:
        """Kelly adjusted for model confidence (0-1 scale)."""
        base_size = self.size_binary(p_win, odds)
        return base_size * confidence


# Example usage
kelly = KellyCriterion(kelly_fraction=0.25, max_position=0.05)

# Binary bet: 60% win rate, 1:1 odds
size = kelly.size_binary(p_win=0.60, odds=1.0)
print(f"Recommended position size: {size:.2%}")  # ~5.0% (quarter-Kelly of 20%)

# Continuous returns (30+ observations)
historical_returns = [0.02, -0.01, 0.03, 0.01, -0.02, 0.04, -0.01, 0.02] * 10
size = kelly.size_continuous(historical_returns)
print(f"Continuous Kelly size: {size:.2%}")

3. Maximum Drawdown Thresholds

Maximum drawdown (MDD) is the largest peak-to-trough decline in portfolio value. It represents the worst-case loss an investor would have experienced if they had entered at the peak and exited at the trough. For AI agents, MDD serves as the primary kill switch — if the agent hits a predefined drawdown limit, it pauses trading.

MDD = (Trough Value - Peak Value) / Peak Value

Hierarchical Drawdown Limits

Effective risk frameworks use multiple drawdown tiers, each triggering progressively stronger responses:

TierDrawdownActionRecovery Condition
Yellow Alert10%Reduce position sizes by 50%DD recovers to 5%
Orange Alert20%Close all new positions, flatten to cashManual review required
Red Alert35%Full stop, liquidate all positionsStrategy reset + reapproval
Hard Stop50%Irrecoverable — agent decommissionedNew agent deployment

The specific thresholds depend on the strategy's expected volatility. A high-frequency arbitrage strategy should have much tighter limits (5%/10%/15%) than a trend-following strategy designed to weather significant drawdowns (15%/25%/40%).

Drawdown Recovery Time

A drawdown of 50% requires a 100% gain to recover. This asymmetry is why lower limits are more conservative than they appear. An agent that has lost 20% now needs 25% gains just to return to breakeven — often with a damaged strategy that produced the drawdown in the first place.

Asymmetry warning: A 30% drawdown requires a 43% gain to recover. A 50% drawdown requires 100%. A 75% drawdown requires 300%. Set limits conservatively.

class DrawdownMonitor:
    """Real-time drawdown monitoring with tiered alerts."""

    def __init__(
        self,
        yellow_threshold: float = 0.10,
        orange_threshold: float = 0.20,
        red_threshold: float = 0.35,
        hard_stop: float = 0.50
    ):
        self.thresholds = {
            'yellow': yellow_threshold,
            'orange': orange_threshold,
            'red': red_threshold,
            'hard_stop': hard_stop
        }
        self.peak_value: float = 0.0
        self.current_value: float = 0.0
        self.alert_level: str = 'none'

    def update(self, portfolio_value: float) -> dict:
        """
        Update peak and calculate current drawdown.

        Returns:
            Status dict with drawdown level and recommended action
        """
        self.current_value = portfolio_value
        if portfolio_value > self.peak_value:
            self.peak_value = portfolio_value

        if self.peak_value == 0:
            return {'drawdown': 0.0, 'alert': 'none', 'action': 'continue'}

        drawdown = (self.peak_value - self.current_value) / self.peak_value
        alert, action = self._evaluate(drawdown)
        self.alert_level = alert

        return {
            'drawdown': drawdown,
            'peak': self.peak_value,
            'current': self.current_value,
            'alert': alert,
            'action': action
        }

    def _evaluate(self, drawdown: float) -> tuple[str, str]:
        if drawdown >= self.thresholds['hard_stop']:
            return 'hard_stop', 'decommission'
        elif drawdown >= self.thresholds['red']:
            return 'red', 'liquidate_all'
        elif drawdown >= self.thresholds['orange']:
            return 'orange', 'halt_new_positions'
        elif drawdown >= self.thresholds['yellow']:
            return 'yellow', 'reduce_sizing_50pct'
        return 'none', 'continue'

    def get_size_multiplier(self) -> float:
        """Return the position size multiplier based on current alert level."""
        multipliers = {
            'none': 1.0,
            'yellow': 0.5,
            'orange': 0.0,
            'red': 0.0,
            'hard_stop': 0.0
        }
        return multipliers.get(self.alert_level, 0.0)

4. Position Concentration Limits

Concentration limits prevent any single position from dominating portfolio risk. Even with excellent Kelly sizing, a correlated cluster of positions can create de facto concentration that limits pretend to prevent. Concentration must be measured at multiple levels: individual asset, sector, strategy, and counterparty.

Individual Position Limits

Common concentration limits for autonomous agents:

  • Max single position: 5-10% of total capital (lower for illiquid or volatile assets)
  • Max single sector: 25-30% of total capital
  • Max single counterparty: 20% (e.g., max exposure to one exchange or protocol)
  • Max correlated cluster: 40% (assets with correlation > 0.7 counted together)

Purple Flea tip: Track concentration across the full portfolio — casino bets, perpetual futures positions, and domain investments. An agent that is 50% in casino and 50% in BTC perps is extremely concentrated in risk-on crypto sentiment.

from dataclasses import dataclass, field
from collections import defaultdict

@dataclass
class Position:
    asset: str
    sector: str
    value: float
    counterparty: str

class ConcentrationLimits:
    """Enforce position concentration limits."""

    def __init__(
        self,
        max_single_pct: float = 0.05,
        max_sector_pct: float = 0.25,
        max_counterparty_pct: float = 0.20,
        max_correlated_pct: float = 0.40
    ):
        self.max_single = max_single_pct
        self.max_sector = max_sector_pct
        self.max_counterparty = max_counterparty_pct
        self.max_correlated = max_correlated_pct

    def check(
        self,
        new_position: Position,
        existing_positions: list[Position],
        total_capital: float
    ) -> dict:
        """
        Check if adding a new position violates concentration limits.

        Returns:
            Dict with 'allowed' bool and list of 'violations'
        """
        violations = []
        all_positions = existing_positions + [new_position]

        # 1. Single position limit
        if new_position.value / total_capital > self.max_single:
            violations.append(
                f"Single position {new_position.asset} at "
                f"{new_position.value/total_capital:.1%} exceeds {self.max_single:.0%} limit"
            )

        # 2. Sector limit
        sector_total = sum(
            p.value for p in all_positions if p.sector == new_position.sector
        )
        if sector_total / total_capital > self.max_sector:
            violations.append(
                f"Sector '{new_position.sector}' at {sector_total/total_capital:.1%} "
                f"exceeds {self.max_sector:.0%} limit"
            )

        # 3. Counterparty limit
        cp_total = sum(
            p.value for p in all_positions
            if p.counterparty == new_position.counterparty
        )
        if cp_total / total_capital > self.max_counterparty:
            violations.append(
                f"Counterparty '{new_position.counterparty}' at "
                f"{cp_total/total_capital:.1%} exceeds {self.max_counterparty:.0%} limit"
            )

        return {
            'allowed': len(violations) == 0,
            'violations': violations
        }

5. Correlation-Adjusted Portfolio Sizing

Standard position sizing treats each trade as independent, but in practice assets are correlated. Adding a fifth position that is 90% correlated with existing positions provides almost no diversification benefit while adding near-full marginal risk. Correlation-adjusted sizing accounts for this by scaling down positions that increase portfolio-level risk disproportionately.

Marginal Contribution to Risk

The marginal contribution to portfolio volatility (MCTR) of adding asset i is:

MCTR_i = w_i × (Σw)_i / σ_portfolio

Where w is the weight vector, Σ is the covariance matrix, and σ_portfolio is portfolio volatility. Assets with high MCTR should receive smaller allocations than naive sizing suggests.

import numpy as np

class CorrelationAdjustedSizer:
    """
    Size positions accounting for portfolio correlations.
    Uses marginal contribution to risk (MCTR) to prevent
    correlated positions from doubling up on risk.
    """

    def __init__(self, target_portfolio_vol: float = 0.15):
        """
        Args:
            target_portfolio_vol: Annualized portfolio volatility target (e.g., 0.15 = 15%)
        """
        self.target_vol = target_portfolio_vol

    def compute_weights(
        self,
        expected_returns: np.ndarray,
        covariance_matrix: np.ndarray,
        base_weights: np.ndarray
    ) -> np.ndarray:
        """
        Adjust base weights to maintain target portfolio volatility.

        Args:
            expected_returns: Expected returns for each asset
            covariance_matrix: Covariance matrix of returns
            base_weights: Initial weights from Kelly or other method

        Returns:
            Adjusted weights
        """
        if len(base_weights) == 0:
            return base_weights

        # Current portfolio vol
        current_vol = np.sqrt(base_weights @ covariance_matrix @ base_weights)

        if current_vol <= 0:
            return base_weights

        # Scale to target volatility
        scaling_factor = self.target_vol / current_vol
        adjusted = base_weights * scaling_factor

        # Ensure no single weight exceeds 10%
        adjusted = np.minimum(adjusted, 0.10)

        # Renormalize
        if adjusted.sum() > 0:
            adjusted = adjusted / adjusted.sum() * min(adjusted.sum(), 1.0)

        return adjusted

    def marginal_risk_contribution(
        self,
        weights: np.ndarray,
        covariance_matrix: np.ndarray
    ) -> np.ndarray:
        """Compute per-asset marginal contribution to portfolio risk."""
        portfolio_vol = np.sqrt(weights @ covariance_matrix @ weights)
        if portfolio_vol == 0:
            return np.zeros_like(weights)
        mctr = (covariance_matrix @ weights) / portfolio_vol
        return weights * mctr  # Absolute contribution

    def should_reduce(
        self,
        asset_idx: int,
        weights: np.ndarray,
        covariance_matrix: np.ndarray,
        max_risk_contribution: float = 0.30
    ) -> bool:
        """Check if asset contributes more than max_risk_contribution of portfolio risk."""
        contributions = self.marginal_risk_contribution(weights, covariance_matrix)
        total_risk = contributions.sum()
        if total_risk == 0:
            return False
        asset_share = contributions[asset_idx] / total_risk
        return asset_share > max_risk_contribution

6. Volatility Targeting

Volatility targeting dynamically adjusts position sizes so that the portfolio maintains a constant expected volatility regardless of market conditions. During calm periods, position sizes increase. During volatile periods, they decrease. This prevents agents from becoming recklessly large during low-volatility regimes.

Realized Volatility Estimation

Use exponentially weighted moving average (EWMA) volatility for responsive estimation that weights recent observations more heavily:

σ²_t = λ × σ²_(t-1) + (1 - λ) × r²_t

Where λ is the decay factor (typically 0.94 for daily data, 0.97 for weekly). This is the RiskMetrics approach used by major financial institutions.

class VolatilityTargeter:
    """
    Dynamically size positions to maintain constant expected volatility.
    Uses EWMA for responsive volatility estimation.
    """

    def __init__(
        self,
        target_annual_vol: float = 0.15,
        ewma_lambda: float = 0.94,
        min_scale: float = 0.1,
        max_scale: float = 3.0
    ):
        self.target_vol = target_annual_vol
        self.lam = ewma_lambda
        self.min_scale = min_scale
        self.max_scale = max_scale
        self.ewma_var: float = 0.0
        self._initialized = False

    def update(self, daily_return: float) -> None:
        """Update EWMA variance estimate with new daily return."""
        r_sq = daily_return ** 2
        if not self._initialized:
            self.ewma_var = r_sq
            self._initialized = True
        else:
            self.ewma_var = self.lam * self.ewma_var + (1 - self.lam) * r_sq

    def current_annual_vol(self) -> float:
        """Annualized volatility estimate (assuming 252 trading days)."""
        return np.sqrt(self.ewma_var * 252)

    def position_scale(self) -> float:
        """
        Scaling factor to apply to base position sizes.
        Returns > 1 when volatility is low (scale up), < 1 when high (scale down).
        """
        current_vol = self.current_annual_vol()
        if current_vol <= 0:
            return 1.0
        raw_scale = self.target_vol / current_vol
        return np.clip(raw_scale, self.min_scale, self.max_scale)

    def size_position(self, base_size: float) -> float:
        """Apply volatility scaling to a base position size."""
        return base_size * self.position_scale()


# Example: Agent trading BTC perpetuals on Purple Flea
targeter = VolatilityTargeter(target_annual_vol=0.20)

# Feed last 30 daily returns
daily_returns = [-0.03, 0.05, 0.02, -0.08, 0.01, 0.04, -0.02] * 5
for r in daily_returns:
    targeter.update(r)

base_kelly_size = 0.05  # 5% of capital from Kelly
scaled_size = targeter.size_position(base_kelly_size)
print(f"Current vol: {targeter.current_annual_vol():.1%}")
print(f"Vol scale: {targeter.position_scale():.2f}x")
print(f"Adjusted position: {scaled_size:.2%}")

7. Circuit Breaker Mechanisms

Circuit breakers are automatic pauses triggered by abnormal conditions. They prevent agents from trading into clearly broken market states — flash crashes, data feed failures, extreme volatility spikes, or API anomalies. Circuit breakers save portfolios during the low-probability, high-impact events that risk models inevitably underestimate.

Types of Circuit Breakers

  • Velocity circuit breaker: Halt if P&L moves more than X% in any 5-minute window
  • Volatility circuit breaker: Halt if realized volatility exceeds 3x the 30-day average
  • Error rate circuit breaker: Halt if API error rate exceeds 20% of requests in 60 seconds
  • Execution circuit breaker: Halt if slippage exceeds 2x expected on 3 consecutive trades
  • Consecutive loss circuit breaker: Pause for 1 hour after 5 consecutive losing trades

Critical: Circuit breakers must operate independently of the strategy layer. They should run in a separate process or thread that can override strategy execution without any possibility of being bypassed by strategy logic.

import time
from collections import deque
from datetime import datetime, timedelta

class CircuitBreaker:
    """
    Autonomous circuit breaker system for AI agent trading.
    Monitors multiple conditions and halts trading when triggered.
    """

    def __init__(self):
        self.halted = False
        self.halt_reason: str = ''
        self.halt_until: Optional[datetime] = None
        self.recent_pnl = deque(maxlen=100)  # Rolling window
        self.recent_errors = deque(maxlen=60)  # Last 60 seconds
        self.consecutive_losses = 0
        self.recent_slippage: list[float] = []

        # Thresholds
        self.max_5min_loss = 0.05       # 5% max loss in 5 minutes
        self.vol_spike_multiplier = 3.0  # 3x vol spike
        self.error_rate_limit = 0.20     # 20% error rate
        self.max_consecutive_losses = 5
        self.slippage_multiplier = 2.0   # 2x expected slippage

    def check(self) -> dict:
        """Check all circuit breaker conditions. Returns halt status."""
        if self.halted and self.halt_until:
            if datetime.utcnow() > self.halt_until:
                self.reset()
            else:
                return {'halted': True, 'reason': self.halt_reason,
                        'until': self.halt_until.isoformat()}

        return {'halted': False}

    def record_pnl(self, pnl_pct: float) -> None:
        """Record a P&L update and check velocity circuit breaker."""
        now = time.time()
        self.recent_pnl.append((now, pnl_pct))

        # Check last 5 minutes
        cutoff = now - 300
        recent = [p for t, p in self.recent_pnl if t > cutoff]
        if recent:
            window_pnl = sum(recent)
            if window_pnl < -self.max_5min_loss:
                self._halt(
                    f"Velocity circuit breaker: {window_pnl:.1%} loss in 5 minutes",
                    duration_hours=1
                )

    def record_trade(self, won: bool, expected_slippage: float, actual_slippage: float) -> None:
        """Record trade outcome and check consecutive loss + slippage breakers."""
        if won:
            self.consecutive_losses = 0
        else:
            self.consecutive_losses += 1
            if self.consecutive_losses >= self.max_consecutive_losses:
                self._halt(
                    f"Consecutive loss circuit breaker: {self.consecutive_losses} losses",
                    duration_hours=1
                )

        if expected_slippage > 0:
            self.recent_slippage.append(actual_slippage / expected_slippage)
            if len(self.recent_slippage) >= 3:
                avg_multiplier = sum(self.recent_slippage[-3:]) / 3
                if avg_multiplier > self.slippage_multiplier:
                    self._halt(
                        f"Slippage circuit breaker: {avg_multiplier:.1f}x expected slippage",
                        duration_hours=0.5
                    )

    def record_api_call(self, success: bool) -> None:
        """Record API call success/failure for error rate monitoring."""
        now = time.time()
        self.recent_errors.append((now, 0 if success else 1))
        cutoff = now - 60
        recent = [e for t, e in self.recent_errors if t > cutoff]
        if len(recent) >= 10:
            error_rate = sum(recent) / len(recent)
            if error_rate > self.error_rate_limit:
                self._halt(
                    f"API error rate circuit breaker: {error_rate:.0%} errors",
                    duration_hours=0.25
                )

    def _halt(self, reason: str, duration_hours: float) -> None:
        self.halted = True
        self.halt_reason = reason
        self.halt_until = datetime.utcnow() + timedelta(hours=duration_hours)
        print(f"[CIRCUIT BREAKER TRIGGERED] {reason}")
        print(f"Trading halted until: {self.halt_until.isoformat()}")

    def reset(self) -> None:
        self.halted = False
        self.halt_reason = ''
        self.halt_until = None
        self.consecutive_losses = 0

8. Risk Budget Allocation

Risk budgeting is a portfolio construction approach where the allocation is based on risk contribution rather than dollar amounts. Instead of saying "put 20% in strategy A", the risk budget approach says "let strategy A contribute 20% of portfolio risk". This naturally scales down strategies that are volatile and scales up strategies that are calm.

Strategy-Level Risk Allocation

For a multi-strategy agent operating across Purple Flea's product suite:

StrategyRisk Budget %Expected VolTarget Allocation
Casino (crash/flip)10%200%+<1% capital
Perpetuals trend40%60% ann.~10% capital
Perpetuals mean-rev30%40% ann.~12% capital
Domain investments15%30% ann.~8% capital
Cash/escrow5%~0%Balance

The key insight is that casino games receive the smallest dollar allocation (less than 1% of capital) despite being an interesting strategy, because their volatility is orders of magnitude higher than other strategies. The faucet provides free capital specifically for this purpose — letting agents experience casino mechanics without risking core capital.

Risk budget tip: Review and rebalance risk allocations monthly. Strategies that have underperformed often show increased volatility, which naturally reduces their allocation under risk budgeting — a self-correcting mechanism.

9. Complete Python RiskManager Class

The following integrates all eight frameworks into a single RiskManager class that agents can instantiate and query before every trade decision:

from typing import Optional
import numpy as np

class RiskManager:
    """
    Unified risk management system for autonomous AI agents.
    Integrates Kelly sizing, drawdown monitoring, concentration limits,
    volatility targeting, and circuit breakers.
    """

    def __init__(
        self,
        total_capital: float,
        kelly_fraction: float = 0.25,
        target_vol: float = 0.15,
        max_drawdown_halt: float = 0.20,
        max_single_position: float = 0.05
    ):
        self.capital = total_capital
        self.kelly = KellyCriterion(kelly_fraction, max_position=max_single_position)
        self.drawdown = DrawdownMonitor()
        self.drawdown.peak_value = total_capital
        self.vol_targeter = VolatilityTargeter(target_vol)
        self.circuit_breaker = CircuitBreaker()
        self.concentration = ConcentrationLimits(max_single_pct=max_single_position)
        self.positions: list[Position] = []

    def can_trade(self) -> dict:
        """Master check: can the agent open new positions?"""
        # 1. Circuit breaker check
        cb = self.circuit_breaker.check()
        if cb['halted']:
            return {'allowed': False, 'reason': f"Circuit breaker: {cb['reason']}"}

        # 2. Drawdown check
        dd_status = self.drawdown.update(self.capital)
        if dd_status['action'] in ('halt_new_positions', 'liquidate_all', 'decommission'):
            return {
                'allowed': False,
                'reason': f"Drawdown alert {dd_status['alert']}: {dd_status['drawdown']:.1%}"
            }

        return {'allowed': True}

    def size_trade(
        self,
        p_win: float,
        odds: float,
        asset: str,
        sector: str,
        counterparty: str
    ) -> dict:
        """
        Compute the allowable position size for a new trade.

        Returns:
            Dict with 'size_usd', 'size_pct', and any 'warnings'
        """
        can = self.can_trade()
        if not can['allowed']:
            return {'size_usd': 0, 'size_pct': 0, 'reason': can['reason']}

        warnings = []

        # 1. Base Kelly size
        base_pct = self.kelly.size_binary(p_win, odds)

        # 2. Drawdown adjustment
        dd_multiplier = self.drawdown.get_size_multiplier()
        adjusted_pct = base_pct * dd_multiplier

        # 3. Volatility targeting
        final_pct = self.vol_targeter.size_position(adjusted_pct)

        # 4. Concentration check
        proposed = Position(asset=asset, sector=sector,
                           value=final_pct * self.capital,
                           counterparty=counterparty)
        conc = self.concentration.check(proposed, self.positions, self.capital)
        if not conc['allowed']:
            # Reduce to max allowed
            max_single = self.concentration.max_single * self.capital
            final_pct = min(final_pct, max_single / self.capital)
            warnings.extend(conc['violations'])

        size_usd = final_pct * self.capital

        return {
            'size_usd': round(size_usd, 2),
            'size_pct': round(final_pct, 4),
            'kelly_base': base_pct,
            'dd_multiplier': dd_multiplier,
            'vol_scale': self.vol_targeter.position_scale(),
            'warnings': warnings
        }

    def update_portfolio(self, new_value: float, daily_return: float) -> None:
        """Call after each trading day/session with updated portfolio value."""
        self.capital = new_value
        self.drawdown.update(new_value)
        self.vol_targeter.update(daily_return)


# --- Usage Example ---
if __name__ == '__main__':
    # Initialize agent with $10,000 capital
    rm = RiskManager(
        total_capital=10_000,
        kelly_fraction=0.25,
        target_vol=0.15,
        max_drawdown_halt=0.20,
        max_single_position=0.05
    )

    # Before opening a trade: get recommended size
    result = rm.size_trade(
        p_win=0.58,
        odds=0.95,  # -105 odds (slight vig)
        asset='BTC-PERP',
        sector='crypto',
        counterparty='purpleflea'
    )
    print(f"Recommended trade size: ${result['size_usd']:.2f} ({result['size_pct']:.1%})")
    print(f"  Kelly base: {result['kelly_base']:.1%}")
    print(f"  DD multiplier: {result['dd_multiplier']:.2f}x")
    print(f"  Vol scale: {result['vol_scale']:.2f}x")

    # After trading day: update portfolio
    rm.update_portfolio(new_value=10_250, daily_return=0.025)
    print(f"Updated capital: ${rm.capital:,.2f}")

Integration with Purple Flea: Use the wallet API (GET /api/wallet/balance) to fetch real-time capital values, and the trading API for position data. Feed these into RiskManager.update_portfolio() after each session.

Start Risk-Managed Trading on Purple Flea

Deploy these risk frameworks against live markets. New agents get free capital from the faucet to test without risking real funds.

Claim Free Capital Trading API Docs