
Position Sizing
- 254 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
position-sizing is a Claude Code skill that calculates trade size using fixed-fractional, volatility-adjusted, Kelly, and liquidity-constrained methods.
About
position-sizing is a Claude Code skill that calculates trade size. It implements fixed fractional, volatility-adjusted, Kelly criterion, liquidity-constrained, and anti-martingale methods, with risk tiers and worked examples oriented toward Solana tokens. A developer uses it to decide how much capital to risk on each trade and which sizing constraint binds.
- Five sizing methods: fixed fractional, volatility-adjusted, Kelly, liquidity-constrained, anti-martingale
- Liquidity-constrained sizing for low-liquidity Solana AMM pools
- Ships size_calculator.py and portfolio_sizer.py that surface the binding constraint
Position Sizing by the numbers
- 254 all-time installs (skills.sh)
- Ranked #359 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
position-sizing capabilities & compatibility
Free; the calculators run in plain Python with no dependencies or API keys.
- Capabilities
- position sizing · risk management · kelly criterion · liquidity analysis
- Use cases
- data analysis
- Pricing
- Free
What position-sizing says it does
Position sizing is the single most important risk management decision in trading. Your entry signal determines direction; your position size determines survival.
Full Kelly assumes perfect knowledge of your edge. In practice, edge estimates are noisy. Always use fractional Kelly
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill position-sizingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 254 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Calculate how large a trade to take using fixed-fractional, volatility-adjusted, Kelly, and liquidity-constrained sizing.
Who is it for?
Deciding how many units to trade so a stop-out loses a controlled fraction of the account.
Skip if: Generating entry signals or executing trades.
When should I use this skill?
You have an entry and stop and need to size the position by risk, volatility, Kelly, or pool liquidity.
What you get
A recommended position size and the binding risk or liquidity constraint for the trade.
- Recommended position size
- Binding-constraint report
- Per-position and total portfolio risk
By the numbers
- 5 sizing methods documented
- 3 risk tiers (conservative 0.5-1%, standard 1-2%, aggressive 3-5%)
Files
Position Sizing
Position sizing is the single most important risk management decision in trading. Your entry signal determines direction; your position size determines survival. A mediocre strategy with proper sizing will outperform a great strategy with reckless sizing over any meaningful time horizon.
Core principle: Size determines survival, not entries. Two traders with the same signals but different sizing will have wildly different outcomes. The one who sizes conservatively survives drawdowns and compounds capital; the one who oversizes blows up.
Methods Covered
| Method | Best For | Key Input |
|---|---|---|
| Fixed Fractional | General trading, most recommended | Account risk % |
| Volatility-Adjusted | Volatile markets, multi-asset | ATR or realized vol |
| Kelly Criterion | Quantified edge with track record | Win rate + payoff ratio |
| Liquidity-Constrained | Low-liquidity Solana tokens | Pool depth |
| Anti-Martingale | Trend-following strategies | Recent P&L streak |
---
1. Fixed Fractional Sizing
The most recommended method for most traders. Risk a fixed percentage of your account on each trade.
Formula
risk_amount = account_value * risk_percentage
price_risk_per_unit = entry_price - stop_loss_price
position_size_units = risk_amount / price_risk_per_unit
position_value = position_size_units * entry_priceRisk Tiers
| Tier | Risk Per Trade | Use Case |
|---|---|---|
| Conservative | 0.5–1% | New strategies, drawdown recovery |
| Standard | 1–2% | Most traders, proven strategies |
| Aggressive | 3–5% | High-conviction setups with strong, measured edge |
Example
account = 10_000 # $10,000 or 100 SOL
risk_pct = 0.02 # 2%
entry = 1.50
stop_loss = 1.30
risk_amount = account * risk_pct # $200
price_risk = entry - stop_loss # $0.20
position_units = risk_amount / price_risk # 1,000 tokens
position_value = position_units * entry # $1,500With this sizing, if the stop loss is hit, you lose exactly 2% of your account regardless of the token's price or volatility.
---
2. Volatility-Adjusted Sizing
Scale position size inversely with volatility. When volatility is high, take smaller positions; when low, take larger positions. This normalizes the dollar risk across different market conditions.
Formula
adjusted_size = base_size * (target_vol / current_vol)Where:
target_vol: your desired daily portfolio volatility (e.g., 2%)current_vol: the token's current daily volatility (from ATR or realized vol)
Using ATR
atr_14 = 0.12 # 14-period ATR
close_price = 1.50
daily_vol_pct = atr_14 / close_price # 8%
target_daily_vol = account * 0.02 # $200 target daily move
position_size = target_daily_vol / atr_14 # 1,667 unitsThis automatically reduces exposure in volatile markets and increases it in calm ones.
---
3. Kelly Criterion
The mathematically optimal fraction of capital to risk, maximizing long-term growth rate. Derived from maximizing expected logarithmic utility.
Formula
f* = (p * b - q) / bWhere:
p= win rate (probability of winning trade)q= 1 - p (probability of losing trade)b= average win / average loss (payoff ratio)f*= optimal fraction of capital to risk
Equivalent form: f* = (p * (b + 1) - 1) / b
Critical Rule: NEVER Use Full Kelly
Full Kelly assumes perfect knowledge of your edge. In practice, edge estimates are noisy. Always use fractional Kelly:
| Fraction | Use Case | Notes |
|---|---|---|
| 0.25x Kelly | Conservative, recommended default | Robust to edge estimation error |
| 0.50x Kelly | Moderate, for well-measured edges | Still significant drawdown risk |
| 1.0x Kelly | Never in practice | Theoretical maximum, catastrophic if edge is overestimated |
Example
win_rate = 0.55 # 55% win rate
avg_win = 2.0 # Average win is 2x the average loss
avg_loss = 1.0
payoff_ratio = avg_win / avg_loss # b = 2.0
kelly = (win_rate * payoff_ratio - (1 - win_rate)) / payoff_ratio
# kelly = (0.55 * 2.0 - 0.45) / 2.0 = 0.325 = 32.5%
quarter_kelly = kelly * 0.25 # 8.1% — use this
half_kelly = kelly * 0.50 # 16.25%If Kelly is negative, you have no edge. Do not trade.
See references/sizing_formulas.md for the full mathematical derivation.
---
4. Liquidity-Constrained Sizing
Critical for Solana tokens. Even if your risk model says you can take a large position, the pool may not support it without unacceptable slippage.
Formula (Constant-Product AMM)
slippage ≈ trade_size / pool_liquidity
max_trade = pool_liquidity * max_slippage_pctRules of Thumb
| Constraint | Guideline |
|---|---|
| Max single trade | 2% of pool liquidity |
| Max position | 5% of pool liquidity |
| Minimum pool depth | 10x your desired position size |
Example
pool_sol = 500 # 500 SOL in pool
max_slippage = 0.02 # 2% max slippage
max_trade_sol = pool_sol * max_slippage # 10 SOL
# For a $150 SOL price, that's $1,500 max per tradeAlways check all pools, not just the largest. Aggregate liquidity across Raydium, Orca, and Meteora for the full picture. See the liquidity-analysis skill for pool depth assessment.
---
5. Anti-Martingale Sizing
Increase size after wins, decrease after losses. This is the opposite of the gambler's fallacy (Martingale). The logic: winning streaks may indicate your strategy is in sync with the market; losing streaks may indicate regime change.
Implementation
def anti_martingale_size(
base_size: float,
consecutive_wins: int,
consecutive_losses: int,
scale_factor: float = 0.25,
max_multiplier: float = 2.0,
min_multiplier: float = 0.5,
) -> float:
if consecutive_losses > 0:
multiplier = max(min_multiplier, 1.0 - consecutive_losses * scale_factor)
elif consecutive_wins > 0:
multiplier = min(max_multiplier, 1.0 + consecutive_wins * scale_factor)
else:
multiplier = 1.0
return base_size * multiplierUse conservatively. After 3+ consecutive losses, reducing size by 50% protects capital during drawdowns.
---
Position Sizing Ladder
Combine all methods and take the most conservative result:
1. Calculate Kelly size → theoretical max based on edge
2. Calculate fixed fractional → risk-based size
3. Calculate volatility-adjusted → vol-normalized size
4. Calculate liquidity-constrained max → market-based ceiling
5. Final size = min(all four) → binding constraint winsThe binding constraint tells you what is limiting your size:
- Kelly-bound: your edge is small, size accordingly
- Risk-bound: standard risk management is the limit
- Volatility-bound: market is too volatile for larger size
- Liquidity-bound: pool cannot absorb more without slippage
---
Account-Level Limits
Individual position sizing is necessary but not sufficient. You also need portfolio-level constraints:
| Limit | Guideline | Rationale |
|---|---|---|
| Max single position | 10% of portfolio | Diversification floor |
| Max correlated exposure | 25% of portfolio | Correlated assets move together |
| Max total exposure | 50–80% of portfolio | Cash reserve for opportunities/margin |
| Max positions | 5–10 concurrent | Attention and management bandwidth |
---
PumpFun / Meme Token Sizing
PumpFun and early-stage meme tokens require special sizing discipline:
- Very small positions: 0.1–1 SOL per trade due to extreme risk
- Scale with bonding curve fill %: smaller when early (high rug risk), slightly larger when proven (graduated to Raydium)
- Never size based on expected return — size based on acceptable total loss
- Treat as lottery tickets: expect most to go to zero
- Position limit: no more than 5–10% of portfolio across all meme positions combined
# PumpFun sizing example
account_sol = 100
meme_budget = account_sol * 0.05 # 5 SOL total for memes
per_trade = meme_budget / 10 # 0.5 SOL each, 10 shots---
Integration with Other Skills
| Skill | Integration |
|---|---|
risk-management | Portfolio-level limits, drawdown rules |
liquidity-analysis | Pool depth data for liquidity constraints |
kelly-criterion | Deeper Kelly math, edge estimation |
exit-strategies | Stop loss placement affects fixed fractional sizing |
volatility-modeling | Better vol estimates for volatility-adjusted sizing |
slippage-modeling | Precise slippage estimates for liquidity constraints |
---
Files
References
references/sizing_formulas.md— Mathematical derivations for all sizing methods with worked examplesreferences/practical_guide.md— Sizing by account size, token type, and common mistakes
Scripts
scripts/size_calculator.py— Calculates position size using all methods, shows binding constraintscripts/portfolio_sizer.py— Portfolio risk dashboard with per-position risk and available budget
---
Quick Reference
# Minimal fixed fractional sizing — copy-paste starter
def calc_position_size(
account: float, risk_pct: float, entry: float, stop: float
) -> float:
"""Return number of units to buy."""
risk_amount = account * risk_pct
price_risk = abs(entry - stop)
if price_risk == 0:
return 0.0
return risk_amount / price_riskPosition Sizing — Practical Guide
Actionable sizing guidelines by account size, token type, and situation. Covers common mistakes and adjustment rules.
---
Sizing by Account Size
Micro Accounts (<$1K / <7 SOL)
- Fixed amount per trade: 0.05–0.1 SOL
- Focus: Learning, not profit. Treat as tuition.
- Max concurrent positions: 3
- Total exposure: up to 80% (small accounts need concentration)
- Key rule: Never deposit more to "average down"
Small Accounts ($1K–$10K / 7–70 SOL)
- Risk per trade: 1–2% of account
- Max concurrent positions: 5
- Max single position: 10% of account
- Total exposure: 50–70%
- Key rule: Fees matter — avoid tokens with wide spreads
Medium Accounts ($10K–$100K / 70–700 SOL)
- Risk per trade: 0.5–1% of account
- Max concurrent positions: 7–10
- Max single position: 8% of account
- Total exposure: 40–60%
- Key rule: Start considering liquidity constraints on smaller tokens
Large Accounts (>$100K / >700 SOL)
- Risk per trade: 0.25–0.5% of account
- Max concurrent positions: 10–15
- Max single position: 5% of account
- Total exposure: 30–50%
- Key rule: Liquidity is your primary constraint. Split entries across time.
---
Sizing by Token Type
Blue Chip (SOL, ETH, BTC)
- Max position size: up to 10% of portfolio
- Sizing method: Fixed fractional or volatility-adjusted
- Typical ATR daily vol: 3–6%
- Liquidity constraint: rarely binding (deep pools)
- Stop loss distance: 5–10% from entry typical
Mid-Cap (Top 100 — JUP, BONK, WIF, PYTH)
- Max position size: up to 5% of portfolio
- Sizing method: Fixed fractional with liquidity check
- Typical ATR daily vol: 5–15%
- Liquidity constraint: check for accounts >$50K
- Stop loss distance: 8–15% from entry typical
Small-Cap (Top 500)
- Max position size: up to 2% of portfolio
- Sizing method: Fixed fractional, liquidity-constrained
- Typical ATR daily vol: 10–30%
- Liquidity constraint: almost always binding
- Stop loss distance: 15–25% from entry typical
PumpFun / Meme / Micro-Cap
- Max position size: 0.5–1% of portfolio per token
- Max total meme allocation: 5–10% of portfolio
- Sizing method: Fixed amount (lottery ticket sizing)
- Typical ATR daily vol: 30–100%+
- Liquidity constraint: always binding
- Stop loss: assume 100% loss is possible; size accordingly
---
When to Adjust Sizing
Reduce Size
| Trigger | Action | Rationale |
|---|---|---|
| 3 consecutive losses | Reduce to 50% normal size | Possible regime change |
| Drawdown > 10% from peak | Reduce to minimum size | Capital preservation mode |
| New/untested strategy | Start at 25% normal size | Earn the right to full size |
| Market volatility spike (VIX-like) | Reduce by vol ratio | Same dollar risk needs less exposure |
| Unusual correlation across positions | Cut weakest positions | Correlated risk compounds |
Increase Size (Cautiously)
| Trigger | Action | Rationale |
|---|---|---|
| 50+ trade track record with edge | Scale from 25% to 100% over time | Statistical confidence |
| Win streak > 5 | Allow up to 1.5x normal size | Strategy may be in sync |
| Low volatility regime | Vol-adjusted increase is mechanical | Same risk, larger notional |
| Account growth milestone | Recalculate base size upward | Compound growth |
Never Increase Size When
- Trying to recover from losses ("revenge trading")
- A single trade seems like a "sure thing" (no such thing)
- You haven't adjusted stops to match the larger size
- Your recent win streak is < 10 trades (not statistically significant)
---
Position Sizing Mistakes
1. Sizing Based on Conviction
Wrong: "I'm really confident in this trade, so I'll 5x my normal size."
Right: Let the math decide. Conviction is emotional, not quantitative. If you have a measured edge that justifies larger size, the Kelly formula will tell you.
2. Not Accounting for Fees and Slippage
Wrong: Calculating stop distance as entry - stop only.
Right: Include round-trip fees (0.3–0.6% typical) and expected slippage (0.1–2% depending on token) in your risk calculation.
3. Ignoring Correlation Between Positions
Wrong: "I'm risking 2% on each of 10 positions = 20% max risk."
Right: If 8 of those positions are meme tokens that all dump together, your effective risk is closer to 16% in a single correlated move. Account for correlation in portfolio limits.
4. Increasing Size to Make Back Losses
Wrong: After a 10% drawdown, doubling size to "get back to even faster."
Right: Reduce size during drawdowns. You need to earn back losses with smaller, consistent gains. The math: a 10% loss needs 11.1% gain to recover; a 50% loss needs 100%.
5. Using Position Size as Stop Loss
Wrong: "I'll just buy a small amount so if it goes to zero it's fine."
Right: Always use an explicit stop loss. "Small size, no stop" leads to holding worthless positions that tie up capital. Exception: PumpFun lottery tickets explicitly sized for 100% loss.
6. Not Adjusting for Timeframe
Wrong: Using the same 2% risk for a 5-minute scalp and a multi-week swing trade.
Right: Shorter timeframes need tighter stops relative to volatility, which means either smaller position sizes or accepting more noise. Scale risk per trade with expected hold time.
7. Forgetting Portfolio-Level Limits
Wrong: Each trade is individually sized, but no check on total exposure.
Right: Before each new position, verify:
- Total portfolio exposure is within limits
- Correlated exposure is within limits
- Total risk-on (sum of all position risks) is acceptable
---
Quick Decision Tree
Want to enter a trade?
│
├─ Do you have a measured edge (50+ trades)?
│ ├─ Yes → Calculate Kelly size (use 0.25x)
│ └─ No → Use fixed fractional (1% risk)
│
├─ Calculate fixed fractional size
│
├─ Is the token volatile (ATR > 10% daily)?
│ ├─ Yes → Also calculate vol-adjusted size
│ └─ No → Skip vol adjustment
│
├─ Is pool liquidity < 50x your desired position?
│ ├─ Yes → Calculate liquidity-constrained max
│ └─ No → Liquidity is not binding
│
├─ Take minimum of all calculated sizes
│
├─ Check portfolio limits:
│ ├─ Single position < 10% portfolio? ✓
│ ├─ Correlated exposure < 25%? ✓
│ └─ Total exposure < 80%? ✓
│
└─ Execute at recommended size---
SOL-Denominated Quick Reference
For a 100 SOL account ($15,000 at $150/SOL):
| Risk Level | Risk/Trade | SOL at Risk | Typical Position |
|---|---|---|---|
| Conservative (0.5%) | 0.5 SOL | $75 | 2–5 SOL notional |
| Standard (1%) | 1.0 SOL | $150 | 5–10 SOL notional |
| Moderate (2%) | 2.0 SOL | $300 | 10–20 SOL notional |
| Aggressive (3%) | 3.0 SOL | $450 | 15–30 SOL notional |
| PumpFun | 0.1–0.5 SOL | $15–$75 | 0.1–0.5 SOL per play |
Position Sizing — Formulas Reference
Complete mathematical derivations for all sizing methods with worked examples.
---
Fixed Fractional Sizing
Derivation
The goal: lose at most R% of account value if the stop loss is hit.
risk_amount = account_value × risk_percentage
price_risk_per_unit = |entry_price - stop_loss_price|
position_size_units = risk_amount / price_risk_per_unit
position_value = position_size_units × entry_price
leverage_ratio = position_value / account_valueWorked Example
| Parameter | Value |
|---|---|
| Account value | $10,000 |
| Risk per trade | 2% |
| Entry price | $1.50 |
| Stop loss | $1.30 |
risk_amount = $10,000 × 0.02 = $200
price_risk = $1.50 - $1.30 = $0.20
units = $200 / $0.20 = 1,000 tokens
position_value = 1,000 × $1.50 = $1,500 (15% of account)If the stop is hit: loss = 1,000 × $0.20 = $200 = 2% of account.
Fee-Adjusted Version
In practice, include fees and slippage in the risk:
effective_risk = price_risk + (entry_price × fee_rate × 2) + estimated_slippage
position_size = risk_amount / effective_riskExample with 0.3% fees each way and 0.5% slippage:
fee_cost = $1.50 × 0.003 × 2 = $0.009
slippage = $1.50 × 0.005 = $0.0075
effective_risk = $0.20 + $0.009 + $0.0075 = $0.2165
units = $200 / $0.2165 = 924 tokens (vs 1,000 without fees)---
Volatility-Adjusted Sizing
Derivation
Normalize position sizes so each position contributes roughly the same dollar volatility to the portfolio.
target_daily_pnl_vol = account_value × target_vol_pct
token_daily_vol = price × daily_return_std
position_size_units = target_daily_pnl_vol / token_daily_volUsing ATR (Average True Range)
ATR is a smoothed measure of daily price range:
daily_vol_pct = ATR(14) / close_price
position_units = (account × target_vol_pct) / ATR(14)Worked Example
| Parameter | Value |
|---|---|
| Account | $10,000 |
| Target daily vol | 2% of account ($200) |
| Token price | $1.50 |
| ATR(14) | $0.12 |
daily_vol_pct = $0.12 / $1.50 = 8%
position_units = $200 / $0.12 = 1,667 tokens
position_value = 1,667 × $1.50 = $2,500
expected_daily_pnl_range = 1,667 × $0.12 = $200 (2% of account)Comparing Across Assets
This method lets you hold equal-risk positions across assets with very different volatilities:
| Token | Price | ATR(14) | Vol % | Units | Value |
|---|---|---|---|---|---|
| SOL | $150 | $6.00 | 4% | 33 | $4,950 |
| BONK | $0.00002 | $0.0000016 | 8% | 125M | $2,500 |
| JUP | $0.80 | $0.064 | 8% | 3,125 | $2,500 |
Each contributes ~$200 daily PnL volatility despite different position sizes.
---
Kelly Criterion
Full Derivation
The Kelly criterion maximizes the expected logarithm of wealth (geometric growth rate).
Given a binary bet with:
- Probability
pof winningbunits per unit risked - Probability
q = 1 - pof losing 1 unit per unit risked
Maximize: E[ln(W)] = p × ln(1 + f×b) + q × ln(1 - f)
Taking the derivative and setting to zero:
dE/df = p×b/(1+f×b) - q/(1-f) = 0
p×b×(1-f) = q×(1+f×b)
p×b - p×b×f = q + q×f×b
p×b - q = f×b×(p + q) = f×b
f* = (p×b - q) / bEquivalent forms:
f* = (p×b - q) / b
f* = p - q/b
f* = (p×(b+1) - 1) / bFractional Kelly
Full Kelly maximizes growth but produces large drawdowns. The variance of returns under Kelly is:
Var = p×q×(b+1)² × f²Fractional Kelly reduces variance quadratically while reducing growth only linearly:
| Fraction | Growth Rate | Drawdown Risk | Recommended? |
|---|---|---|---|
| 1.0x | 100% | Very high | No |
| 0.5x | 75% | Moderate | For well-measured edges |
| 0.25x | 44% | Low | Default recommendation |
| 0.1x | 19% | Very low | Ultra-conservative |
Growth rate at fraction g: G(g) = g × (2 - g) × G(1) approximately.
Worked Examples
Example 1: Moderate edge
Win rate = 55%, avg win = 1.5x avg loss
p = 0.55, q = 0.45, b = 1.5
f* = (0.55 × 1.5 - 0.45) / 1.5 = 0.25 / 1.5 = 0.167 (16.7%)
Quarter Kelly: 4.2% risk per trade
Half Kelly: 8.3% risk per tradeExample 2: High win rate, small payoff
Win rate = 70%, avg win = 0.8x avg loss
p = 0.70, q = 0.30, b = 0.8
f* = (0.70 × 0.8 - 0.30) / 0.8 = 0.26 / 0.8 = 0.325 (32.5%)
Quarter Kelly: 8.1%Example 3: Low win rate, large payoff (trend following)
Win rate = 35%, avg win = 4x avg loss
p = 0.35, q = 0.65, b = 4.0
f* = (0.35 × 4.0 - 0.65) / 4.0 = 0.75 / 4.0 = 0.1875 (18.75%)
Quarter Kelly: 4.7%Example 4: No edge
Win rate = 45%, avg win = 1.0x avg loss
f* = (0.45 × 1.0 - 0.55) / 1.0 = -0.10
Negative Kelly → NO EDGE → do not trade---
Liquidity-Constrained Sizing
Constant-Product AMM Slippage
For a constant-product AMM (x × y = k):
price_impact = trade_size / (pool_reserve + trade_size)For small trades relative to pool size:
slippage ≈ trade_size / pool_reserveMaximum Trade Size
Given a maximum acceptable slippage:
max_trade = pool_reserve × max_slippage_pctWorked Example
| Parameter | Value |
|---|---|
| Pool SOL reserve | 500 SOL |
| Pool token reserve | 5,000,000 tokens |
| Token price | 0.0001 SOL |
| Max slippage | 2% |
max_trade_sol = 500 × 0.02 = 10 SOL
max_trade_tokens = 5,000,000 × 0.02 = 100,000 tokensMulti-Pool Aggregation
When a token has liquidity across multiple pools:
total_available = sum(pool_reserve_i × max_slippage for each pool_i)However, a DEX aggregator (Jupiter) will split the order optimally. The effective available liquidity is typically 70-90% of the simple sum due to routing overhead.
Position vs Trade Size
A position may require multiple trades to build:
max_single_trade = pool_liquidity × 0.02 # 2% per trade
max_position = pool_liquidity × 0.05 # 5% total (built in chunks)
trades_needed = ceil(desired_position / max_single_trade)---
Combined Sizing Ladder
Calculate all four methods and take the minimum:
def sizing_ladder(
account: float,
risk_pct: float,
entry: float,
stop: float,
atr: float,
target_vol: float,
win_rate: float,
payoff_ratio: float,
kelly_fraction: float,
pool_liquidity: float,
max_slippage: float,
) -> dict:
# 1. Fixed fractional
ff_units = (account * risk_pct) / abs(entry - stop)
# 2. Volatility-adjusted
vol_units = (account * target_vol) / atr
# 3. Kelly
kelly_f = (win_rate * payoff_ratio - (1 - win_rate)) / payoff_ratio
kelly_risk = max(0, kelly_f * kelly_fraction)
kelly_units = (account * kelly_risk) / abs(entry - stop)
# 4. Liquidity-constrained
liq_value = pool_liquidity * max_slippage
liq_units = liq_value / entry
sizes = {
"fixed_fractional": ff_units,
"volatility_adjusted": vol_units,
"kelly": kelly_units,
"liquidity": liq_units,
}
binding = min(sizes, key=sizes.get)
return {"sizes": sizes, "recommended": sizes[binding], "binding": binding}#!/usr/bin/env python3
"""Portfolio-level position sizer and risk dashboard.
Analyzes a portfolio of positions, calculates per-position and total risk,
correlation-adjusted risk, and recommends sizing for the next trade.
Usage:
python scripts/portfolio_sizer.py # Uses demo portfolio
python scripts/portfolio_sizer.py --demo # Explicitly use demo data
Dependencies:
None (pure math, no external packages required)
Environment Variables:
ACCOUNT_SIZE: Total account value in USD (default: 15000, i.e. ~100 SOL)
"""
import math
import os
import sys
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
ACCOUNT_SIZE = float(os.getenv("ACCOUNT_SIZE", "15000"))
# Portfolio limits
MAX_SINGLE_POSITION_PCT = 0.10 # 10% max single position
MAX_CORRELATED_PCT = 0.25 # 25% max correlated exposure
MAX_TOTAL_EXPOSURE_PCT = 0.70 # 70% max total exposure
DEFAULT_RISK_PCT = 0.02 # 2% risk per trade
DEFAULT_CORRELATION = 0.50 # Assumed correlation within same sector
# ── Data Structures ─────────────────────────────────────────────────
class Position:
"""Represents a single portfolio position.
Attributes:
symbol: Token symbol.
entry_price: Price at entry.
current_price: Current market price.
stop_loss: Stop loss price.
units: Number of units held.
sector: Sector/category for correlation grouping.
"""
def __init__(
self,
symbol: str,
entry_price: float,
current_price: float,
stop_loss: float,
units: float,
sector: str = "general",
) -> None:
self.symbol = symbol
self.entry_price = entry_price
self.current_price = current_price
self.stop_loss = stop_loss
self.units = units
self.sector = sector
@property
def position_value(self) -> float:
"""Current position value."""
return self.units * self.current_price
@property
def entry_value(self) -> float:
"""Position value at entry."""
return self.units * self.entry_price
@property
def unrealized_pnl(self) -> float:
"""Unrealized profit/loss."""
return self.units * (self.current_price - self.entry_price)
@property
def unrealized_pnl_pct(self) -> float:
"""Unrealized P&L as percentage of entry value."""
if self.entry_value == 0:
return 0.0
return (self.unrealized_pnl / self.entry_value) * 100
@property
def risk_per_unit(self) -> float:
"""Dollar risk per unit (distance to stop)."""
return abs(self.current_price - self.stop_loss)
@property
def total_risk(self) -> float:
"""Total dollar risk if stop is hit."""
return self.units * self.risk_per_unit
@property
def risk_reward_from_current(self) -> float:
"""Current risk as fraction of current value."""
if self.position_value == 0:
return 0.0
return self.total_risk / self.position_value
# ── Demo Data ───────────────────────────────────────────────────────
def get_demo_positions() -> list:
"""Return a demo portfolio with 5 example positions.
Returns:
List of Position objects representing a sample portfolio.
"""
return [
Position(
symbol="SOL",
entry_price=145.00,
current_price=150.00,
stop_loss=135.00,
units=20.0,
sector="l1",
),
Position(
symbol="JUP",
entry_price=0.72,
current_price=0.80,
stop_loss=0.60,
units=5000.0,
sector="defi",
),
Position(
symbol="BONK",
entry_price=0.000018,
current_price=0.000020,
stop_loss=0.000012,
units=50_000_000.0,
sector="meme",
),
Position(
symbol="WIF",
entry_price=1.80,
current_price=1.65,
stop_loss=1.40,
units=500.0,
sector="meme",
),
Position(
symbol="PYTH",
entry_price=0.35,
current_price=0.38,
stop_loss=0.28,
units=3000.0,
sector="defi",
),
]
# ── Portfolio Analytics ─────────────────────────────────────────────
def calculate_portfolio_metrics(
positions: list,
account_size: float,
) -> dict:
"""Calculate portfolio-level risk metrics.
Args:
positions: List of Position objects.
account_size: Total account value in USD.
Returns:
Dictionary containing portfolio metrics.
"""
total_value = sum(p.position_value for p in positions)
total_risk = sum(p.total_risk for p in positions)
total_pnl = sum(p.unrealized_pnl for p in positions)
cash = account_size - total_value
# Exposure percentages
exposure_pct = (total_value / account_size) * 100 if account_size > 0 else 0
risk_pct = (total_risk / account_size) * 100 if account_size > 0 else 0
cash_pct = (cash / account_size) * 100 if account_size > 0 else 0
return {
"total_value": total_value,
"total_risk": total_risk,
"total_pnl": total_pnl,
"cash": cash,
"exposure_pct": exposure_pct,
"risk_pct": risk_pct,
"cash_pct": cash_pct,
"num_positions": len(positions),
}
def calculate_sector_exposure(
positions: list,
account_size: float,
) -> dict:
"""Calculate exposure by sector for correlation analysis.
Args:
positions: List of Position objects.
account_size: Total account value.
Returns:
Dictionary mapping sector to exposure metrics.
"""
sectors: dict = {}
for p in positions:
if p.sector not in sectors:
sectors[p.sector] = {
"positions": [],
"total_value": 0.0,
"total_risk": 0.0,
}
sectors[p.sector]["positions"].append(p.symbol)
sectors[p.sector]["total_value"] += p.position_value
sectors[p.sector]["total_risk"] += p.total_risk
for sector_data in sectors.values():
sector_data["exposure_pct"] = (sector_data["total_value"] / account_size) * 100
sector_data["risk_pct"] = (sector_data["total_risk"] / account_size) * 100
return sectors
def calculate_correlation_adjusted_risk(
positions: list,
default_corr: float = DEFAULT_CORRELATION,
) -> float:
"""Calculate portfolio risk adjusted for assumed correlation.
Uses a simplified model where positions in the same sector have
a fixed correlation and positions in different sectors are uncorrelated.
The correlation-adjusted portfolio variance is:
Var(P) = sum(var_i) + 2 * sum_{i<j, same sector}(corr * std_i * std_j)
Args:
positions: List of Position objects.
default_corr: Assumed correlation between same-sector positions.
Returns:
Correlation-adjusted total risk in USD.
"""
if not positions:
return 0.0
# Group by sector
sectors: dict = {}
for p in positions:
if p.sector not in sectors:
sectors[p.sector] = []
sectors[p.sector].append(p.total_risk)
total_variance = 0.0
for sector_risks in sectors.values():
n = len(sector_risks)
# Variance of each position's risk (treat risk as std dev proxy)
for risk in sector_risks:
total_variance += risk ** 2
# Cross-terms for correlated positions
for i in range(n):
for j in range(i + 1, n):
total_variance += 2 * default_corr * sector_risks[i] * sector_risks[j]
return math.sqrt(total_variance)
def calculate_available_budget(
account_size: float,
positions: list,
risk_pct_per_trade: float = DEFAULT_RISK_PCT,
) -> dict:
"""Calculate available risk budget for new positions.
Args:
account_size: Total account value.
positions: Current positions.
risk_pct_per_trade: Risk percentage for new trade.
Returns:
Dictionary with budget metrics and recommended size.
"""
metrics = calculate_portfolio_metrics(positions, account_size)
sectors = calculate_sector_exposure(positions, account_size)
# Remaining exposure capacity
max_exposure = account_size * MAX_TOTAL_EXPOSURE_PCT
remaining_exposure = max(0, max_exposure - metrics["total_value"])
# Max single position
max_single = account_size * MAX_SINGLE_POSITION_PCT
# Effective max for new position
max_new_position = min(remaining_exposure, max_single)
# Risk budget
risk_for_new_trade = account_size * risk_pct_per_trade
# Sector limits
sector_room: dict = {}
max_sector_value = account_size * MAX_CORRELATED_PCT
for sector, data in sectors.items():
room = max(0, max_sector_value - data["total_value"])
sector_room[sector] = room
return {
"remaining_exposure": remaining_exposure,
"max_single_position": max_single,
"max_new_position": max_new_position,
"risk_budget": risk_for_new_trade,
"sector_room": sector_room,
"can_add_position": remaining_exposure > 0,
}
# ── Report Formatting ──────────────────────────────────────────────
def fmt(val: float, decimals: int = 2) -> str:
"""Format a number with commas."""
return f"{val:,.{decimals}f}"
def print_header(title: str) -> None:
"""Print a section header."""
width = 68
print(f"\n{'=' * width}")
print(f" {title}")
print(f"{'=' * width}")
def print_portfolio_report(
positions: list,
account_size: float,
) -> None:
"""Print the complete portfolio risk dashboard.
Args:
positions: List of Position objects.
account_size: Total account value.
"""
metrics = calculate_portfolio_metrics(positions, account_size)
sectors = calculate_sector_exposure(positions, account_size)
corr_risk = calculate_correlation_adjusted_risk(positions)
budget = calculate_available_budget(account_size, positions)
# ── Account Overview ────────────────────────────────────────
print_header("PORTFOLIO RISK DASHBOARD")
print(f" Account Size: ${fmt(account_size)}")
print(f" Total Invested: ${fmt(metrics['total_value'])} ({metrics['exposure_pct']:.1f}%)")
print(f" Cash Available: ${fmt(metrics['cash'])} ({metrics['cash_pct']:.1f}%)")
print(f" Unrealized P&L: ${fmt(metrics['total_pnl'])} ({metrics['total_pnl'] / account_size * 100:+.2f}%)")
print(f" Active Positions: {metrics['num_positions']}")
# ── Per-Position Breakdown ──────────────────────────────────
print_header("POSITION BREAKDOWN")
header = f" {'Symbol':<8} {'Value':>10} {'% Acct':>8} {'P&L':>10} {'P&L%':>8} {'Risk$':>10} {'Risk%':>8} {'Sector':<10}"
print(header)
print(f" {'-' * 74}")
for p in sorted(positions, key=lambda x: x.position_value, reverse=True):
pct_acct = (p.position_value / account_size) * 100
risk_pct_acct = (p.total_risk / account_size) * 100
print(
f" {p.symbol:<8} "
f"${fmt(p.position_value):>9} "
f"{pct_acct:>7.1f}% "
f"${fmt(p.unrealized_pnl):>9} "
f"{p.unrealized_pnl_pct:>+7.1f}% "
f"${fmt(p.total_risk):>9} "
f"{risk_pct_acct:>7.1f}% "
f"{p.sector:<10}"
)
print(f" {'-' * 74}")
total_risk_pct = (metrics['total_risk'] / account_size) * 100
print(
f" {'TOTAL':<8} "
f"${fmt(metrics['total_value']):>9} "
f"{metrics['exposure_pct']:>7.1f}% "
f"${fmt(metrics['total_pnl']):>9} "
f"{'':>8} "
f"${fmt(metrics['total_risk']):>9} "
f"{total_risk_pct:>7.1f}% "
)
# ── Risk Analysis ───────────────────────────────────────────
print_header("RISK ANALYSIS")
print(f" Simple Total Risk: ${fmt(metrics['total_risk'])} ({total_risk_pct:.1f}% of account)")
print(f" Correlation-Adjusted Risk: ${fmt(corr_risk)} ({corr_risk / account_size * 100:.1f}% of account)")
print(f" Diversification Benefit: ${fmt(metrics['total_risk'] - corr_risk)} saved by diversification")
if metrics["total_risk"] > 0:
div_ratio = corr_risk / metrics["total_risk"]
print(f" Diversification Ratio: {div_ratio:.2f} (1.0 = no benefit, lower = better)")
# ── Sector Exposure ─────────────────────────────────────────
print_header("SECTOR EXPOSURE")
print(f" {'Sector':<12} {'Positions':<20} {'Value':>10} {'% Acct':>8} {'Risk$':>10} {'Limit':>10}")
print(f" {'-' * 70}")
max_sector = account_size * MAX_CORRELATED_PCT
for sector, data in sorted(sectors.items(), key=lambda x: x[1]["total_value"], reverse=True):
pos_str = ", ".join(data["positions"])
over = " OVER" if data["total_value"] > max_sector else ""
print(
f" {sector:<12} "
f"{pos_str:<20} "
f"${fmt(data['total_value']):>9} "
f"{data['exposure_pct']:>7.1f}% "
f"${fmt(data['total_risk']):>9} "
f"${fmt(max_sector):>9}{over}"
)
# ── Portfolio Limit Checks ──────────────────────────────────
print_header("LIMIT CHECKS")
checks = [
(
"Total exposure < 70%",
metrics["exposure_pct"] <= MAX_TOTAL_EXPOSURE_PCT * 100,
f"{metrics['exposure_pct']:.1f}%",
),
(
"Each position < 10%",
all((p.position_value / account_size) * 100 <= MAX_SINGLE_POSITION_PCT * 100 for p in positions),
f"max {max((p.position_value / account_size) * 100 for p in positions):.1f}%" if positions else "N/A",
),
]
# Check each sector
for sector, data in sectors.items():
ok = data["exposure_pct"] <= MAX_CORRELATED_PCT * 100
checks.append((
f"{sector} sector < 25%",
ok,
f"{data['exposure_pct']:.1f}%",
))
for label, passed, detail in checks:
status = "PASS" if passed else "FAIL"
print(f" [{status}] {label:<35} ({detail})")
# ── Available Budget ────────────────────────────────────────
print_header("AVAILABLE BUDGET FOR NEXT TRADE")
print(f" Remaining exposure capacity: ${fmt(budget['remaining_exposure'])}")
print(f" Max single position: ${fmt(budget['max_single_position'])}")
print(f" Max new position value: ${fmt(budget['max_new_position'])}")
print(f" Risk budget (2% of account): ${fmt(budget['risk_budget'])}")
if budget["can_add_position"]:
print(f"\n You can open a new position up to ${fmt(budget['max_new_position'])} in value.")
print(f" With 2% risk ({fmt(budget['risk_budget'])} USD), and a 10% stop,")
print(f" that allows ~${fmt(budget['risk_budget'] / 0.10)} notional position.")
else:
print("\n Portfolio is at maximum exposure. Close or reduce a position before adding new ones.")
if budget["sector_room"]:
print("\n Room by sector before hitting 25% limit:")
for sector, room in sorted(budget["sector_room"].items(), key=lambda x: x[1]):
print(f" {sector:<12} ${fmt(room)} remaining")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the portfolio sizer with demo or configured data."""
positions = get_demo_positions()
if ACCOUNT_SIZE <= 0:
print("Error: ACCOUNT_SIZE must be positive")
sys.exit(1)
print("Using demo portfolio (5 positions)")
print(f"Account size: ${fmt(ACCOUNT_SIZE)} (set ACCOUNT_SIZE env var to change)")
print_portfolio_report(positions, ACCOUNT_SIZE)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Position size calculator using multiple sizing methods.
Calculates position size using fixed fractional, volatility-adjusted,
Kelly criterion, and liquidity-constrained methods. Shows the binding
constraint and provides a formatted report.
Usage:
python scripts/size_calculator.py
Or with environment variables:
ACCOUNT_SIZE=10000 ENTRY_PRICE=1.50 STOP_LOSS=1.30 python scripts/size_calculator.py
Dependencies:
None (pure math, no external packages required)
Environment Variables:
ACCOUNT_SIZE: Account value in USD (default: 10000)
ENTRY_PRICE: Entry price per unit (default: 1.50)
STOP_LOSS: Stop loss price per unit (default: 1.30)
WIN_RATE: Historical win rate 0-1 (default: 0.55)
AVG_WIN_RATIO: Average win / average loss ratio (default: 1.8)
POOL_LIQUIDITY: Total pool liquidity in USD (default: 50000)
ATR_VALUE: 14-period ATR in price units (default: 0.12)
TARGET_VOL_PCT: Target daily portfolio vol as decimal (default: 0.02)
FEE_RATE: One-way trading fee as decimal (default: 0.003)
SLIPPAGE_EST: Estimated slippage as decimal (default: 0.005)
"""
import os
import sys
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
def get_float_env(name: str, default: float) -> float:
"""Read a float from an environment variable with a default."""
val = os.getenv(name, "")
if not val:
return default
try:
return float(val)
except ValueError:
print(f"Warning: {name}='{val}' is not a valid number, using default {default}")
return default
ACCOUNT_SIZE = get_float_env("ACCOUNT_SIZE", 10_000.0)
ENTRY_PRICE = get_float_env("ENTRY_PRICE", 1.50)
STOP_LOSS = get_float_env("STOP_LOSS", 1.30)
WIN_RATE = get_float_env("WIN_RATE", 0.55)
AVG_WIN_RATIO = get_float_env("AVG_WIN_RATIO", 1.8)
POOL_LIQUIDITY = get_float_env("POOL_LIQUIDITY", 50_000.0)
ATR_VALUE = get_float_env("ATR_VALUE", 0.12)
TARGET_VOL_PCT = get_float_env("TARGET_VOL_PCT", 0.02)
FEE_RATE = get_float_env("FEE_RATE", 0.003)
SLIPPAGE_EST = get_float_env("SLIPPAGE_EST", 0.005)
# ── Fixed Fractional Sizing ────────────────────────────────────────
def fixed_fractional(
account: float,
risk_pct: float,
entry: float,
stop: float,
fee_rate: float = 0.0,
slippage: float = 0.0,
) -> dict:
"""Calculate position size using fixed fractional method.
Args:
account: Total account value.
risk_pct: Fraction of account to risk (e.g., 0.02 for 2%).
entry: Entry price per unit.
stop: Stop loss price per unit.
fee_rate: One-way fee rate (applied to entry and exit).
slippage: Estimated slippage as fraction of entry price.
Returns:
Dictionary with units, value, risk_amount, and effective_risk.
"""
risk_amount = account * risk_pct
price_risk = abs(entry - stop)
fee_cost = entry * fee_rate * 2 # round-trip fees
slippage_cost = entry * slippage
effective_risk = price_risk + fee_cost + slippage_cost
if effective_risk <= 0:
return {"units": 0.0, "value": 0.0, "risk_amount": risk_amount, "effective_risk": 0.0}
units = risk_amount / effective_risk
value = units * entry
return {
"units": units,
"value": value,
"risk_amount": risk_amount,
"effective_risk": effective_risk,
"price_risk": price_risk,
"fee_cost": fee_cost,
"slippage_cost": slippage_cost,
}
# ── Volatility-Adjusted Sizing ─────────────────────────────────────
def volatility_adjusted(
account: float,
target_vol_pct: float,
atr: float,
entry: float,
) -> dict:
"""Calculate position size scaled by volatility.
Args:
account: Total account value.
target_vol_pct: Target daily portfolio volatility as decimal.
atr: Average True Range (14-period) in price units.
entry: Current price per unit.
Returns:
Dictionary with units, value, and volatility metrics.
"""
if atr <= 0:
return {"units": 0.0, "value": 0.0, "daily_vol_pct": 0.0, "expected_daily_pnl": 0.0}
target_daily_pnl = account * target_vol_pct
daily_vol_pct = atr / entry
units = target_daily_pnl / atr
value = units * entry
expected_daily_pnl = units * atr
return {
"units": units,
"value": value,
"daily_vol_pct": daily_vol_pct,
"expected_daily_pnl": expected_daily_pnl,
"target_daily_pnl": target_daily_pnl,
}
# ── Kelly Criterion Sizing ─────────────────────────────────────────
def kelly_criterion(
account: float,
win_rate: float,
payoff_ratio: float,
entry: float,
stop: float,
fraction: float = 0.25,
) -> dict:
"""Calculate position size using Kelly criterion.
Args:
account: Total account value.
win_rate: Probability of winning trade (0-1).
payoff_ratio: Average win / average loss.
entry: Entry price per unit.
stop: Stop loss price per unit.
fraction: Kelly fraction to use (0.25 recommended).
Returns:
Dictionary with full Kelly, fractional Kelly, units, and value.
"""
q = 1.0 - win_rate
if payoff_ratio <= 0:
return {
"full_kelly": 0.0, "fractional_kelly": 0.0, "fraction_used": fraction,
"units": 0.0, "value": 0.0, "has_edge": False,
}
full_kelly = (win_rate * payoff_ratio - q) / payoff_ratio
has_edge = full_kelly > 0
fractional_kelly = max(0.0, full_kelly * fraction)
price_risk = abs(entry - stop)
if price_risk <= 0 or not has_edge:
units = 0.0
else:
risk_amount = account * fractional_kelly
units = risk_amount / price_risk
value = units * entry
return {
"full_kelly": full_kelly,
"fractional_kelly": fractional_kelly,
"fraction_used": fraction,
"units": units,
"value": value,
"has_edge": has_edge,
"expected_growth_fraction": fractional_kelly * (2 - fractional_kelly / max(full_kelly, 1e-9)),
}
# ── Liquidity-Constrained Sizing ───────────────────────────────────
def liquidity_constrained(
pool_liquidity: float,
max_slippage_pct: float,
entry: float,
) -> dict:
"""Calculate maximum position size based on pool liquidity.
Args:
pool_liquidity: Total pool liquidity in USD.
max_slippage_pct: Maximum acceptable slippage as decimal.
entry: Token price per unit.
Returns:
Dictionary with max_value, max_units, and expected slippage.
"""
if pool_liquidity <= 0 or entry <= 0:
return {"max_value": 0.0, "max_units": 0.0, "expected_slippage_pct": 0.0}
max_value = pool_liquidity * max_slippage_pct
max_units = max_value / entry
expected_slippage_pct = max_slippage_pct
return {
"max_value": max_value,
"max_units": max_units,
"expected_slippage_pct": expected_slippage_pct,
"pool_liquidity": pool_liquidity,
"position_as_pct_of_pool": (max_value / pool_liquidity) * 100,
}
# ── R:R Targets ────────────────────────────────────────────────────
def calculate_rr_targets(
entry: float,
stop: float,
units: float,
) -> list:
"""Calculate reward-to-risk targets for a given position.
Args:
entry: Entry price.
stop: Stop loss price.
units: Number of units in position.
Returns:
List of dicts with R multiple, target price, and P&L.
"""
risk_per_unit = abs(entry - stop)
direction = 1 if entry > stop else -1 # long if stop below entry
targets = []
for r_multiple in [1.0, 1.5, 2.0, 3.0, 5.0]:
target_price = entry + direction * risk_per_unit * r_multiple
pnl = units * direction * (target_price - entry)
targets.append({
"r_multiple": r_multiple,
"target_price": round(target_price, 6),
"pnl": round(pnl, 2),
})
return targets
# ── Report Formatting ──────────────────────────────────────────────
def format_number(val: float, decimals: int = 2) -> str:
"""Format a number with commas and specified decimals."""
return f"{val:,.{decimals}f}"
def print_header(title: str) -> None:
"""Print a section header."""
width = 60
print(f"\n{'=' * width}")
print(f" {title}")
print(f"{'=' * width}")
def print_row(label: str, value: str, width: int = 40) -> None:
"""Print a label-value row."""
print(f" {label:<{width}} {value}")
def print_report(
account: float,
entry: float,
stop: float,
win_rate: float,
payoff_ratio: float,
pool_liquidity: float,
atr: float,
target_vol_pct: float,
fee_rate: float,
slippage_est: float,
) -> None:
"""Print the complete position sizing report.
Args:
account: Account value in USD.
entry: Entry price per unit.
stop: Stop loss price per unit.
win_rate: Historical win rate (0-1).
payoff_ratio: Avg win / avg loss.
pool_liquidity: Pool liquidity in USD.
atr: 14-period ATR in price units.
target_vol_pct: Target daily vol as decimal.
fee_rate: One-way fee rate.
slippage_est: Estimated slippage fraction.
"""
# ── Input Summary ───────────────────────────────────────────
print_header("POSITION SIZE CALCULATOR")
print_row("Account Size", f"${format_number(account)}")
print_row("Entry Price", f"${format_number(entry, 6)}")
print_row("Stop Loss", f"${format_number(stop, 6)}")
print_row("Price Risk", f"${format_number(abs(entry - stop), 6)} ({abs(entry - stop) / entry * 100:.1f}%)")
print_row("Win Rate", f"{win_rate * 100:.1f}%")
print_row("Payoff Ratio", f"{payoff_ratio:.2f}x")
print_row("Pool Liquidity", f"${format_number(pool_liquidity)}")
print_row("ATR(14)", f"${format_number(atr, 6)}")
print_row("Fee Rate (one-way)", f"{fee_rate * 100:.2f}%")
print_row("Slippage Estimate", f"{slippage_est * 100:.2f}%")
# ── Fixed Fractional ────────────────────────────────────────
print_header("METHOD 1: FIXED FRACTIONAL")
results_ff = {}
for risk_pct in [0.01, 0.02, 0.03]:
ff = fixed_fractional(account, risk_pct, entry, stop, fee_rate, slippage_est)
results_ff[risk_pct] = ff
label = f"{risk_pct * 100:.0f}% risk"
print_row(
label,
f"{format_number(ff['units'])} units | ${format_number(ff['value'])} value | ${format_number(ff['risk_amount'])} at risk",
)
ff_default = results_ff[0.02]
print(f"\n Fee-adjusted risk per unit: ${format_number(ff_default['effective_risk'], 4)}")
print(f" (Price risk ${format_number(ff_default['price_risk'], 4)} + fees ${format_number(ff_default['fee_cost'], 4)} + slippage ${format_number(ff_default['slippage_cost'], 4)})")
# ── Volatility-Adjusted ─────────────────────────────────────
print_header("METHOD 2: VOLATILITY-ADJUSTED")
va = volatility_adjusted(account, target_vol_pct, atr, entry)
print_row("Daily Vol (ATR/Price)", f"{va['daily_vol_pct'] * 100:.1f}%")
print_row("Target Daily PnL", f"${format_number(va['target_daily_pnl'])}")
print_row("Position Size", f"{format_number(va['units'])} units")
print_row("Position Value", f"${format_number(va['value'])}")
print_row("Expected Daily PnL Range", f"+/- ${format_number(va['expected_daily_pnl'])}")
# ── Kelly Criterion ─────────────────────────────────────────
print_header("METHOD 3: KELLY CRITERION")
kelly_results = {}
for frac, label in [(1.0, "Full Kelly"), (0.5, "Half Kelly"), (0.25, "Quarter Kelly")]:
kc = kelly_criterion(account, win_rate, payoff_ratio, entry, stop, frac)
kelly_results[frac] = kc
edge_str = "" if kc["has_edge"] else " [NO EDGE]"
print_row(
f"{label} ({frac:.0%})",
f"{kc['fractional_kelly'] * 100:.1f}% risk | {format_number(kc['units'])} units | ${format_number(kc['value'])}{edge_str}",
)
full_kc = kelly_results[1.0]
print(f"\n Full Kelly fraction: {full_kc['full_kelly'] * 100:.2f}%")
if not full_kc["has_edge"]:
print(" *** NEGATIVE KELLY: No statistical edge detected. Do not trade. ***")
else:
print(" Recommendation: Use Quarter Kelly (0.25x) as default")
# ── Liquidity-Constrained ───────────────────────────────────
print_header("METHOD 4: LIQUIDITY-CONSTRAINED")
liq_results = {}
for slip_pct in [0.01, 0.02, 0.05]:
lc = liquidity_constrained(pool_liquidity, slip_pct, entry)
liq_results[slip_pct] = lc
print_row(
f"{slip_pct * 100:.0f}% max slippage",
f"{format_number(lc['max_units'])} units | ${format_number(lc['max_value'])} max value | {lc['position_as_pct_of_pool']:.1f}% of pool",
)
# ── Combined Recommendation ─────────────────────────────────
print_header("RECOMMENDATION (BINDING CONSTRAINT)")
candidates = {
"Fixed Fractional (2%)": ff_default["units"],
"Volatility-Adjusted": va["units"],
"Quarter Kelly": kelly_results[0.25]["units"],
"Liquidity (2% slip)": liq_results[0.02]["max_units"],
}
# Filter out zero/negative
valid = {k: v for k, v in candidates.items() if v > 0}
if not valid:
print(" No valid position size found. Check inputs.")
return
binding_method = min(valid, key=valid.get)
recommended_units = valid[binding_method]
recommended_value = recommended_units * entry
pct_of_account = (recommended_value / account) * 100
print_row("Binding Constraint", binding_method)
print_row("Recommended Size", f"{format_number(recommended_units)} units")
print_row("Position Value", f"${format_number(recommended_value)}")
print_row("% of Account", f"{pct_of_account:.1f}%")
# Check portfolio limits
print("\n Portfolio limit checks:")
single_ok = pct_of_account <= 10
print(f" Single position < 10%: {'PASS' if single_ok else 'FAIL'} ({pct_of_account:.1f}%)")
print("\n All methods compared:")
for method, units in sorted(candidates.items(), key=lambda x: x[1]):
marker = " <-- BINDING" if method == binding_method else ""
val = units * entry
print(f" {method:<30} {format_number(units):>12} units ${format_number(val):>12}{marker}")
# ── R:R Targets ─────────────────────────────────────────────
print_header("R:R TARGETS (at recommended size)")
risk_per_unit = abs(entry - stop)
risk_total = recommended_units * risk_per_unit
targets = calculate_rr_targets(entry, stop, recommended_units)
print_row("Risk per trade", f"${format_number(risk_total)}")
print()
print(f" {'R:R':<8} {'Target Price':<16} {'P&L':<16} {'% of Account'}")
print(f" {'-' * 56}")
for t in targets:
pct = (t["pnl"] / account) * 100
print(f" {t['r_multiple']:<8.1f} ${format_number(t['target_price'], 6):<14} ${format_number(t['pnl']):<14} {pct:+.2f}%")
# ── Validation ──────────────────────────────────────────────────────
def validate_inputs(
account: float,
entry: float,
stop: float,
win_rate: float,
payoff_ratio: float,
pool_liquidity: float,
) -> list:
"""Validate inputs and return list of warning messages.
Args:
account: Account size.
entry: Entry price.
stop: Stop loss price.
win_rate: Win rate (0-1).
payoff_ratio: Avg win / avg loss.
pool_liquidity: Pool liquidity.
Returns:
List of warning/error strings. Empty list means all OK.
"""
errors: list = []
if account <= 0:
errors.append("Account size must be positive")
if entry <= 0:
errors.append("Entry price must be positive")
if stop <= 0:
errors.append("Stop loss must be positive")
if entry == stop:
errors.append("Entry and stop loss cannot be the same price")
if not 0 < win_rate < 1:
errors.append(f"Win rate must be between 0 and 1, got {win_rate}")
if payoff_ratio <= 0:
errors.append("Payoff ratio must be positive")
if pool_liquidity <= 0:
errors.append("Pool liquidity must be positive")
# Warnings (non-fatal)
risk_pct = abs(entry - stop) / entry * 100
if risk_pct > 30:
errors.append(f"Warning: Stop distance is {risk_pct:.1f}% from entry (very wide)")
if pool_liquidity < account * 0.1:
errors.append("Warning: Pool liquidity is less than 10% of account size")
return errors
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the position size calculator with configured parameters."""
issues = validate_inputs(
ACCOUNT_SIZE, ENTRY_PRICE, STOP_LOSS,
WIN_RATE, AVG_WIN_RATIO, POOL_LIQUIDITY,
)
fatal = [i for i in issues if not i.startswith("Warning")]
warnings = [i for i in issues if i.startswith("Warning")]
if fatal:
print("Input errors:")
for e in fatal:
print(f" - {e}")
sys.exit(1)
if warnings:
print("Warnings:")
for w in warnings:
print(f" - {w}")
print_report(
account=ACCOUNT_SIZE,
entry=ENTRY_PRICE,
stop=STOP_LOSS,
win_rate=WIN_RATE,
payoff_ratio=AVG_WIN_RATIO,
pool_liquidity=POOL_LIQUIDITY,
atr=ATR_VALUE,
target_vol_pct=TARGET_VOL_PCT,
fee_rate=FEE_RATE,
slippage_est=SLIPPAGE_EST,
)
if __name__ == "__main__":
main()
Related skills
FAQ
Which method is recommended?
Fixed fractional sizing is described as the most recommended for most traders, risking a fixed percentage of the account per trade.
Should you use full Kelly?
No; the skill says to never use full Kelly and to use fractional Kelly (0.25x default) because edge estimates are noisy.