
Kelly Criterion
- 255 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
kelly-criterion is a Claude Code skill that computes Kelly-optimal position sizing with fractional variants and edge estimation for crypto trading.
About
kelly-criterion computes the Kelly-optimal fraction of capital to risk per trade, with fractional variants and edge estimation. A developer uses it to size positions for crypto trading based on win rate and payoff ratio while avoiding the overbetting that reduces long-term growth. It stresses fractional Kelly and minimum sample sizes.
- Computes Kelly-optimal bet sizing with fractional variants (0.25x-0.5x) for crypto trading
- Ships kelly_calculator.py and kelly_from_trades.py plus derivation and practical references
- Documents edge estimation and minimum-trade-count data requirements
Kelly Criterion by the numbers
- 255 all-time installs (skills.sh)
- Ranked #358 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
kelly-criterion capabilities & compatibility
- Capabilities
- position sizing · risk management · kelly criterion
- Use cases
- data analysis · research · trading
What kelly-criterion says it does
The Kelly criterion is the mathematically optimal bet size that maximizes long-term geometric growth of capital.
You should almost never use full Kelly. Estimation error in your edge means full Kelly will overbets in practice.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill kelly-criterionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 255 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Compute Kelly-optimal, risk-adjusted position sizes from win rate and payoff ratio.
Who is it for?
Deriving risk-adjusted position sizes from an estimated edge and payoff ratio.
Skip if: Trades with fewer than ~50 samples or estimating the underlying win rate itself.
When should I use this skill?
You need to size a trade optimally from a known win rate and payoff ratio.
By the numbers
- Kelly formula f*=(p*b-q)/b
- 5x5 win-rate/payoff reference table
- 50 trades minimum for any calculation
Files
Kelly Criterion — Optimal Bet Sizing
The Kelly criterion is the mathematically optimal bet size that maximizes long-term geometric growth of capital. Developed by John Kelly at Bell Labs in 1956, it answers a precise question: given a known edge, what fraction of your bankroll should you risk to maximize the compounding rate?
Core insight: Betting too small leaves growth on the table. Betting too large increases ruin risk and actually reduces long-term growth. Kelly finds the exact optimum between these extremes.
Practical insight: You should almost never use full Kelly. Estimation error in your edge means full Kelly will overbets in practice. Use fractional Kelly (0.25x to 0.5x) for real trading.
---
The Kelly Formula
For a binary outcome (win or lose):
f* = (p * b - q) / bWhere:
f*= optimal fraction of bankroll to betp= probability of winningq= probability of losing (1 - p)b= payoff ratio (average win / average loss)
Equivalent forms:
f* = p - q / b
f* = p - (1 - p) / b
f* = (p * b - (1 - p)) / bEdge = p * b - q = expected value per unit risked. Kelly only makes sense when edge > 0. If edge is zero or negative, the optimal bet is zero — do not trade.
Quick Reference
| Win Rate | Payoff 1:1 | Payoff 1.5:1 | Payoff 2:1 | Payoff 3:1 |
|---|---|---|---|---|
| 40% | -20% | -6.7% | 10% | 20% |
| 45% | -10% | 3.3% | 15% | 25% |
| 50% | 0% | 16.7% | 25% | 33.3% |
| 55% | 10% | 18.3% | 27.5% | 35% |
| 60% | 20% | 26.7% | 35% | 40% |
Values are full Kelly fraction. In practice, use 0.25x to 0.5x of these numbers.
---
Why Use Fractional Kelly
Full Kelly assumes you know p and b exactly. You never do. Here is why fractional Kelly is essential:
1. Estimation Error
Your win rate estimate from 100 trades has a standard error of roughly ±5%. If your true win rate is 55% but you estimate 60%, full Kelly will overbets by ~50%, which reduces long-term growth below what half Kelly would achieve.
2. Variance and Drawdowns
Full Kelly has extremely high variance. Expected maximum drawdown for full Kelly is roughly 50-80% of account. This is psychologically devastating and practically dangerous (margin calls, inability to continue trading).
| Kelly Fraction | Relative Growth Rate | Approximate Max Drawdown |
|---|---|---|
| 1.0x (full) | 100% | 50-80% |
| 0.5x (half) | ~75% | 25-40% |
| 0.25x (quarter) | ~50% | 12-20% |
| 0.1x (tenth) | ~25% | 5-10% |
3. Asymmetry of Over vs. Under Betting
Overbetting by 2x (betting at 2f) produces zero long-term growth — the same as not trading at all. Underbetting by 2x (betting at 0.5f) still captures ~75% of the optimal growth rate. The penalty for overbetting is catastrophically worse than for underbetting.
Recommended Fractions
| Fraction | When to Use |
|---|---|
| 0.10x Kelly | Very uncertain edge, new strategy, < 30 trades in sample |
| 0.25x Kelly | Moderate confidence, 30-100 trades, reasonable Sharpe |
| 0.50x Kelly | High confidence, 100+ trades, consistent performance |
| 1.00x Kelly | Never recommended in practice |
---
Estimating Your Edge
Kelly requires two inputs: win rate (p) and payoff ratio (b). Both must be estimated from data.
Minimum Data Requirements
- 50 trades minimum for any Kelly calculation. Below this, estimation error dominates.
- 100+ trades preferred for half Kelly sizing.
- 200+ trades before considering aggressive fractions.
Calculation from Trade History
wins = [t for t in trades if t > 0]
losses = [t for t in trades if t < 0]
win_rate = len(wins) / len(trades) # p
payoff_ratio = mean(wins) / abs(mean(losses)) # b
edge = win_rate * payoff_ratio - (1 - win_rate) # should be > 0
kelly_full = (win_rate * payoff_ratio - (1 - win_rate)) / payoff_ratioConservative Estimation
Use the lower bound of a Wilson confidence interval for win rate rather than the point estimate:
import math
def wilson_lower(wins: int, total: int, z: float = 1.96) -> float:
"""Lower bound of Wilson score interval (95% confidence)."""
p = wins / total
denominator = 1 + z**2 / total
centre = p + z**2 / (2 * total)
spread = z * math.sqrt((p * (1 - p) + z**2 / (4 * total)) / total)
return (centre - spread) / denominatorUsing the lower bound of the confidence interval for win rate automatically builds in conservatism, reducing the risk of overbetting due to sampling luck.
Edge Strength Classification
| Edge Value | Classification | Notes |
|---|---|---|
| < 0 | Negative edge | Do not trade this strategy |
| 0 - 0.02 | No meaningful edge | Transaction costs likely exceed edge |
| 0.02 - 0.10 | Marginal edge | Conservative fractions only |
| 0.10 - 0.20 | Good edge | Standard fractions appropriate |
| > 0.20 | Excellent edge | Rare; verify not overfitting or temporary |
---
Multi-Bet Kelly (Simultaneous Positions)
When holding multiple positions simultaneously:
Independent Bets
If bets are uncorrelated, each can be sized at its individual Kelly fraction. However, the sum of all Kelly fractions should not exceed 1.0 (total portfolio). If it does, scale each proportionally:
kelly_fractions = [0.15, 0.10, 0.12, 0.08] # individual Kelly fractions
total = sum(kelly_fractions) # 0.45
if total > 1.0:
scale = 1.0 / total
kelly_fractions = [f * scale for f in kelly_fractions]Correlated Bets
Correlated positions (e.g., multiple SOL memecoins) are effectively one larger bet. Reduce each position proportionally to the correlation:
# Simple correlation adjustment
def adjust_for_correlation(kelly_fractions: list, avg_correlation: float) -> list:
"""Reduce Kelly fractions based on average inter-position correlation."""
n = len(kelly_fractions)
# Effective number of independent bets
n_eff = n / (1 + (n - 1) * avg_correlation)
scale = n_eff / n
return [f * scale for f in kelly_fractions]In crypto, meme token positions often have correlations of 0.5-0.8 with each other (they all dump together in risk-off). Treat them as partially one bet.
Portfolio Kelly Cap
Regardless of individual calculations, enforce a hard cap: total Kelly allocation should never exceed 1.0 (100% of portfolio). A practical maximum is 0.6-0.8 to leave cash buffer for drawdowns and new opportunities.
---
PumpFun / Meme Token Kelly
Meme token trading presents specific challenges for Kelly:
1. Edge is hard to estimate: Win rates and payoff ratios shift rapidly with market regime. 2. Fat tails dominate: A few large winners and many small losers. Standard Kelly assumes thin tails. 3. Correlation spikes in drawdowns: All meme tokens can dump simultaneously.
Practical Adjustments
- Use 0.1x to 0.25x Kelly maximum for meme tokens.
- Cap absolute position size at 2-5% of portfolio regardless of Kelly output.
- Recalculate edge weekly — stale estimates are dangerous.
- If Kelly suggests > 30%, your edge estimate is almost certainly wrong. Use 5% maximum.
def meme_kelly(win_rate: float, payoff_ratio: float, account: float) -> float:
"""Conservative Kelly for high-uncertainty meme token trades."""
kelly_full = (win_rate * payoff_ratio - (1 - win_rate)) / payoff_ratio
kelly_conservative = kelly_full * 0.15 # 0.15x fractional
max_fraction = 0.05 # hard cap at 5%
return min(max(kelly_conservative, 0), max_fraction) * account---
When Kelly Does Not Work
Kelly optimality relies on assumptions that are often violated:
| Assumption | Reality | Impact |
|---|---|---|
| Known edge (p, b) | Estimated from noisy data | Overbetting risk |
| Independent bets | Correlated positions | Ruin risk increases |
| Binary outcomes | Continuous P&L distribution | Formula approximation |
| Stationary edge | Edge changes over time | Stale sizing |
| No transaction costs | Slippage, fees, MEV | Effective edge lower |
| Unlimited divisibility | Minimum position sizes | Rounding needed |
Mitigations
1. Use fractional Kelly (addresses estimation error) 2. Adjust for correlation (addresses dependence) 3. Use continuous Kelly for non-binary returns (see references/kelly_derivation.md) 4. Recalculate regularly (addresses non-stationarity) 5. Subtract estimated costs from edge before calculating Kelly
---
Continuous Kelly (For Portfolio Returns)
When returns are continuous rather than binary win/lose:
f* = (μ - r) / σ²Where:
μ= expected return of the strategyr= risk-free rate (often 0 for crypto)σ²= variance of returns
This is equivalent to Sharpe² / (2 * σ) when the Sharpe ratio is computed as (μ - r) / σ.
Use this form when you have a return stream rather than discrete win/loss trades. See references/kelly_derivation.md for the full derivation.
---
Integration with Other Skills
- `position-sizing`: Kelly provides the optimal fraction; position-sizing translates that into units. Use Kelly as one input, then apply liquidity and volatility constraints from position-sizing.
- `risk-management`: Kelly sizing must respect portfolio-level risk limits. If Kelly suggests 10% per trade but your risk policy caps at 5%, the cap wins.
- `strategy-framework`: Document your Kelly parameters (fraction used, sample size, recalculation frequency) as part of strategy specification.
- `regime-detection`: Recalculate Kelly when regime changes. Edge in a trending market differs from edge in a ranging market.
---
Files
References
references/kelly_derivation.md— Full mathematical derivation of Kelly criterion, fractional Kelly growth rates, continuous Kelly, and multi-outcome Kellyreferences/practical_kelly.md— Edge estimation from trading data, confidence intervals, worked examples, common pitfalls, and danger zones
Scripts
scripts/kelly_calculator.py— Kelly calculator from win rate, payoff ratio, and account size. Prints fractional Kelly recommendations and sensitivity analysis. Dependencies: none.scripts/kelly_from_trades.py— Estimate Kelly from a list of trade P&L values. Computes confidence intervals, rolling stability analysis, and recommended fraction. Dependencies: numpy.
Kelly Criterion — Mathematical Derivation
Objective
Maximize the expected logarithm of wealth, which is equivalent to maximizing the long-term geometric growth rate of capital. This is known as the Kelly objective or log-optimal criterion.
Single Binary Bet Derivation
Setup
You have wealth W. You bet fraction f of W on a binary outcome:
- With probability
p, you win and gainf * b * W(payoff ratiob) - With probability
q = 1 - p, you lose and losef * W
After one bet:
- Win:
W_new = W * (1 + f * b) - Lose:
W_new = W * (1 - f)
Growth Rate
The expected log growth rate per bet is:
G(f) = p * log(1 + f * b) + q * log(1 - f)We want to find f* that maximizes G(f).
First Derivative
dG/df = p * b / (1 + f * b) - q / (1 - f)Set equal to zero:
p * b / (1 + f * b) = q / (1 - f)Cross-multiply:
p * b * (1 - f) = q * (1 + f * b)
p * b - p * b * f = q + q * b * f
p * b - q = p * b * f + q * b * f
p * b - q = b * f * (p + q)
p * b - q = b * f * 1 [since p + q = 1]
f = (p * b - q) / bTherefore:
f* = (p * b - q) / b = p - q / b = p - (1 - p) / bSecond Derivative (Verify Maximum)
d²G/df² = -p * b² / (1 + f * b)² - q / (1 - f)²This is always negative for 0 < f < 1, confirming that f* is a maximum.
Edge Condition
Kelly fraction is positive only when p * b - q > 0, i.e., the expected value of the bet is positive. If p * b - q <= 0, the optimal bet is f* = 0.
---
Growth Rate at the Optimum
Substituting f* back into G(f):
G* = p * log(1 + f* * b) + q * log(1 - f*)With f* = (p * b - q) / b:
1 + f* * b = 1 + p * b - q = p * b + p = p * (b + 1)
1 - f* = 1 - p + q / b = q + q / b = q * (b + 1) / bTherefore:
G* = p * log(p * (b + 1)) + q * log(q * (b + 1) / b)
G* = p * log(p) + q * log(q) + log(b + 1) + q * log(1/b)
G* = p * log(p) + q * log(q/b) + log(b + 1)Numerical Example
Win rate p = 0.55, payoff ratio b = 1.5:
f* = (0.55 * 1.5 - 0.45) / 1.5 = (0.825 - 0.45) / 1.5 = 0.375 / 1.5 = 0.25
G* = 0.55 * log(1 + 0.25 * 1.5) + 0.45 * log(1 - 0.25)
G* = 0.55 * log(1.375) + 0.45 * log(0.75)
G* = 0.55 * 0.3185 + 0.45 * (-0.2877)
G* = 0.1752 - 0.1295 = 0.0457Growth rate of ~4.57% per bet at full Kelly.
---
Overbetting vs. Underbetting Asymmetry
At 2x Kelly (f = 2f*)
G(2f*) = p * log(1 + 2f* * b) + q * log(1 - 2f*)For the example above (f* = 0.25):
G(0.50) = 0.55 * log(1.75) + 0.45 * log(0.50)
G(0.50) = 0.55 * 0.5596 + 0.45 * (-0.6931)
G(0.50) = 0.3078 - 0.3119 = -0.0041Growth rate is negative at 2x Kelly. Overbetting by 2x leads to long-term ruin.
At 0.5x Kelly (f = 0.5f*)
G(0.125) = 0.55 * log(1.1875) + 0.45 * log(0.875)
G(0.125) = 0.55 * 0.1719 + 0.45 * (-0.1335)
G(0.125) = 0.0945 - 0.0601 = 0.0345Growth rate is 0.0345, which is 75.4% of the optimal 0.0457. Half Kelly sacrifices only ~25% of growth but dramatically reduces variance and drawdown.
---
Fractional Kelly Growth Rates
For fractional Kelly f = α * f* where 0 < α < 1:
Approximation
Near the optimum, growth rate is approximately parabolic:
G(α * f*) ≈ G* * (2α - α²) = G* * α * (2 - α)This gives:
α = 0.25:G ≈ G* * 0.4375(~44% of optimal growth)α = 0.50:G ≈ G* * 0.75(~75% of optimal growth)α = 0.75:G ≈ G* * 0.9375(~94% of optimal growth)α = 1.00:G = G*(100% of optimal growth)
Variance Scaling
The variance of log-wealth growth scales with α²:
Var(log growth) ∝ α²So half Kelly has 25% of the variance of full Kelly, while retaining 75% of the growth rate. This is the core argument for fractional Kelly.
Maximum Drawdown Scaling
Expected maximum drawdown scales approximately linearly with α:
E[max drawdown] ∝ αHalf Kelly roughly halves the expected maximum drawdown compared to full Kelly.
---
Continuous Kelly (Gaussian Returns)
When returns follow a continuous distribution rather than binary outcomes:
Setup
Strategy has expected return μ and standard deviation σ per period. Risk-free rate is r. Invest fraction f of wealth.
Portfolio return per period: R_p = r + f * (R - r) where R is the strategy return.
Log Growth Rate
G(f) = E[log(1 + R_p)]
≈ E[R_p] - Var(R_p) / 2 [second-order Taylor expansion]
= r + f * (μ - r) - f² * σ² / 2Optimal Fraction
dG/df = (μ - r) - f * σ² = 0
f* = (μ - r) / σ²Relation to Sharpe Ratio
With Sharpe ratio S = (μ - r) / σ:
f* = S / σ = S² / (μ - r)Optimal growth rate: G* = r + S² / 2
Example
Strategy: μ = 0.10 (10% per period), σ = 0.20, r = 0:
f* = 0.10 / 0.04 = 2.5Kelly says lever 2.5x. In practice, use 0.5x → lever 1.25x.
---
Multi-Outcome Kelly
When outcomes are not binary but have multiple possible returns r_1, r_2, ..., r_n with probabilities p_1, p_2, ..., p_n:
Optimization Problem
maximize Σ p_i * log(1 + f * r_i)
subject to 0 ≤ f ≤ 1First-Order Condition
Σ p_i * r_i / (1 + f * r_i) = 0This generally requires numerical solution (Newton's method or bisection).
Practical Approach
For trade P&L data with many distinct outcomes:
from scipy.optimize import minimize_scalar
def neg_growth_rate(f: float, returns: list[float]) -> float:
"""Negative expected log growth rate (for minimization)."""
n = len(returns)
g = sum(math.log(1 + f * r) for r in returns if (1 + f * r) > 0)
# Penalize if any outcome leads to ruin
if any((1 + f * r) <= 0 for r in returns):
return 1e10
return -g / n
result = minimize_scalar(neg_growth_rate, bounds=(0, 1), method='bounded',
args=(returns,))
kelly_fraction = result.xThis approach handles arbitrary return distributions and automatically accounts for fat tails, skewness, and multi-modal outcomes.
---
Key Takeaways
1. *f = (pb - q) / b for binary outcomes. Only bet when edge `pb - q > 0`. 2. Overbetting is catastrophic. At 2x Kelly, growth rate drops to zero. At 3x Kelly, you go broke. 3. Half Kelly retains ~75% of growth with ~25% of the variance. This is the practical sweet spot. 4. Continuous Kelly: f = (μ - r) / σ². Use when you have return streams rather than win/loss data. 5. Multi-outcome Kelly requires numerical optimization but handles real-world return distributions. 6. All Kelly variants assume known parameters*. Estimation error always pushes you toward using smaller fractions.
Kelly Criterion — Practical Application Guide
Estimating Edge from Trading Data
Required Metrics
From a set of completed trades (P&L values):
trades = [0.5, -0.3, 0.8, -0.2, 0.4, -0.35, ...] # P&L per trade
wins = [t for t in trades if t > 0]
losses = [t for t in trades if t < 0]
win_rate = len(wins) / len(trades) # p
avg_win = sum(wins) / len(wins) # average win
avg_loss = abs(sum(losses) / len(losses)) # average loss (positive)
payoff_ratio = avg_win / avg_loss # b
edge = win_rate * payoff_ratio - (1 - win_rate) # expected value per unitMinimum Sample Sizes
| Sample Size | Reliability | Recommendation |
|---|---|---|
| < 20 trades | Very unreliable | Do not use Kelly at all |
| 20-50 trades | Poor | 0.1x Kelly maximum, if any |
| 50-100 trades | Moderate | 0.25x Kelly |
| 100-200 trades | Good | 0.25x - 0.5x Kelly |
| 200+ trades | Strong | Up to 0.5x Kelly |
Never use full Kelly regardless of sample size.
---
Confidence Intervals for Win Rate
Wilson Score Interval
The Wilson interval is preferred over the naive interval for proportions because it handles edge cases (win rate near 0 or 1, small samples) correctly.
import math
def wilson_interval(wins: int, total: int, z: float = 1.96) -> tuple[float, float]:
"""95% confidence interval for win rate using Wilson score.
Args:
wins: Number of winning trades.
total: Total number of trades.
z: Z-score (1.96 for 95%, 1.645 for 90%).
Returns:
(lower_bound, upper_bound) of win rate.
"""
if total == 0:
return (0.0, 1.0)
p = wins / total
denominator = 1 + z**2 / total
centre = p + z**2 / (2 * total)
spread = z * math.sqrt((p * (1 - p) + z**2 / (4 * total)) / total)
lower = (centre - spread) / denominator
upper = (centre + spread) / denominator
return (max(0.0, lower), min(1.0, upper))Conservative Kelly
Use the lower bound of the confidence interval for win rate when calculating Kelly. This automatically adjusts for sample size: smaller samples produce wider intervals, leading to more conservative sizing.
def conservative_kelly(wins: int, total: int, payoff_ratio: float) -> float:
"""Kelly fraction using conservative win rate estimate."""
lower, _ = wilson_interval(wins, total)
edge = lower * payoff_ratio - (1 - lower)
if edge <= 0:
return 0.0
return edge / payoff_ratio---
Common Edge Values in Crypto Trading
| Edge Range | Classification | Typical Sources |
|---|---|---|
| < 0 | Negative edge | Poor strategy, overtransacting |
| 0 - 0.02 | No meaningful edge | Fees and slippage consume it |
| 0.02 - 0.05 | Weak edge | Marginal strategies, crowded signals |
| 0.05 - 0.10 | Moderate edge | Solid momentum or mean reversion |
| 0.10 - 0.20 | Good edge | Well-tuned niche strategies |
| 0.20 - 0.50 | Excellent edge | Rare; typically temporary or informational |
| > 0.50 | Suspicious | Almost certainly overfitting or survivorship bias |
If your calculated edge exceeds 0.30, scrutinize your methodology before trusting it. Common causes of inflated edge:
- Survivorship bias (only analyzing tokens that still exist)
- Lookahead bias (using future information in backtest)
- Small sample size with lucky streak
- Not accounting for slippage and fees
---
Fractional Kelly Recommendations
| Fraction | Growth (% of optimal) | Drawdown (% of full Kelly) | When to Use |
|---|---|---|---|
| 0.10x | ~19% | ~10% | Brand new strategy, < 30 trades, very uncertain |
| 0.15x | ~28% | ~15% | PumpFun/meme tokens, regime uncertainty |
| 0.25x | ~44% | ~25% | Standard for moderate confidence (50-100 trades) |
| 0.33x | ~55% | ~33% | Good track record, 100+ trades |
| 0.50x | ~75% | ~50% | High confidence, 200+ trades, consistent Sharpe |
| 0.75x | ~94% | ~75% | Rarely justified in practice |
| 1.00x | 100% | 100% | Never recommended |
---
Worked Examples
Example 1: Moderate Edge Strategy
Data: 120 trades, 66 wins, 54 losses. Average win: 0.45 SOL. Average loss: 0.30 SOL.
win_rate = 66 / 120 = 0.55
payoff_ratio = 0.45 / 0.30 = 1.50
edge = 0.55 * 1.50 - 0.45 = 0.825 - 0.45 = 0.375
kelly_full = 0.375 / 1.50 = 0.25 (25%)
# Conservative: Wilson lower bound
wilson_lower(66, 120) ≈ 0.460
conservative_kelly = (0.460 * 1.50 - 0.540) / 1.50 = 0.15 / 1.50 = 0.10
# Recommended fractions (of point-estimate Kelly)
kelly_quarter = 0.25 * 0.25 = 6.25%
kelly_half = 0.50 * 0.25 = 12.5%
# With 100 SOL account:
position_quarter = 6.25 SOL per trade
position_half = 12.5 SOL per tradeRecommendation: Use 6-12% per trade (0.25x-0.50x Kelly). The 120-trade sample gives moderate confidence.
Example 2: High Win Rate, Low Payoff
Data: 200 trades, 120 wins, 80 losses. Average win: 0.20 SOL. Average loss: 0.25 SOL.
win_rate = 120 / 200 = 0.60
payoff_ratio = 0.20 / 0.25 = 0.80
edge = 0.60 * 0.80 - 0.40 = 0.48 - 0.40 = 0.08
kelly_full = 0.08 / 0.80 = 0.10 (10%)
kelly_quarter = 2.5%
kelly_half = 5.0%Recommendation: Use 2.5-5% per trade. The edge is real but small; larger sizing risks ruin from inevitable losing streaks.
Example 3: Low Win Rate, High Payoff (Trend Following)
Data: 80 trades, 32 wins, 48 losses. Average win: 1.50 SOL. Average loss: 0.50 SOL.
win_rate = 32 / 80 = 0.40
payoff_ratio = 1.50 / 0.50 = 3.00
edge = 0.40 * 3.00 - 0.60 = 1.20 - 0.60 = 0.60
kelly_full = 0.60 / 3.00 = 0.20 (20%)
# Conservative with Wilson lower bound
wilson_lower(32, 80) ≈ 0.298
conservative_edge = 0.298 * 3.0 - 0.702 = 0.192
conservative_kelly = 0.192 / 3.0 = 0.064
kelly_quarter = 0.25 * 0.20 = 5.0%
kelly_half = 0.50 * 0.20 = 10.0%Recommendation: Use 5-6% per trade. Despite the apparently high edge, the 80-trade sample with 40% win rate has wide confidence intervals. Conservative Kelly (6.4%) aligns with quarter Kelly (5.0%).
---
Kelly Danger Zones
Kelly > 50%
If your calculation produces a Kelly fraction above 50%, something is almost certainly wrong:
- Your win rate estimate is inflated (survivorship bias, small sample)
- Your payoff ratio is inflated (outlier winners dominating)
- You are not accounting for transaction costs
Action: Cap at 25% before applying fraction. Investigate your data.
Negative Kelly
A negative Kelly fraction means your strategy has negative expected value. You lose money on average.
Action: Stop trading this strategy. Investigate why. Common causes:
- Transaction costs exceed gross edge
- Strategy no longer works (regime change)
- Implementation differs from backtest
Monotonically Increasing Kelly
If your rolling Kelly fraction keeps growing over time, be suspicious:
- Your recent trades may be in an unusually favorable regime
- You may be experiencing a lucky streak
- The market may be about to revert
Action: Use the minimum of recent and long-term Kelly, not the maximum.
Kelly Oscillating Wildly
If Kelly swings dramatically between recalculations (e.g., 5% one month, 20% the next):
- Your sample size is too small for stable estimates
- The underlying edge is not stationary
Action: Use longer lookback windows, apply heavier fractional Kelly (0.1x-0.25x), or average multiple estimates.
---
Recalculation Frequency
| Trading Frequency | Kelly Recalculation | Rationale |
|---|---|---|
| Many trades/day | Weekly | Fast data accumulation |
| Few trades/day | Bi-weekly to monthly | Need time to accumulate samples |
| Few trades/week | Monthly to quarterly | Very slow data accumulation |
When recalculating: 1. Use a rolling window (last 100-200 trades), not all-time 2. Compare new Kelly to previous — large jumps warrant investigation 3. Phase in changes gradually (blend old and new over 1-2 weeks)
---
Transaction Cost Adjustment
Always subtract estimated round-trip costs from your edge before computing Kelly:
def kelly_with_costs(win_rate: float, payoff_ratio: float,
cost_per_trade: float) -> float:
"""Kelly fraction adjusted for transaction costs.
Args:
win_rate: Probability of winning.
payoff_ratio: Average win / average loss.
cost_per_trade: Round-trip cost as fraction of position
(e.g., 0.005 for 0.5%).
"""
# Adjust payoff ratio and win rate for costs
# Costs reduce wins and increase losses
adj_payoff = (payoff_ratio - cost_per_trade) / (1 + cost_per_trade)
edge = win_rate * adj_payoff - (1 - win_rate)
if edge <= 0:
return 0.0
return edge / adj_payoffTypical round-trip costs in Solana DeFi:
- DEX swap fees: 0.25-0.30%
- Slippage (small trades): 0.10-0.50%
- Slippage (larger trades): 0.50-2.00%
- Priority fees: negligible for non-HFT
- Total: 0.40-2.50% round-trip
A strategy needs at least this much gross edge per trade to be worth trading.
#!/usr/bin/env python3
"""Kelly criterion calculator for optimal position sizing.
Computes full and fractional Kelly fractions from win rate and payoff ratio,
with sensitivity analysis and growth rate estimates.
Usage:
python scripts/kelly_calculator.py
# Or with custom parameters via environment variables:
WIN_RATE=0.55 AVG_WIN=1.5 AVG_LOSS=1.0 ACCOUNT_SIZE=100 python scripts/kelly_calculator.py
Dependencies:
None (pure Python math)
Environment Variables:
WIN_RATE: Probability of winning (0 to 1). Default: 0.55
AVG_WIN: Average winning trade size. Default: 1.5
AVG_LOSS: Average losing trade size (positive number). Default: 1.0
ACCOUNT_SIZE: Total account size in SOL. Default: 100
"""
import math
import os
import sys
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
WIN_RATE = float(os.getenv("WIN_RATE", "0.55"))
AVG_WIN = float(os.getenv("AVG_WIN", "1.5"))
AVG_LOSS = float(os.getenv("AVG_LOSS", "1.0"))
ACCOUNT_SIZE = float(os.getenv("ACCOUNT_SIZE", "100"))
FRACTIONAL_KELLYS = [0.10, 0.25, 0.33, 0.50]
SENSITIVITY_OFFSETS = [-0.10, -0.05, -0.02, 0.0, 0.02, 0.05, 0.10]
# ── Core Functions ──────────────────────────────────────────────────
def kelly_fraction(win_rate: float, payoff_ratio: float) -> float:
"""Calculate the full Kelly fraction.
Args:
win_rate: Probability of winning (0 < p < 1).
payoff_ratio: Average win / average loss (b > 0).
Returns:
Optimal fraction of bankroll to bet. Can be negative
(indicating negative edge — do not bet).
"""
if payoff_ratio <= 0:
return 0.0
q = 1.0 - win_rate
return (win_rate * payoff_ratio - q) / payoff_ratio
def edge(win_rate: float, payoff_ratio: float) -> float:
"""Calculate the edge (expected value per unit risked).
Args:
win_rate: Probability of winning.
payoff_ratio: Average win / average loss.
Returns:
Edge value. Positive means profitable in expectation.
"""
return win_rate * payoff_ratio - (1.0 - win_rate)
def growth_rate(win_rate: float, payoff_ratio: float, fraction: float) -> float:
"""Calculate expected log growth rate per trade.
Args:
win_rate: Probability of winning.
payoff_ratio: Average win / average loss.
fraction: Fraction of bankroll to bet.
Returns:
Expected log growth rate G(f). Higher is better.
Returns -inf if fraction leads to ruin.
"""
if fraction <= 0:
return 0.0
if fraction >= 1.0:
return float("-inf")
win_term = 1.0 + fraction * payoff_ratio
lose_term = 1.0 - fraction
if win_term <= 0 or lose_term <= 0:
return float("-inf")
q = 1.0 - win_rate
return win_rate * math.log(win_term) + q * math.log(lose_term)
def approx_max_drawdown(fraction: float, num_trades: int = 100) -> float:
"""Estimate approximate maximum drawdown for a given Kelly fraction.
Uses the heuristic that expected max drawdown scales roughly with
the fraction size. This is a rough approximation.
Args:
fraction: Fraction of bankroll bet per trade.
num_trades: Number of trades in the evaluation period.
Returns:
Estimated maximum drawdown as a fraction (e.g., 0.25 = 25%).
"""
if fraction <= 0:
return 0.0
# Rough heuristic: max drawdown ~ 2 * fraction * sqrt(num_trades) / sqrt(num_trades)
# Simplified: for full Kelly, expect ~50-80% drawdown over long horizons
# Scale linearly with fraction relative to a baseline
baseline_dd = 0.6 # ~60% max drawdown for full Kelly over 100 trades
return min(fraction / 0.25 * baseline_dd * 0.25, 0.95)
def classify_edge(edge_value: float) -> str:
"""Classify the strength of a trading edge.
Args:
edge_value: The calculated edge.
Returns:
Human-readable classification string.
"""
if edge_value < 0:
return "NEGATIVE — do not trade"
elif edge_value < 0.02:
return "NO MEANINGFUL EDGE — costs likely exceed edge"
elif edge_value < 0.05:
return "WEAK — conservative fractions only"
elif edge_value < 0.10:
return "MODERATE — standard fractions appropriate"
elif edge_value < 0.20:
return "GOOD — well-calibrated strategy"
elif edge_value < 0.50:
return "EXCELLENT — verify not overfitting"
else:
return "SUSPICIOUS — almost certainly overfitting"
def validate_inputs(win_rate: float, avg_win: float, avg_loss: float,
account_size: float) -> Optional[str]:
"""Validate input parameters.
Args:
win_rate: Probability of winning.
avg_win: Average win size.
avg_loss: Average loss size.
account_size: Account size.
Returns:
Error message string if invalid, None if valid.
"""
if not 0.0 < win_rate < 1.0:
return f"WIN_RATE must be between 0 and 1 (exclusive), got {win_rate}"
if avg_win <= 0:
return f"AVG_WIN must be positive, got {avg_win}"
if avg_loss <= 0:
return f"AVG_LOSS must be positive, got {avg_loss}"
if account_size <= 0:
return f"ACCOUNT_SIZE must be positive, got {account_size}"
return None
def sensitivity_table(base_win_rate: float, payoff_ratio: float,
offsets: list[float]) -> list[dict]:
"""Compute Kelly fractions for various win rate offsets.
Args:
base_win_rate: The estimated win rate.
payoff_ratio: Average win / average loss.
offsets: List of win rate adjustments to test.
Returns:
List of dicts with win_rate, kelly, edge for each offset.
"""
results = []
for offset in offsets:
wr = base_win_rate + offset
if not 0.0 < wr < 1.0:
continue
k = kelly_fraction(wr, payoff_ratio)
e = edge(wr, payoff_ratio)
results.append({
"offset": offset,
"win_rate": wr,
"kelly": k,
"edge": e,
})
return results
def print_separator(char: str = "─", width: int = 65) -> None:
"""Print a visual separator line."""
print(char * width)
def print_header(title: str) -> None:
"""Print a section header."""
print()
print_separator()
print(f" {title}")
print_separator()
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the Kelly criterion calculator and print results."""
# Validate
error = validate_inputs(WIN_RATE, AVG_WIN, AVG_LOSS, ACCOUNT_SIZE)
if error:
print(f"Input error: {error}")
sys.exit(1)
payoff_ratio = AVG_WIN / AVG_LOSS
e = edge(WIN_RATE, payoff_ratio)
k_full = kelly_fraction(WIN_RATE, payoff_ratio)
# ── Header ──────────────────────────────────────────────────
print_header("KELLY CRITERION CALCULATOR")
print(f" Win Rate: {WIN_RATE:.1%}")
print(f" Avg Win: {AVG_WIN:.4f} SOL")
print(f" Avg Loss: {AVG_LOSS:.4f} SOL")
print(f" Payoff Ratio: {payoff_ratio:.2f} : 1")
print(f" Account Size: {ACCOUNT_SIZE:.2f} SOL")
print(f" Edge: {e:.4f}")
print(f" Edge Class: {classify_edge(e)}")
# ── Edge Check ──────────────────────────────────────────────
if e <= 0:
print()
print(" ** No positive edge detected. Kelly fraction is zero. **")
print(" ** Do not trade this strategy. **")
print_separator()
return
# ── Full Kelly ──────────────────────────────────────────────
print_header("FULL KELLY")
g_full = growth_rate(WIN_RATE, payoff_ratio, k_full)
print(f" Kelly Fraction: {k_full:.4f} ({k_full:.1%})")
print(f" Position Size: {k_full * ACCOUNT_SIZE:.4f} SOL")
print(f" Growth Rate/Trade: {g_full:.6f}")
print()
print(" ** Full Kelly is NOT recommended for live trading. **")
print(" ** Use fractional Kelly below. **")
# ── Fractional Kelly Table ──────────────────────────────────
print_header("FRACTIONAL KELLY RECOMMENDATIONS")
print(f" {'Fraction':<12} {'Kelly %':<10} {'Size (SOL)':<12} "
f"{'Growth Rate':<14} {'Est. Max DD':<12}")
print(f" {'--------':<12} {'-------':<10} {'----------':<12} "
f"{'───────────':<14} {'───────────':<12}")
for alpha in FRACTIONAL_KELLYS:
frac = k_full * alpha
pos_size = frac * ACCOUNT_SIZE
g = growth_rate(WIN_RATE, payoff_ratio, frac)
dd = approx_max_drawdown(frac)
g_pct = (g / g_full * 100) if g_full > 0 else 0
label = f"{alpha:.2f}x Kelly"
print(f" {label:<12} {frac:>8.2%} {pos_size:>10.4f} "
f" {g:.6f} ({g_pct:4.0f}%) {dd:>8.1%}")
# ── Sensitivity Analysis ────────────────────────────────────
print_header("SENSITIVITY ANALYSIS (Win Rate Uncertainty)")
print(f" {'Win Rate':<12} {'Offset':<10} {'Edge':<10} "
f"{'Full Kelly':<12} {'Half Kelly':<12}")
print(f" {'--------':<12} {'------':<10} {'----':<10} "
f"{'──────────':<12} {'──────────':<12}")
sens = sensitivity_table(WIN_RATE, payoff_ratio, SENSITIVITY_OFFSETS)
for row in sens:
marker = " <-- base" if row["offset"] == 0.0 else ""
offset_str = f"{row['offset']:+.0%}"
half_k = max(row["kelly"] * 0.5, 0)
print(f" {row['win_rate']:>8.1%} {offset_str:>6} "
f"{row['edge']:>8.4f} {row['kelly']:>10.2%} "
f"{half_k:>10.2%}{marker}")
# ── Recommendation ──────────────────────────────────────────
print_header("RECOMMENDATION")
if k_full > 0.50:
print(" WARNING: Full Kelly > 50%. Your edge estimate is likely")
print(" inflated. Cap effective Kelly at 25% before applying fraction.")
effective_kelly = min(k_full, 0.25)
else:
effective_kelly = k_full
rec_fraction = 0.25 # default recommendation
rec_reason = "default conservative"
if e < 0.05:
rec_fraction = 0.10
rec_reason = "weak edge"
elif e < 0.10:
rec_fraction = 0.25
rec_reason = "moderate edge"
elif e < 0.20:
rec_fraction = 0.33
rec_reason = "good edge, but verify with more trades"
else:
rec_fraction = 0.25
rec_reason = "high edge (suspicious — stay conservative)"
rec_kelly = effective_kelly * rec_fraction
rec_size = rec_kelly * ACCOUNT_SIZE
rec_cap = min(rec_size, ACCOUNT_SIZE * 0.25) # hard cap at 25%
print(f" Suggested fraction: {rec_fraction:.2f}x Kelly ({rec_reason})")
print(f" Effective Kelly: {rec_kelly:.2%}")
print(f" Position size: {rec_cap:.4f} SOL per trade")
print(f" Max portfolio cap: {ACCOUNT_SIZE * 0.25:.4f} SOL (25% hard cap)")
print()
print(" NOTE: This is a mathematical calculation, not financial advice.")
print(" Always apply additional risk limits from your risk management rules.")
print_separator()
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Estimate Kelly criterion parameters from a list of trade P&L values.
Computes win rate, payoff ratio, edge, confidence intervals, rolling
stability analysis, and recommends an appropriate Kelly fraction.
Usage:
# Demo mode with synthetic trades:
python scripts/kelly_from_trades.py
# Or provide trades as comma-separated P&L values:
TRADES="0.5,-0.3,0.8,-0.2,0.4,-0.35,1.2,-0.25" python scripts/kelly_from_trades.py
Dependencies:
uv pip install numpy
Environment Variables:
TRADES: Comma-separated P&L values (optional; uses demo data if not set).
DEMO_SEED: Random seed for demo data generation (default: 42).
DEMO_COUNT: Number of synthetic trades in demo mode (default: 100).
"""
import math
import os
import sys
from typing import Optional
try:
import numpy as np
except ImportError:
print("numpy is required. Install with: uv pip install numpy")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
TRADES_ENV = os.getenv("TRADES", "")
DEMO_SEED = int(os.getenv("DEMO_SEED", "42"))
DEMO_COUNT = int(os.getenv("DEMO_COUNT", "100"))
ROLLING_WINDOW = 30 # trades per rolling window
# ── Data Generation ─────────────────────────────────────────────────
def generate_demo_trades(count: int = 100, seed: int = 42) -> list[float]:
"""Generate synthetic trade P&L data for demonstration.
Simulates a strategy with ~55% win rate and ~1.5:1 payoff ratio,
plus some noise and occasional outliers.
Args:
count: Number of trades to generate.
seed: Random seed for reproducibility.
Returns:
List of P&L values (positive = win, negative = loss).
"""
rng = np.random.default_rng(seed)
trades = []
for _ in range(count):
if rng.random() < 0.55:
# Winner: base 0.4 SOL, with some variance and occasional big win
pnl = rng.exponential(0.4)
if rng.random() < 0.05: # 5% chance of big winner
pnl *= 3.0
trades.append(round(pnl, 4))
else:
# Loser: base 0.3 SOL, less variance (stops hit more consistently)
pnl = rng.exponential(0.25) + 0.05
trades.append(round(-pnl, 4))
return trades
def parse_trades(trades_str: str) -> list[float]:
"""Parse comma-separated trade P&L string into list of floats.
Args:
trades_str: Comma-separated P&L values.
Returns:
List of float P&L values.
Raises:
ValueError: If any value cannot be parsed as float.
"""
values = []
for part in trades_str.split(","):
part = part.strip()
if part:
values.append(float(part))
return values
# ── Statistics ──────────────────────────────────────────────────────
def wilson_interval(wins: int, total: int,
z: float = 1.96) -> tuple[float, float]:
"""Wilson score confidence interval for a proportion.
Args:
wins: Number of successes.
total: Total trials.
z: Z-score (1.96 for 95% CI, 1.645 for 90% CI).
Returns:
(lower_bound, upper_bound) of the proportion.
"""
if total == 0:
return (0.0, 1.0)
p = wins / total
denominator = 1.0 + z ** 2 / total
centre = p + z ** 2 / (2.0 * total)
spread = z * math.sqrt(
(p * (1.0 - p) + z ** 2 / (4.0 * total)) / total
)
lower = (centre - spread) / denominator
upper = (centre + spread) / denominator
return (max(0.0, lower), min(1.0, upper))
def compute_trade_stats(trades: list[float]) -> dict:
"""Compute trading statistics from P&L data.
Args:
trades: List of P&L values.
Returns:
Dictionary with win_rate, payoff_ratio, edge, kelly, and more.
"""
wins = [t for t in trades if t > 0]
losses = [t for t in trades if t < 0]
flat = [t for t in trades if t == 0]
total = len(trades)
n_wins = len(wins)
n_losses = len(losses)
if n_wins == 0 or n_losses == 0:
return {
"total": total,
"wins": n_wins,
"losses": n_losses,
"flat": len(flat),
"win_rate": n_wins / total if total > 0 else 0.0,
"avg_win": float(np.mean(wins)) if wins else 0.0,
"avg_loss": abs(float(np.mean(losses))) if losses else 0.0,
"payoff_ratio": 0.0,
"edge": 0.0 if n_wins == 0 else float("inf"),
"kelly_full": 0.0,
"total_pnl": sum(trades),
"error": "Need both wins and losses to compute Kelly.",
}
win_rate = n_wins / total
avg_win = float(np.mean(wins))
avg_loss = abs(float(np.mean(losses)))
payoff_ratio = avg_win / avg_loss if avg_loss > 0 else float("inf")
edge_val = win_rate * payoff_ratio - (1.0 - win_rate)
kelly_full = edge_val / payoff_ratio if payoff_ratio > 0 else 0.0
# Confidence intervals
ci_lower, ci_upper = wilson_interval(n_wins, total)
conservative_edge = ci_lower * payoff_ratio - (1.0 - ci_lower)
conservative_kelly = (
conservative_edge / payoff_ratio if conservative_edge > 0 else 0.0
)
return {
"total": total,
"wins": n_wins,
"losses": n_losses,
"flat": len(flat),
"win_rate": win_rate,
"win_rate_ci": (ci_lower, ci_upper),
"avg_win": avg_win,
"avg_loss": avg_loss,
"median_win": float(np.median(wins)),
"median_loss": abs(float(np.median(losses))),
"max_win": max(wins),
"max_loss": abs(min(losses)),
"payoff_ratio": payoff_ratio,
"edge": edge_val,
"kelly_full": kelly_full,
"conservative_kelly": conservative_kelly,
"total_pnl": sum(trades),
"avg_pnl": float(np.mean(trades)),
"std_pnl": float(np.std(trades)),
}
def rolling_kelly(trades: list[float],
window: int = 30) -> list[Optional[float]]:
"""Compute Kelly fraction using a rolling window.
Args:
trades: Full list of P&L values.
window: Number of trades per window.
Returns:
List of Kelly fractions (None where insufficient data).
"""
results: list[Optional[float]] = []
for i in range(len(trades)):
if i < window - 1:
results.append(None)
continue
window_trades = trades[i - window + 1: i + 1]
stats = compute_trade_stats(window_trades)
if "error" in stats:
results.append(None)
else:
results.append(stats["kelly_full"])
return results
def kelly_stability_score(rolling_values: list[Optional[float]]) -> float:
"""Compute stability score for rolling Kelly values.
A score of 1.0 means perfectly stable. Lower values indicate
more volatility in the Kelly estimate, suggesting less reliable edge.
Args:
rolling_values: Output from rolling_kelly().
Returns:
Stability score between 0 and 1.
"""
valid = [v for v in rolling_values if v is not None]
if len(valid) < 5:
return 0.0
arr = np.array(valid)
mean_val = np.mean(arr)
if mean_val == 0:
return 0.0
cv = float(np.std(arr) / abs(mean_val)) # coefficient of variation
# Map CV to 0-1 score: CV=0 → 1.0, CV=2 → 0.0
return max(0.0, min(1.0, 1.0 - cv / 2.0))
def recommend_fraction(stats: dict, stability: float) -> tuple[float, str]:
"""Recommend a Kelly fraction based on stats and stability.
Args:
stats: Output from compute_trade_stats().
stability: Output from kelly_stability_score().
Returns:
(recommended_fraction, explanation_string)
"""
total = stats["total"]
edge_val = stats["edge"]
if edge_val <= 0:
return (0.0, "Negative or zero edge — do not use Kelly sizing.")
reasons = []
# Sample size factor
if total < 30:
size_frac = 0.10
reasons.append(f"small sample ({total} trades)")
elif total < 50:
size_frac = 0.15
reasons.append(f"limited sample ({total} trades)")
elif total < 100:
size_frac = 0.25
reasons.append(f"moderate sample ({total} trades)")
elif total < 200:
size_frac = 0.33
reasons.append(f"good sample ({total} trades)")
else:
size_frac = 0.50
reasons.append(f"large sample ({total} trades)")
# Stability factor
if stability < 0.3:
stab_frac = 0.10
reasons.append(f"unstable Kelly (score {stability:.2f})")
elif stability < 0.5:
stab_frac = 0.20
reasons.append(f"moderately stable (score {stability:.2f})")
elif stability < 0.7:
stab_frac = 0.30
reasons.append(f"fairly stable (score {stability:.2f})")
else:
stab_frac = 0.50
reasons.append(f"stable Kelly (score {stability:.2f})")
# Edge plausibility
if edge_val > 0.30:
edge_frac = 0.15
reasons.append("suspiciously high edge")
elif edge_val > 0.15:
edge_frac = 0.30
reasons.append("high edge (verify)")
else:
edge_frac = 0.50
reasons.append("plausible edge level")
# Take the minimum (most conservative)
rec = min(size_frac, stab_frac, edge_frac)
reason_str = "; ".join(reasons)
return (rec, reason_str)
def print_separator(char: str = "─", width: int = 65) -> None:
"""Print a visual separator line."""
print(char * width)
def print_header(title: str) -> None:
"""Print a section header."""
print()
print_separator()
print(f" {title}")
print_separator()
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run Kelly analysis on trade data and print report."""
# Load or generate trades
if TRADES_ENV:
try:
trades = parse_trades(TRADES_ENV)
except ValueError as exc:
print(f"Error parsing TRADES: {exc}")
sys.exit(1)
data_source = "user-provided"
else:
trades = generate_demo_trades(DEMO_COUNT, DEMO_SEED)
data_source = f"demo (seed={DEMO_SEED}, count={DEMO_COUNT})"
if len(trades) < 5:
print("Need at least 5 trades for analysis.")
sys.exit(1)
# Compute stats
stats = compute_trade_stats(trades)
# Rolling analysis
rolling = rolling_kelly(trades, ROLLING_WINDOW)
stability = kelly_stability_score(rolling)
# ── Report ──────────────────────────────────────────────────
print_header("KELLY FROM TRADES — ANALYSIS REPORT")
print(f" Data source: {data_source}")
print(f" Total trades: {stats['total']}")
# ── Basic Stats ─────────────────────────────────────────────
print_header("TRADE STATISTICS")
print(f" Wins: {stats['wins']} ({stats['win_rate']:.1%})")
print(f" Losses: {stats['losses']} ({1 - stats['win_rate']:.1%})")
if stats["flat"] > 0:
print(f" Flat: {stats['flat']}")
print(f" Total P&L: {stats['total_pnl']:+.4f}")
print(f" Avg P&L: {stats['avg_pnl']:+.4f}")
print(f" Std P&L: {stats['std_pnl']:.4f}")
if "error" in stats:
print(f"\n {stats['error']}")
print_separator()
return
print(f"\n Avg Win: {stats['avg_win']:.4f}")
print(f" Avg Loss: {stats['avg_loss']:.4f}")
print(f" Median Win: {stats['median_win']:.4f}")
print(f" Median Loss: {stats['median_loss']:.4f}")
print(f" Max Win: {stats['max_win']:.4f}")
print(f" Max Loss: {stats['max_loss']:.4f}")
print(f" Payoff Ratio: {stats['payoff_ratio']:.3f}")
# ── Edge & Kelly ────────────────────────────────────────────
print_header("EDGE & KELLY ANALYSIS")
print(f" Edge: {stats['edge']:.4f}")
# Edge classification
if stats["edge"] < 0:
edge_class = "NEGATIVE — do not trade"
elif stats["edge"] < 0.02:
edge_class = "NO MEANINGFUL EDGE"
elif stats["edge"] < 0.05:
edge_class = "WEAK"
elif stats["edge"] < 0.10:
edge_class = "MODERATE"
elif stats["edge"] < 0.20:
edge_class = "GOOD"
elif stats["edge"] < 0.50:
edge_class = "EXCELLENT (verify)"
else:
edge_class = "SUSPICIOUS"
print(f" Edge Class: {edge_class}")
print(f"\n Full Kelly: {stats['kelly_full']:.4f} ({stats['kelly_full']:.1%})")
# Confidence intervals
ci_lo, ci_hi = stats["win_rate_ci"]
print(f"\n Win Rate 95% CI: [{ci_lo:.3f}, {ci_hi:.3f}]")
print(f" Conservative Kelly: {stats['conservative_kelly']:.4f} "
f"({stats['conservative_kelly']:.1%}) [using CI lower bound]")
if stats["edge"] <= 0:
print("\n No positive edge. Kelly sizing not applicable.")
print_separator()
return
# ── Fractional Kelly ────────────────────────────────────────
print_header("FRACTIONAL KELLY OPTIONS")
print(f" {'Fraction':<14} {'Kelly %':<10} {'Notes'}")
print(f" {'--------':<14} {'-------':<10} {'-----'}")
for alpha in [0.10, 0.25, 0.33, 0.50]:
frac = stats["kelly_full"] * alpha
note = ""
if alpha == 0.10:
note = "very conservative"
elif alpha == 0.25:
note = "standard conservative"
elif alpha == 0.33:
note = "moderate"
elif alpha == 0.50:
note = "aggressive (needs 100+ trades)"
print(f" {alpha:.2f}x Kelly {frac:>8.2%} {note}")
conservative_half = stats["conservative_kelly"] * 0.50
print(f"\n Conservative 0.50x: {conservative_half:>8.2%} "
f"[CI lower bound + half Kelly]")
# ── Rolling Stability ───────────────────────────────────────
print_header(f"ROLLING STABILITY (window = {ROLLING_WINDOW} trades)")
valid_rolling = [v for v in rolling if v is not None]
if len(valid_rolling) >= 3:
arr = np.array(valid_rolling)
print(f" Rolling Kelly mean: {np.mean(arr):.4f}")
print(f" Rolling Kelly std: {np.std(arr):.4f}")
print(f" Rolling Kelly min: {np.min(arr):.4f}")
print(f" Rolling Kelly max: {np.max(arr):.4f}")
print(f" Stability score: {stability:.3f} (0=unstable, 1=stable)")
# Show a few rolling values
n_show = min(8, len(valid_rolling))
step = max(1, len(valid_rolling) // n_show)
print(f"\n Sample rolling Kelly (every {step} values):")
for i in range(0, len(valid_rolling), step):
trade_idx = ROLLING_WINDOW - 1 + i
val = valid_rolling[i]
bar_len = max(0, int(val * 100))
bar = "#" * min(bar_len, 40)
print(f" Trade {trade_idx + 1:>4}: {val:>7.3f} {bar}")
# Check for trend
if len(valid_rolling) >= 10:
first_half = np.mean(arr[: len(arr) // 2])
second_half = np.mean(arr[len(arr) // 2:])
if second_half > first_half * 1.3:
print("\n TREND: Kelly is INCREASING over time. Verify that "
"edge improvement is real.")
elif second_half < first_half * 0.7:
print("\n TREND: Kelly is DECREASING over time. Edge may be "
"deteriorating.")
else:
print("\n TREND: Kelly is relatively stable over time.")
else:
print(" Insufficient data for rolling analysis "
f"(need {ROLLING_WINDOW}+ trades).")
print(f" Stability score: {stability:.3f}")
# ── Recommendation ──────────────────────────────────────────
print_header("RECOMMENDATION")
rec_frac, rec_reason = recommend_fraction(stats, stability)
if rec_frac == 0:
print(f" {rec_reason}")
else:
rec_kelly = stats["kelly_full"] * rec_frac
print(f" Recommended fraction: {rec_frac:.2f}x Kelly")
print(f" Effective Kelly: {rec_kelly:.2%}")
print(f" Reasoning: {rec_reason}")
print()
print(f" For a 100 SOL account: {rec_kelly * 100:.2f} SOL per trade")
print(f" Hard cap reminder: 25 SOL (25% of 100 SOL)")
print()
print(" NOTE: This is a mathematical analysis, not financial advice.")
print(" Always verify edge estimates with out-of-sample data.")
print_separator()
print()
if __name__ == "__main__":
main()