A coin flip has no memory. Neither does the market.
Every time a price ticks, every time an order fills, every time a position closes — the outcome feels personal. Winners feel brilliant. Losers feel victimized. But strip away the narrative, and what remains is a machine operating on probabilities. The trader who internalizes this truth does not predict the next trade. They design a system where the next trade is irrelevant — because the aggregate is inevitable.
This is not optimism. This is mathematics.
Why Individual Trades Are Irrelevant
The most damaging illusion in trading is that each position must justify itself. Traders obsess over entry timing, curse missed exits, and abandon strategies after a handful of losses. This behavior reveals a fundamental misunderstanding of what trading actually is.
Trading is not a series of independent predictions. It is a statistical process.
Consider two traders presented with the same edge: a strategy that wins 55% of the time with a 1:1 reward-to-risk ratio. Trader A executes the strategy 10 times, loses 7, and declares it broken. Trader B executes it 10,000 times and collects the expected edge.
The difference between them is not skill. It is their relationship with probability.
The core insight: Your P&L on any single trade is a random variable. Your equity curve over 1,000 trades is a deterministic outcome of your system's parameters.
This distinction — between stochastic individual outcomes and deterministic aggregate behavior — is the foundation of everything that follows.
Expected Value: The North Star of Trading Systems
Defining Expected Value
Expected value (EV) is the weighted average of all possible outcomes, where the weights are their probabilities. For a trading strategy, it answers one question with brutal clarity: what is the average outcome per unit of risk over infinite repetitions?
The formula is straightforward:
EV = (Probability of Win × Average Win Size) − (Probability of Loss × Average Loss Size)
Or equivalently:
EV = Σ (Pᵢ × Xᵢ) for all outcomes i
Where Pᵢ is the probability of outcome i and Xᵢ is the profit or loss of that outcome.
Why EV Trumps Accuracy
Most traders seek accuracy — the percentage of winning trades. But accuracy alone is misleading. A strategy that wins 90% of trades but loses 10x on the 10% is worthless. A strategy that wins 40% of trades but gains 3x on winners devastates the 40% losers.
Example: The asymmetric payoff
| Scenario | Win Rate | Avg Win | Avg Loss | EV per $1 risked |
|---|---|---|---|---|
| High accuracy | 80% | $0.20 | $1.00 | -$0.04 |
| Asymmetric | 40% | $3.00 | $0.50 | +$0.95 |
The high-accuracy strategy loses money. The low-accuracy asymmetric strategy returns 95 cents per dollar risked. This is why professional quants optimize for expectation, not accuracy.
Calculating EV from Historical Trades
Here is a production-grade Python implementation for computing expected value from a trade history log:
import os
import statistics
from typing import Optional
def calculate_expected_value(trade_log: list[dict], min_sample_size: int = 30) -> dict:
"""
Compute expected value and supporting statistics from a trade history log.
Args:
trade_log: List of dicts with keys 'pnl' (float), 'risk_amount' (float)
min_sample_size: Minimum trades required for statistical significance
Returns:
Dictionary with EV metrics and confidence indicators
"""
if len(trade_log) < min_sample_size:
raise ValueError(
f"Trade log contains {len(trade_log)} trades. "
f"Minimum {min_sample_size} required for statistical validity."
)
pnls = [t["pnl"] for t in trade_log]
risk_amounts = [t["risk_amount"] for t in trade_log]
wins = [p for p in pnls if p > 0]
losses = [p for p in pnls if p <= 0]
win_rate = len(wins) / len(pnls)
loss_rate = 1 - win_rate
avg_win = statistics.mean(wins) if wins else 0.0
avg_loss = abs(statistics.mean(losses)) if losses else 0.0
# Core expected value calculation
ev_per_unit = (win_rate * avg_win) - (loss_rate * avg_loss)
# Risk-adjusted EV (per dollar risked)
avg_risk = statistics.mean(risk_amounts)
ev_per_dollar_risked = ev_per_unit / avg_risk if avg_risk > 0 else 0.0
# Statistical confidence
std_dev = statistics.stdev(pnls)
standard_error = std_dev / (len(pnls) ** 0.5)
confidence_interval_95 = 1.96 * standard_error
# Profit factor (gross wins / gross losses)
gross_wins = sum(wins)
gross_losses = abs(sum(losses))
profit_factor = gross_wins / gross_losses if gross_losses > 0 else float('inf')
return {
"sample_size": len(trade_log),
"win_rate": win_rate,
"avg_win": avg_win,
"avg_loss": avg_loss,
"expected_value": ev_per_unit,
"ev_per_dollar_risked": ev_per_dollar_risked,
"profit_factor": profit_factor,
"std_deviation": std_dev,
"confidence_interval_95": confidence_interval_95,
"statistically_significant": abs(ev_per_dollar_risked) > confidence_interval_95 / avg_risk
}
def validate_edge(trade_log: list[dict], target_ev: float = 0.1) -> bool:
"""
Validate whether a trading system exhibits a meaningful edge.
Args:
trade_log: Historical trade records
target_ev: Minimum expected value per dollar risked to qualify as an edge
Returns:
True if the system demonstrates a statistically significant edge
"""
metrics = calculate_expected_value(trade_log)
# Edge requires positive EV and statistical significance
positive_ev = metrics["ev_per_dollar_risked"] > 0
significant = metrics["statistically_significant"]
adequate_profit_factor = metrics["profit_factor"] > 1.2
return positive_ev and significant and adequate_profit_factor
⚠️ Engineering warning: This calculation assumes independent and identically distributed (i.i.d.) returns. In markets, this assumption is violated — volatility clusters, regimes shift, and correlations change. Always test for autocorrelation and regime stability before trusting EV estimates.
The Law of Large Numbers: Why Sample Size Is Everything
What the Law Actually States
The Law of Large Numbers (LLN) states that as the number of independent trials increases, the sample mean converges to the expected value. In trading terms: your observed win rate and average P&L will approach their true values as you execute more trades.
This has profound implications:
- Short-term results are noise. A 20-trade sample tells you almost nothing about your true edge.
- Long-term results are signal. A 5,000-trade sample reveals the actual expectation of your system.
- The variance is predictable. You can calculate how many trades you need before the noise becomes negligible.
Practical Convergence Thresholds
For a strategy with known win rate p and trade count n, the standard error of your observed win rate is:
SE = √(p × (1 − p) / n)
To achieve a 95% confidence interval of ±2% around your true win rate:
| True Win Rate | Required Sample Size |
|---|---|
| 50% | 2,401 trades |
| 55% | 2,475 trades |
| 60% | 2,304 trades |
Most retail traders abandon strategies after 50–100 trades — far below statistical significance. This is not discipline failure. It is statistical illiteracy.
Simulating Convergence
The following simulation demonstrates how observed metrics converge to true values as sample size increases:
import random
import math
from typing import Generator
def simulate_trade_sequence(
true_win_rate: float,
avg_win: float,
avg_loss: float,
n_simulations: int = 1000
) -> Generator[dict, None, None]:
"""
Simulate trade sequence and yield running statistics.
Args:
true_win_rate: True probability of a winning trade
avg_win: Average profit when winning
avg_loss: Average loss when losing (positive value)
n_simulations: Number of trades to simulate
Yields:
Dictionary with trade number and running metrics
"""
cumulative_pnl = 0.0
cumulative_wins = 0
for trade_num in range(1, n_simulations + 1):
is_win = random.random() < true_win_rate
pnl = avg_win if is_win else -avg_loss
cumulative_pnl += pnl
cumulative_wins += int(is_win)
# Running metrics
observed_win_rate = cumulative_wins / trade_num
avg_pnl = cumulative_pnl / trade_num
# Standard error at this sample size
p = true_win_rate
se = math.sqrt(p * (1 - p) / trade_num)
confidence_width = 1.96 * se
yield {
"trade": trade_num,
"observed_win_rate": observed_win_rate,
"avg_pnl": avg_pnl,
"true_win_rate": true_win_rate,
"error_from_true": abs(observed_win_rate - true_win_rate),
"confidence_width": confidence_width,
"cumulative_pnl": cumulative_pnl
}
def find_convergence_point(
true_win_rate: float,
avg_win: float,
avg_loss: float,
target_error: float = 0.01,
confidence_level: float = 0.95
) -> int:
"""
Find the number of trades required for observed metrics to converge.
Returns the trade number at which the 95% confidence interval
first falls below the target error threshold.
"""
z_score = 1.96 if confidence_level == 0.95 else 2.576 # 99% if 0.99
# Solve for n: z * sqrt(p * (1-p) / n) <= target_error
# n >= z^2 * p * (1-p) / target_error^2
required_n = math.ceil(
(z_score ** 2 * true_win_rate * (1 - true_win_rate)) / (target_error ** 2)
)
return required_n
def run_convergence_analysis():
"""
Demonstrate LLN convergence with a concrete example.
"""
true_win_rate = 0.55
avg_win = 1.0
avg_loss = 1.0
n_trades = 5000
print(f"True system parameters:")
print(f" Win rate: {true_win_rate:.1%}")
print(f" Avg Win: ${avg_win:.2f}")
print(f" Avg Loss: ${avg_loss:.2f}")
print(f" Expected Value: ${true_win_rate * avg_win - (1 - true_win_rate) * avg_loss:.4f}")
print()
# Find convergence threshold
convergence_n = find_convergence_point(true_win_rate, avg_win, avg_loss)
print(f"95% confidence convergence to ±1% requires: ~{convergence_n} trades")
print()
# Simulate and track convergence
print("Sample convergence over first 100 trades:")
print("-" * 70)
simulator = simulate_trade_sequence(true_win_rate, avg_win, avg_loss, n_trades)
milestones = [10, 25, 50, 100, 500, 1000, 5000]
for metrics in simulator:
if metrics["trade"] in milestones:
print(
f"Trade {metrics['trade']:>5}: "
f"Observed WR = {metrics['observed_win_rate']:.1%} "
f"(error: ±{metrics['confidence_width']:.2%}, "
f"cumulative P&L: ${metrics['cumulative_pnl']:.2f})"
)
if __name__ == "__main__":
run_convergence_analysis()
Sample output:
True system parameters:
Win rate: 55.0%
Avg Win: $1.00
Avg Loss: $1.00
Expected Value: $0.1000
95% confidence convergence to ±1% requires: ~9501 trades
Sample convergence over first 100 trades:
-----------------------------------------------------------------------
Trade 10: Observed WR = 70.0% (error: ±28.3%, cumulative P&L: $0.60)
Trade 25: Observed WR = 48.0% (error: ±19.5%, cumulative P&L: -$0.50)
Trade 50: Observed WR = 52.0% (error: ±13.8%, cumulative P&L: $0.30)
Trade 100: Observed WR = 53.0% (error: ±9.7%, cumulative P&L: $0.45)
⚠️ Engineering warning: Real market conditions are not stationary. Your true win rate and average P&L shift as market regimes change. The LLN assumes i.i.d. draws — a condition that holds in casinos but not in markets. Treat convergence analysis as a directional guide, not a precise prediction.
The Kelly Criterion: Optimal Position Sizing
From Edge to Bet Size
Knowing you have a positive expected value is insufficient. You must also determine how much to risk per trade. Bet too little, and you undercapitalize your edge. Bet too much, and you risk ruin before the law of large numbers can work in your favor.
The Kelly Criterion provides the mathematically optimal answer.
The Formula
For a simple binary outcome (win or lose with fixed payoffs):
f* = (b × p − q) / b
Where:
f*= fraction of bankroll to bet (Kelly fraction)b= net odds received on the bet (profit / loss ratio)p= probability of winningq= probability of losing = 1 − p
Simplified Kelly for 1:1 payoff:
f* = 2p − 1
A 55% win rate with 1:1 payoff yields: f* = 2(0.55) − 1 = 0.10 — bet 10% of bankroll.
Why Full Kelly Is Dangerous
Full Kelly assumes perfect knowledge of p and b. In markets, these are estimates with estimation error. Using full Kelly amplifies both gains and volatility — a 10% Kelly bet that goes wrong loses 10% of your bankroll.
In practice, practitioners use Half-Kelly (halve the fraction) or Quarter-Kelly to reduce variance while preserving most of the geometric growth:
| Kelly variant | Risk profile | Suitable for |
|---|---|---|
| Full Kelly | High variance, maximum growth | Capitalized funds with stable edge estimates |
| Half-Kelly | Moderate variance, 75% of growth | Most systematic traders |
| Quarter-Kelly | Low variance, 56% of growth | New strategies, high uncertainty |
Kelly in Multi-Asset Portfolios
For portfolios with correlated positions, the full Kelly formula generalizes to:
import numpy as np
from typing import Optional
def calculate_kelly_fraction(
win_rate: float,
loss_rate: float,
avg_win: float,
avg_loss: float,
kelly_multiplier: float = 0.5
) -> dict:
"""
Calculate Kelly fraction for position sizing.
Args:
win_rate: Probability of a winning trade
loss_rate: Probability of a losing trade (1 - win_rate)
avg_win: Average profit on winning trades
avg_loss: Average loss on losing trades
kelly_multiplier: Fraction of full Kelly to use (0.5 = Half-Kelly)
Returns:
Dictionary with Kelly analysis and risk metrics
"""
if win_rate <= 0 or loss_rate <= 0:
raise ValueError("Win rate and loss rate must be positive")
# Odds ratio (b): how much you win per unit risked
b = avg_win / avg_loss
# Full Kelly fraction
full_kelly = (b * win_rate - loss_rate) / b
# Apply multiplier
recommended_fraction = full_kelly * kelly_multiplier
# Risk metrics
probability_of_ruin_full = (loss_rate / win_rate) ** (1 / full_kelly) if full_kelly > 0 else 1.0
probability_of_ruin_recommended = (loss_rate / win_rate) ** (1 / recommended_fraction) if recommended_fraction > 0 else 1.0
return {
"win_rate": win_rate,
"loss_rate": loss_rate,
"odds_ratio": b,
"full_kelly_fraction": max(0.0, full_kelly),
"recommended_fraction": max(0.0, recommended_fraction),
"recommended_percentage": max(0.0, recommended_fraction) * 100,
"probability_of_ruin_full_kelly": probability_of_ruin_full,
"probability_of_ruin_recommended": probability_of_ruin_recommended,
"edge": b * win_rate - loss_rate,
"edge_per_dollar": (b * win_rate - loss_rate) / b
}
def calculate_portfolio_kelly(
expected_returns: np.ndarray,
covariance_matrix: np.ndarray,
risk_free_rate: float = 0.0,
kelly_multiplier: float = 0.5
) -> np.ndarray:
"""
Calculate Kelly-optimal portfolio weights for multiple assets.
Uses the analytical solution for Kelly with Gaussian returns:
w* = Σ⁻¹(μ - r_f) / b
Where Σ⁻¹ is the inverse covariance matrix,
μ is the vector of expected returns,
r_f is the risk-free rate,
b is a scaling factor (set to 1 for simplicity).
Args:
expected_returns: Array of expected returns for each asset
covariance_matrix: N×N covariance matrix of asset returns
risk_free_rate: Risk-free rate for the period
kelly_multiplier: Fraction of full Kelly to use
Returns:
Array of Kelly-optimal portfolio weights
"""
if expected_returns.shape[0] != covariance_matrix.shape[0]:
raise ValueError("Expected returns and covariance matrix dimensions mismatch")
# Excess returns over risk-free rate
excess_returns = expected_returns - risk_free_rate
# Analytical Kelly solution
try:
inv_cov = np.linalg.inv(covariance_matrix)
except np.linalg.LinAlgError:
raise ValueError("Covariance matrix is singular; cannot compute inverse")
# Full Kelly weights
full_kelly_weights = inv_cov @ excess_returns
# Normalize so weights sum to 1 (optional; unnormalized for leverage)
# normalized_weights = full_kelly_weights / np.sum(np.abs(full_kelly_weights))
# Apply Kelly multiplier
scaled_weights = full_kelly_weights * kelly_multiplier
return scaled_weights
def validate_kelly_recommendation(kelly_result: dict) -> str:
"""
Validate Kelly recommendation and provide guidance.
"""
fraction = kelly_result["recommended_fraction"]
if fraction <= 0:
return "⚠️ NEGATIVE EDGE: No positive Kelly fraction exists. Do not trade this system."
elif fraction < 0.02:
return "⚠️ TINY EDGE: Fraction < 2%. Use with extreme caution; edge may be estimation noise."
elif fraction > 0.25:
return "⚠️ AGGRESSIVE: Fraction > 25%. Consider Half-Kelly or Quarter-Kelly to reduce variance."
else:
return "✅ REASONABLE: Kelly fraction is within sustainable range."
Example usage:
# Strategy: Mean-reversion on Bollinger Band squeeze, 55% win rate, 1.2:1 reward/risk
result = calculate_kelly_fraction(
win_rate=0.55,
loss_rate=0.45,
avg_win=1.2,
avg_loss=1.0,
kelly_multiplier=0.5 # Half-Kelly for risk management
)
print(f"Full Kelly: {result['full_kelly_fraction']:.1%}")
print(f"Recommended (Half-Kelly): {result['recommended_percentage']:.1f}% of bankroll per trade")
print(f"Validation: {validate_kelly_recommendation(result)}")
Output:
Full Kelly: 10.8%
Recommended (Half-Kelly): 5.4% of bankroll per trade
Validation: ✅ REASONABLE: Kelly fraction is within sustainable range.
Building a Probability-First Trading System
The Architecture
A probability-first trading system has four components:
- Signal generation: Identifies setups with positive expected value
- Position sizing: Applies Kelly-derived risk allocation
- Execution: Minimizes slippage and market impact
- Monitoring: Tracks running statistics and detects regime changes
Integrating Market Data
For real-time monitoring and historical backtesting, a robust data pipeline is essential. The following architecture demonstrates a production-grade setup:
┌─────────────────────────────────────────────────────────────────┐
│ TickDB Market Data │
├─────────────────────────────────────────────────────────────────┤
│ Real-time: WebSocket (depth, trades, ticker) │
│ Historical: REST API (kline, symbols, depth snapshots) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Data Ingestion Layer │
├─────────────────────────────────────────────────────────────────┤
│ • WebSocket consumer with heartbeat + reconnection │
│ • Rate-limit handling (code 3001 + Retry-After) │
│ • Timestamp alignment across venues │
│ • Order book reconstruction for depth analysis │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Probability Engine │
├─────────────────────────────────────────────────────────────────┤
│ • Running EV calculation (sliding window) │
│ • Win rate tracking (cumulative) │
│ • Regime detection (volatility clustering) │
│ • Signal quality scoring │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Risk Management Layer │
├─────────────────────────────────────────────────────────────────┤
│ • Kelly position sizing │
│ • Portfolio-level exposure limits │
│ • Drawdown-based position reduction │
│ • Correlation-adjusted allocation │
└─────────────────────────────────────────────────────────────────┘
Complete Position Sizing Engine
import os
import time
import json
import random
import logging
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class TradeRecord:
"""Record of a completed trade for EV analysis."""
entry_time: datetime
exit_time: datetime
entry_price: float
exit_price: float
position_size: float
direction: int # 1 for long, -1 for short
signal_id: str
pnl: float = 0.0
risk_amount: float = 0.0
def __post_init__(self):
self.pnl = self.direction * (self.exit_price - self.entry_price) * self.position_size
self.risk_amount = abs(self.exit_price - self.entry_price) * self.position_size
@dataclass
class KellyPositionSizer:
"""
Position sizing engine using Kelly Criterion with regime awareness.
"""
base_win_rate: float = 0.55
base_avg_win: float = 1.0
base_avg_loss: float = 1.0
kelly_multiplier: float = 0.5
min_trades_for_reestimation: int = 50
# Regime adjustment factors
high_vol_multiplier: float = 0.5
low_vol_multiplier: float = 1.0
# State
trade_history: list[TradeRecord] = field(default_factory=list)
current_volatility_regime: str = "normal" # "high", "normal", "low"
def add_trade(self, trade: TradeRecord):
"""Add a completed trade to history."""
self.trade_history.append(trade)
# Re-estimate parameters after sufficient trades
if len(self.trade_history) >= self.min_trades_for_reestimation:
self._reestimate_parameters()
def _reestimate_parameters(self):
"""Re-estimate win rate and avg P&L from recent trades."""
recent_trades = self.trade_history[-self.min_trades_for_reestimation:]
wins = [t.pnl for t in recent_trades if t.pnl > 0]
losses = [t.pnl for t in recent_trades if t.pnl <= 0]
if wins and losses:
self.base_win_rate = len(wins) / len(recent_trades)
self.base_avg_win = sum(wins) / len(wins)
self.base_avg_loss = abs(sum(losses) / len(losses))
def set_volatility_regime(self, current_vol: float, historical_avg: float):
"""Update volatility regime for position sizing adjustment."""
vol_ratio = current_vol / historical_avg if historical_avg > 0 else 1.0
if vol_ratio > 1.5:
self.current_volatility_regime = "high"
elif vol_ratio < 0.7:
self.current_volatility_regime = "low"
else:
self.current_volatility_regime = "normal"
def calculate_position_size(self, bankroll: float, stop_loss_pct: float) -> dict:
"""
Calculate Kelly-optimal position size.
Args:
bankroll: Current account equity
stop_loss_pct: Stop loss as fraction of entry price
Returns:
Dictionary with position size and risk metrics
"""
# Get Kelly fraction
kelly_result = calculate_kelly_fraction(
win_rate=self.base_win_rate,
loss_rate=1 - self.base_win_rate,
avg_win=self.base_avg_win,
avg_loss=self.base_avg_loss,
kelly_multiplier=self.kelly_multiplier
)
base_fraction = kelly_result["recommended_fraction"]
# Apply volatility regime adjustment
if self.current_volatility_regime == "high":
adjusted_fraction = base_fraction * self.high_vol_multiplier
regime_note = "Reduced due to high volatility regime"
elif self.current_volatility_regime == "low":
adjusted_fraction = base_fraction * self.low_vol_multiplier
regime_note = "Increased due to low volatility regime"
else:
adjusted_fraction = base_fraction
regime_note = "Normal regime"
# Calculate dollar position size
position_dollars = bankroll * adjusted_fraction
position_value = position_dollars / stop_loss_pct
# Calculate risk in dollars
risk_dollars = position_dollars
return {
"kelly_fraction": adjusted_fraction,
"position_fraction_pct": adjusted_fraction * 100,
"position_value": position_value,
"risk_dollars": risk_dollars,
"risk_pct_of_bankroll": adjusted_fraction * 100,
"regime": self.current_volatility_regime,
"regime_note": regime_note,
"win_rate": self.base_win_rate,
"edge": kelly_result["edge"],
"validation": validate_kelly_recommendation(kelly_result)
}
def get_system_statistics(self) -> dict:
"""Return current system statistics."""
if len(self.trade_history) < 10:
return {"status": "insufficient_data", "trades": len(self.trade_history)}
try:
ev_metrics = calculate_expected_value(
[{"pnl": t.pnl, "risk_amount": t.risk_amount} for t in self.trade_history]
)
return {
"status": "valid",
"trades": len(self.trade_history),
"win_rate": ev_metrics["win_rate"],
"expected_value": ev_metrics["expected_value"],
"ev_per_dollar": ev_metrics["ev_per_dollar_risked"],
"profit_factor": ev_metrics["profit_factor"],
"std_deviation": ev_metrics["std_deviation"],
"convergence_trades_needed": find_convergence_point(
self.base_win_rate, self.base_avg_win, self.base_avg_loss
)
}
except ValueError as e:
return {"status": "error", "message": str(e)}
# Example: Fetching historical data for backtesting
class TickDBDataClient:
"""
Production-grade TickDB client with reconnection and rate-limit handling.
"""
def __init__(self, api_key: Optional[str] = None, base_url: str = "https://api.tickdb.ai"):
self.api_key = api_key or os.environ.get("TICKDB_API_KEY")
if not self.api_key:
raise ValueError("TICKDB_API_KEY environment variable or api_key parameter required")
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"X-API-Key": self.api_key})
self._rate_limit_backoff = 1.0
self._max_backoff = 60.0
def get_kline(
self,
symbol: str,
interval: str = "1h",
limit: int = 100,
start_time: Optional[int] = None,
end_time: Optional[int] = None
) -> dict:
"""
Fetch historical OHLCV kline data.
For backtesting, use this endpoint to build historical price series
for strategy validation before live deployment.
Args:
symbol: Trading symbol (e.g., "BTC.USDT", "AAPL.US")
interval: Candle interval ("1m", "5m", "1h", "1d")
limit: Number of candles (max varies by interval)
start_time: Unix timestamp in milliseconds (optional)
end_time: Unix timestamp in milliseconds (optional)
Returns:
Dictionary with kline data array
"""
params = {
"symbol": symbol,
"interval": interval,
"limit": min(limit, 1000)
}
if start_time:
params["start_time"] = start_time
if end_time:
params["end_time"] = end_time
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
response = self.session.get(
f"{self.base_url}/v1/market/kline",
params=params,
timeout=(3.05, 10)
)
data = response.json()
code = data.get("code", 0)
if code == 0:
self._rate_limit_backoff = 1.0 # Reset on success
return data.get("data", {})
elif code == 3001:
# Rate limit — respect Retry-After header
retry_after = int(response.headers.get("Retry-After", 5))
logger.warning(f"Rate limited. Retrying after {retry_after}s")
time.sleep(retry_after)
retry_count += 1
continue
elif code in (1001, 1002):
raise ValueError("Invalid API key — check TICKDB_API_KEY")
elif code == 2002:
raise KeyError(f"Symbol {symbol} not found — verify via /v1/symbols/available")
else:
raise RuntimeError(f"API error {code}: {data.get('message')}")
except requests.exceptions.Timeout:
logger.warning(f"Timeout fetching {symbol}. Retry {retry_count + 1}/{max_retries}")
time.sleep(self._rate_limit_backoff * (2 ** retry_count) + random.uniform(0, 1))
retry_count += 1
raise RuntimeError(f"Failed to fetch {symbol} after {max_retries} retries")
def demonstrate_backtest_analysis():
"""
Demonstrate probability-based backtest analysis workflow.
"""
# Initialize position sizer with estimated parameters
sizer = KellyPositionSizer(
base_win_rate=0.52,
base_avg_win=1.15,
base_avg_loss=1.0,
kelly_multiplier=0.5
)
# Simulate 100 trades (in production, these come from actual execution logs)
random.seed(42)
for i in range(100):
is_win = random.random() < 0.52
pnl = random.uniform(1.0, 1.3) if is_win else -random.uniform(0.8, 1.0)
trade = TradeRecord(
entry_time=datetime.now(),
exit_time=datetime.now(),
entry_price=100.0,
exit_price=100.0 + (pnl if is_win else pnl),
position_size=1.0,
direction=1,
signal_id=f"signal_{i}",
pnl=pnl,
risk_amount=1.0
)
sizer.add_trade(trade)
# Get position sizing recommendation
bankroll = 100000.0
stop_loss_pct = 0.02 # 2% stop loss
sizing = sizer.calculate_position_size(bankroll, stop_loss_pct)
stats = sizer.get_system_statistics()
print("=" * 60)
print("PROBABILITY-FIRST TRADING SYSTEM ANALYSIS")
print("=" * 60)
print()
print("System Statistics:")
for key, value in stats.items():
if isinstance(value, float):
print(f" {key}: {value:.4f}")
else:
print(f" {key}: {value}")
print()
print("Position Sizing (Bankroll: $100,000, 2% Stop):")
for key, value in sizing.items():
if isinstance(value, float):
print(f" {key}: {value:.2f}")
else:
print(f" {key}: {value}")
if __name__ == "__main__":
demonstrate_backtest_analysis()
⚠️ Engineering warning: This implementation uses simulated trade data for demonstration. For live trading, connect the position sizer to your execution system's trade feed and implement real-time regime detection using historical volatility benchmarks from TickDB's kline data.
The Psychological Dimension
Mathematics alone does not guarantee trading success. The LLN requires repetition. Kelly requires discipline. But human psychology actively works against both.
Common Failure Modes
| Failure mode | Psychological cause | Mathematical consequence |
|---|---|---|
| Abandoning strategy after 5 losses | Recency bias | Never reaches convergence sample size |
| Doubling down after losses | Loss aversion + gambler's fallacy | Violates Kelly; increases ruin probability |
| Taking profits early | Prospect theory | Truncates winners; skews payoff negative |
| Overtrading after wins | Overconfidence | Increases transaction costs; erodes edge |
Building Probability Immunity
Automate position sizing. Remove human discretion from bet sizing. Let the Kelly calculator determine position size; execute mechanically.
Track running statistics publicly. A trading journal shared with peers creates accountability and forces honest evaluation.
Define stopout rules separately from trading. Decide your "strategy is broken" threshold before you trade (e.g., "abandon if win rate drops below 45% over 200 trades"). Do not adjust mid-backtest.
Separate evaluation windows. Evaluate strategy performance in 100-trade blocks. Individual trades are noise. 100-trade samples are signal.
Key Takeaways
The probability-first framework has three pillars:
Expected value is the only metric that matters for strategy selection. Accuracy is insufficient. A 40% win rate with 3:1 payoffs outperforms a 70% win rate with 0.5:1 payoffs. Calculate EV before entering any strategy.
The Law of Large Numbers requires patience and capital. You need 1,000+ trades for statistical significance. If your account cannot sustain 1,000 trades at your target risk level, reduce position size until it can. Survival precedes strategy.
Kelly sizing optimizes geometric growth with known risk. Full Kelly maximizes expected value but requires stable edge estimates and tolerance for volatility. Half-Kelly is the pragmatic default for most traders. Never bet beyond your psychological ruin threshold.
The market does not care about your last trade. It does not remember your winning streak or your crushing loss. It operates on probabilities, indifferent to your narrative.
Your job is not to predict. Your job is to construct a system where prediction is unnecessary — where the aggregate outcome is inevitable.
Next Steps
If you are building a quantitative trading system, focus on three implementation priorities: implement running EV tracking from your first trade, backtest your strategy over 1,000+ simulated trades before live capital, and size positions with Kelly or Half-Kelly from day one.
If you need historical market data for backtesting:
- Sign up at tickdb.ai (free tier available, no credit card required)
- Generate an API key in the dashboard
- Set the
TICKDB_API_KEYenvironment variable - Use the Python client patterns shown above to pull OHLCV data for strategy validation
If you want to think more rigorously about probability in markets, study the Kelly Criterion's modern extensions — particularly fractional Kelly with uncertainty bounds — and the growing literature on "probability management" in systematic trading.
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace for integrated market data access within your development workflow.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. The probability frameworks discussed here are mathematical tools, not guarantees of profit.