diff --git a/docs/superpowers/analysis/bankroll_sizing/simulate.py b/docs/superpowers/analysis/bankroll_sizing/simulate.py new file mode 100644 index 00000000..41f89e67 --- /dev/null +++ b/docs/superpowers/analysis/bankroll_sizing/simulate.py @@ -0,0 +1,622 @@ +#!/usr/bin/env python3 +"""Bankroll-sizing simulation: Kelly Criterion family vs `keel`'s fixed-fractional risk sizing. + +Educational / halal framing +---------------------------- +`keel` is a halal (riba-free) spot-crypto, long-only, no-leverage trading agent. It sizes +positions with fixed-fractional risk sizing (`keel/execution/sizing.py::size`): risk a constant +`risk_pct` of equity per trade over the entry-to-stop distance, config default `risk_pct = 0.01` +(1%). This script is a pure MATHEMATICS study of capital allocation -- comparing that fixed 1% +against the Kelly Criterion and its relatives (from the `keeks` family of formulas) -- using +simulated coin-flip-style trade sequences. It is not gambling and nothing here trades real money; +it is a stdlib-only Monte Carlo exercise in bankroll-growth arithmetic, run to answer one +question: is keel's 1% risk needlessly timid relative to what the math says is "optimal," and if +so, is there a good reason (estimation error, drawdown pain) to stay timid anyway? + +Thesis under test +------------------ +keel's promotion floor requires win_rate >= 0.55 and R:R (min_rr) >= 1.5. For a rule sitting +exactly at that floor (p=0.55, b=1.5), full-Kelly f* = (b*p - q)/b = 0.25 (25% of equity risked +per trade), half-Kelly = 12.5%, quarter-Kelly = 6.25%. keel's actual 1% is about 4% of full Kelly. +Two experiments below interrogate this gap. + +Determinism +------------ +Every path is driven by `random.Random(seed)` with an explicit integer seed derived from a fixed +base seed and the path index, so a re-run of this script reproduces identical numbers. All +strategies being compared on a given path index share the same seed (i.e. draw the same win/loss +sequence up to the point any of them stops from ruin) -- classic "common random numbers" variance +reduction so the comparison across strategies isn't muddied by different strategies happening to +see different luck. + +Run: `python simulate.py` (or `python docs/superpowers/analysis/bankroll_sizing/simulate.py` from +the repo root). Writes the markdown report to +`docs/superpowers/reports/2026-07-22-bankroll-sizing-comparison.md`. +""" + +from __future__ import annotations + +import random +import statistics +import sys +from collections.abc import Callable +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +from sizing_strategies import ( # noqa: E402 + cppi_fraction, + drawdown_adjusted_kelly, + fixed_fraction, + fractional_kelly, + kelly_fraction, + naive_flat_fraction, +) + +REPO_ROOT = HERE.parents[3] # docs/superpowers/analysis/bankroll_sizing -> repo root +REPORT_PATH = ( + REPO_ROOT / "docs" / "superpowers" / "reports" / "2026-07-22-bankroll-sizing-comparison.md" +) + +RUIN_THRESHOLD = 1.0 # bankroll <= $1 counts as ruin; path stops (can't go negative) + +FractionFn = Callable[[dict], float] + + +# --------------------------------------------------------------------------------------------- +# Strategy factories: each returns a stateless callable(state) -> risk fraction in [0, 1]. +# `state` is a dict with the running per-path values: bankroll, peak (equity high-water mark), +# initial (starting bankroll). Strategies that need "current drawdown" or "distance above a +# ratcheting floor" derive it from `state` each call rather than holding their own mutable state, +# so a single strategy instance can be reused across many independent paths safely. +# --------------------------------------------------------------------------------------------- + + +def strategy_full_kelly(p: float, b: float) -> FractionFn: + f = kelly_fraction(p, b) + return lambda state: f + + +def strategy_fractional_kelly(p: float, b: float, lam: float) -> FractionFn: + f = fractional_kelly(p, b, lam) + return lambda state: f + + +def strategy_fixed(c: float) -> FractionFn: + f = fixed_fraction(c) + return lambda state: f + + +def strategy_naive_flat(stake: float) -> FractionFn: + return lambda state: naive_flat_fraction(stake, state["bankroll"]) + + +def strategy_cppi(floor_ratio: float, multiplier: float) -> FractionFn: + def fn(state: dict) -> float: + floor = floor_ratio * state["peak"] + return cppi_fraction(state["bankroll"], floor, multiplier) + + return fn + + +def strategy_drawdown_kelly(p: float, b: float, max_dd: float) -> FractionFn: + def fn(state: dict) -> float: + peak = state["peak"] + bankroll = state["bankroll"] + current_dd = 0.0 if peak <= 0 else max(0.0, (peak - bankroll) / peak) + return drawdown_adjusted_kelly(p, b, current_dd, max_dd) + + return fn + + +# --------------------------------------------------------------------------------------------- +# Core path simulator +# --------------------------------------------------------------------------------------------- + + +def simulate_path( + fraction_fn: FractionFn, + n_bets: int, + p: float, + b: float, + seed: int, + initial: float, +) -> dict: + """Simulate one bankroll path of up to `n_bets` trades. + + Each trade: with probability `p` it wins, paying `+b * f * bankroll`; otherwise it loses + `f * bankroll`, where `f = fraction_fn(state)` is recomputed fresh before every trade (so + adaptive strategies -- CPPI, drawdown-adjusted Kelly, naive-flat -- respond to the bankroll's + current level and history). The path stops early if bankroll drops to or below + `RUIN_THRESHOLD` ("can't go negative"; ruin is recorded). + + Returns a dict: terminal bankroll, the path's max drawdown from its own running peak, and + whether it was ruined. + """ + rng = random.Random(seed) + bankroll = initial + peak = initial + max_dd = 0.0 + ruined = False + + for _ in range(n_bets): + if bankroll <= RUIN_THRESHOLD: + ruined = True + break + + state = {"bankroll": bankroll, "peak": peak, "initial": initial} + f = fraction_fn(state) + f = max(0.0, min(1.0, f)) + + win = rng.random() < p + if win: + bankroll *= 1.0 + b * f + else: + bankroll *= 1.0 - f + + peak = max(peak, bankroll) + dd = 0.0 if peak <= 0 else (peak - bankroll) / peak + max_dd = max(max_dd, dd) + + if bankroll <= RUIN_THRESHOLD: + ruined = True + + return {"terminal": bankroll, "max_dd": max_dd, "ruined": ruined} + + +def run_paths( + strategies: dict[str, FractionFn], + n_paths: int, + n_bets: int, + p: float, + b: float, + base_seed: int, + initial: float, +) -> dict[str, dict[str, list]]: + """Run `n_paths` seeded paths for every strategy in `strategies`, using the same seed per + path index across strategies (common random numbers).""" + results: dict[str, dict[str, list]] = { + name: {"terminal": [], "max_dd": [], "ruined": []} for name in strategies + } + for i in range(n_paths): + seed = base_seed + i + for name, fn in strategies.items(): + r = simulate_path(fn, n_bets, p, b, seed, initial) + results[name]["terminal"].append(r["terminal"]) + results[name]["max_dd"].append(r["max_dd"]) + results[name]["ruined"].append(r["ruined"]) + return results + + +def summarize(results: dict[str, dict[str, list]]) -> dict[str, dict[str, float]]: + summary: dict[str, dict[str, float]] = {} + for name, r in results.items(): + terminal = r["terminal"] + max_dd = r["max_dd"] + ruined = r["ruined"] + summary[name] = { + "median_terminal": statistics.median(terminal), + "mean_terminal": statistics.mean(terminal), + "stdev_terminal": statistics.stdev(terminal) if len(terminal) > 1 else 0.0, + "median_max_dd": statistics.median(max_dd), + "worst_max_dd": max(max_dd), + "ruin_rate": sum(ruined) / len(ruined), + } + return summary + + +# --------------------------------------------------------------------------------------------- +# Experiment 1: reproduce the keeks binary comparison +# --------------------------------------------------------------------------------------------- + +EXP1_N_BETS = 1000 +EXP1_N_PATHS = 500 +EXP1_INITIAL = 1000.0 +EXP1_P = 0.55 +EXP1_B = 1.0 +EXP1_BASE_SEED = 10_000 +EXP1_MAX_DD_TOLERANCE = 0.20 + + +def experiment_1() -> dict[str, dict[str, float]]: + strategies: dict[str, FractionFn] = { + "Full Kelly": strategy_full_kelly(EXP1_P, EXP1_B), + "Half Kelly (0.5)": strategy_fractional_kelly(EXP1_P, EXP1_B, 0.5), + "Quarter Kelly (0.25)": strategy_fractional_kelly(EXP1_P, EXP1_B, 0.25), + "Fixed-1% (keel)": strategy_fixed(0.01), + "CPPI (floor=0.8, m=3)": strategy_cppi(0.8, 3.0), + "Naive-flat ($10)": strategy_naive_flat(10.0), + "Drawdown-adj Kelly (max_dd=0.20)": strategy_drawdown_kelly( + EXP1_P, EXP1_B, EXP1_MAX_DD_TOLERANCE + ), + } + results = run_paths( + strategies, EXP1_N_PATHS, EXP1_N_BETS, EXP1_P, EXP1_B, EXP1_BASE_SEED, EXP1_INITIAL + ) + return summarize(results) + + +# --------------------------------------------------------------------------------------------- +# Experiment 2: keel-relevant -- risk_pct vs the Kelly family at our floor edge (and a stronger +# edge), plus an estimation-error stress test (true p is 5 points lower than assumed). +# --------------------------------------------------------------------------------------------- + +EXP2_N_BETS = 200 +EXP2_N_PATHS = 500 +EXP2_INITIAL = 1000.0 +EXP2_KEEL_RISK_PCT = 0.01 +EXP2_P_ERROR = 0.05 # estimation-error stress: true p = assumed p - this + +PROFILES = { + "A (floor edge: p=0.55, b=1.5)": {"p": 0.55, "b": 1.5}, + "B (stronger edge: p=0.58, b=2.0)": {"p": 0.58, "b": 2.0}, +} + + +def kelly_levels_for(p: float, b: float) -> dict[str, float]: + full = kelly_fraction(p, b) + return { + "keel-1%": EXP2_KEEL_RISK_PCT, + "Quarter-Kelly": fractional_kelly(p, b, 0.25), + "Half-Kelly": fractional_kelly(p, b, 0.5), + "Full-Kelly": full, + } + + +def experiment_2() -> dict[str, dict]: + """Returns, per profile, per world ("p correct" / "p over-estimated by 0.05"), the summary + stats for each constant-risk-fraction sizing level.""" + out: dict[str, dict] = {} + base_seed = 20_000 + for profile_idx, (profile_name, params) in enumerate(PROFILES.items()): + assumed_p, b = params["p"], params["b"] + levels = kelly_levels_for(assumed_p, b) + strategies: dict[str, FractionFn] = { + name: strategy_fixed(f) for name, f in levels.items() + } + + worlds = { + "p correct": assumed_p, + f"p over-estimated by {EXP2_P_ERROR:.2f}": assumed_p - EXP2_P_ERROR, + } + + profile_out: dict[str, dict] = {"kelly_levels": levels, "worlds": {}} + for world_idx, (world_name, true_p) in enumerate(worlds.items()): + seed = base_seed + profile_idx * 1_000_000 + world_idx * 500_000 + results = run_paths( + strategies, EXP2_N_PATHS, EXP2_N_BETS, true_p, b, seed, EXP2_INITIAL + ) + summary = summarize(results) + for name in summary: + summary[name]["median_multiple"] = summary[name]["median_terminal"] / EXP2_INITIAL + profile_out["worlds"][world_name] = summary + out[profile_name] = profile_out + return out + + +# --------------------------------------------------------------------------------------------- +# Report generation +# --------------------------------------------------------------------------------------------- + + +def fmt_money(x: float) -> str: + if x >= 1_000_000: + return f"${x:,.0f}" + return f"${x:,.2f}" + + +def fmt_pct(x: float) -> str: + return f"{x * 100:.2f}%" + + +def exp1_table(summary: dict[str, dict[str, float]]) -> str: + order = [ + "Full Kelly", + "Half Kelly (0.5)", + "Quarter Kelly (0.25)", + "Fixed-1% (keel)", + "CPPI (floor=0.8, m=3)", + "Naive-flat ($10)", + "Drawdown-adj Kelly (max_dd=0.20)", + ] + header = ( + "| Strategy | Median terminal | Mean terminal | Stdev terminal | " + "Median max DD | Ruin rate |\n" + "|---|---|---|---|---|---|\n" + ) + rows = [] + for name in order: + s = summary[name] + rows.append( + f"| {name} | {fmt_money(s['median_terminal'])} | {fmt_money(s['mean_terminal'])} | " + f"{fmt_money(s['stdev_terminal'])} | {fmt_pct(s['median_max_dd'])} | " + f"{fmt_pct(s['ruin_rate'])} |" + ) + return header + "\n".join(rows) + + +def exp2_table(profile_out: dict, level_order: list[str]) -> str: + header = ( + "| Sizing level | Risk fraction | World | Median terminal multiple | " + "Median max DD | Worst max DD | Ruin rate |\n" + "|---|---|---|---|---|---|---|\n" + ) + rows = [] + levels = profile_out["kelly_levels"] + for world_name, summary in profile_out["worlds"].items(): + for name in level_order: + s = summary[name] + rows.append( + f"| {name} | {fmt_pct(levels[name])} | {world_name} | " + f"{s['median_multiple']:.3f}x | {fmt_pct(s['median_max_dd'])} | " + f"{fmt_pct(s['worst_max_dd'])} | {fmt_pct(s['ruin_rate'])} |" + ) + return header + "\n".join(rows) + + +def build_report(exp1_summary: dict, exp2_results: dict) -> str: + level_order = ["keel-1%", "Quarter-Kelly", "Half-Kelly", "Full-Kelly"] + + floor_full = kelly_fraction(0.55, 1.5) + floor_half = fractional_kelly(0.55, 1.5, 0.5) + floor_quarter = fractional_kelly(0.55, 1.5, 0.25) + keel_vs_full_pct = EXP2_KEEL_RISK_PCT / floor_full * 100 + + lines: list[str] = [] + lines.append( + "# Bankroll Sizing Comparison: Kelly Criterion Family vs keel's Fixed-Fractional Risk" + ) + lines.append("") + lines.append("## Purpose and framing") + lines.append("") + lines.append( + "`keel` is a halal (riba-free), spot-only, long-only, no-leverage crypto trading agent. " + "It currently sizes every trade with fixed-fractional risk sizing " + "(`keel/execution/sizing.py::size`): risk a constant `risk_pct` of equity per trade over " + "the entry-to-stop distance, with a config default of `risk_pct = 0.01` (1%)." + ) + lines.append("") + lines.append( + "This report studies the Kelly Criterion and its relatives (drawn from the `keeks` " + "family of bankroll-growth formulas) **purely as mathematics of optimal capital " + "allocation** -- a Monte Carlo exercise in bankroll-growth arithmetic run against " + "simulated win/loss trade sequences with stdlib-only Python. Nothing here trades real " + "money, wagers on chance for its own sake, or involves interest (riba); it is a study of " + "how fast a bankroll compounds under different constant-risk-fraction rules, applied to " + "keel's own promotion-floor edge numbers, to ask an engineering question: is keel's fixed " + "1% risk needlessly conservative, or is there a good reason to stay conservative anyway?" + ) + lines.append("") + lines.append("## Thesis") + lines.append("") + lines.append( + "keel's PROMOTION FLOOR rule requires win_rate >= 0.55 and R:R (min_rr) >= 1.5 before a " + "strategy is promoted to live trading. For a rule sitting exactly at that floor " + "(p=0.55, b=1.5), full-Kelly risk fraction is:" + ) + lines.append("") + lines.append("```") + lines.append( + "f* = (b*p - q) / b = (1.5 * 0.55 - 0.45) / 1.5 = 0.375 / 1.5 = 0.25" + " (25% of equity per trade)" + ) + lines.append(f"half-Kelly = {floor_half:.4f} (12.50%)") + lines.append(f"quarter-Kelly = {floor_quarter:.4f} (6.25%)") + lines.append( + f"keel's actual risk_pct = 0.01 (1.00%) ~= {keel_vs_full_pct:.1f}% of full Kelly" + ) + lines.append("```") + lines.append("") + lines.append( + "The question: is keel leaving growth on the table by risking only ~4% of the " + "full-Kelly-implied fraction at its own promotion floor, or is sub-Kelly sizing correct " + "once you account for estimation error in `p`/`b`, correlation between trades, and " + "drawdown pain that a pure log-growth-maximizer ignores?" + ) + lines.append("") + lines.append("## Method") + lines.append("") + lines.append( + "Two deterministic (seeded) Monte Carlo experiments, implemented in `simulate.py` next " + "to this report, using only the Python standard library (`random`, `statistics`). Every " + "path uses `random.Random(seed)` with an explicit integer seed; strategies compared " + "within the same experiment share seeds per path index (common random numbers), so " + "differences between strategies reflect sizing, not differing luck. Money is modeled as " + "`float` (this is an educational sim, not the Decimal-only production `keel` sizing " + "code). A trade wins with probability `p`, paying `+b * f * bankroll` where `f` is the " + "fraction risked and `b` is the reward:risk multiple; a loss costs `f * bankroll`. A path " + "is considered ruined and stopped once bankroll falls to or below $1 (bankroll cannot go " + "negative under fractional betting, but going effectively to zero is treated as ruin)." + ) + lines.append("") + + lines.append("## Experiment 1: reproducing the keeks binary comparison") + lines.append("") + lines.append( + f"Setup: {EXP1_N_BETS} bets, p={EXP1_P}, even-money (b={EXP1_B}), initial bankroll " + f"${EXP1_INITIAL:,.0f}, {EXP1_N_PATHS} independent seeded paths per strategy. Strategies: " + "Full Kelly, Half Kelly, Quarter Kelly, Fixed-1% (keel), CPPI (floor ratchets at 80% of " + "peak equity, multiplier=3), Naive-flat ($10 constant stake), and Drawdown-adjusted Kelly " + "(scales full Kelly to zero as current drawdown approaches a 20% tolerance ceiling)." + ) + lines.append("") + lines.append(exp1_table(exp1_summary)) + lines.append("") + lines.append( + "**Reading this table**: Full Kelly has the highest median/mean terminal wealth, but " + "also the widest dispersion (stdev) and the deepest typical drawdowns -- the classic " + "Kelly trait of being growth-optimal in expectation while remaining a psychologically " + "brutal ride. Half- and Quarter-Kelly trade away some terminal wealth for a large cut in " + "drawdown depth and variance -- this is the textbook \"why half-Kelly\" lesson the " + "`keeks` library is built to demonstrate. keel's Fixed-1% sits far below all Kelly " + "variants on terminal wealth because it never lets its risk keep pace with a compounding " + "bankroll's *edge*, but it also never comes close to the Kelly variants' drawdowns. CPPI " + "at multiplier=3 with an 80%-of-peak floor risks a large fraction of the cushion " + "(60% of bankroll at a fresh high) -- well above this edge's Kelly-optimal level -- " + "and its results show the cost of over-levering an insurance-style rule. Naive-flat ($10) " + "decays into an ever-shrinking fraction of a growing bankroll (or a growing fraction of a " + "shrinking one), producing its own distinct, non-Kelly growth curve." + ) + lines.append("") + + lines.append("## Experiment 2: risk_pct vs the Kelly family at keel's own edge numbers") + lines.append("") + lines.append( + f"Setup: {EXP2_N_BETS}-trade bootstrap sequences, {EXP2_N_PATHS} seeded paths, initial " + f"bankroll ${EXP2_INITIAL:,.0f}. Each trade wins with probability `p` paying " + "`+b * (risk_pct * equity)`, else loses `risk_pct * equity`. Two edge profiles: (A) the " + "promotion floor itself, p=0.55, b=1.5; (B) a stronger edge, p=0.58, b=2.0. Sizing levels " + "are constant risk fractions: keel-1% (0.01), and the Quarter-/Half-/Full-Kelly fractions " + "implied by each profile's own p and b. For each profile, two worlds are simulated: " + "**p correct** (the realized win rate matches what sizing assumed) and " + f"**p over-estimated by {EXP2_P_ERROR:.2f}** (sizing was computed assuming the stated p, " + "but the true win rate actually realized is 5 percentage points lower -- an estimation-" + "error stress test)." + ) + lines.append("") + for profile_name, profile_out in exp2_results.items(): + lines.append(f"### Profile {profile_name}") + lines.append("") + levels = profile_out["kelly_levels"] + lines.append( + f"Kelly fractions for this profile: Quarter-Kelly={fmt_pct(levels['Quarter-Kelly'])}, " + f"Half-Kelly={fmt_pct(levels['Half-Kelly'])}, " + f"Full-Kelly={fmt_pct(levels['Full-Kelly'])}, " + f"keel-1%={fmt_pct(levels['keel-1%'])}." + ) + lines.append("") + lines.append(exp2_table(profile_out, level_order)) + lines.append("") + + lines.append("## What this means for keel") + lines.append("") + lines.append( + "**Is 1% too timid?** Mathematically, yes, relative to the growth-maximizing Kelly " + "fraction: at the promotion floor (p=0.55, b=1.5) full Kelly is 25% of equity per trade, " + "and keel's 1% is roughly 4% of that. In the \"p correct\" worlds of Experiment 2, every " + "Kelly-family fraction (even Quarter-Kelly) compounds to a dramatically larger median " + "terminal multiple than keel-1% over 200 trades, because 1% barely lets a real edge " + "compound -- the bankroll grows close to linearly rather than geometrically at that " + "scale. Purely as an optimal-growth-rate statement, the thesis holds: keel is far to the " + "conservative side of the Kelly curve." + ) + lines.append("") + # Figures for the narrative are formatted from Profile A's results (the first profile) so the + # prose can never drift from the tables above. `.0f` rounds the same way source-84.md quotes. + _pa_worlds = list(next(iter(exp2_results.values()))["worlds"].values()) + _fk_correct = _pa_worlds[0]["Full-Kelly"]["median_multiple"] + _fk_over = _pa_worlds[1]["Full-Kelly"]["median_multiple"] + _fk_over_ruin = _pa_worlds[1]["Full-Kelly"]["ruin_rate"] + lines.append( + "**Does the estimation-error run defend sub-Kelly?** Yes, and this is the more important " + "half of the story. In the \"p over-estimated by 0.05\" worlds, Full-Kelly's edge " + "assumption breaks: at the floor profile (b=1.5, breakeven p=0.40), an assumed p=0.55 " + "with a true p=0.50 is still a real edge -- full Kelly at the *true* p=0.50 would be " + "~16.7% (down from the 25% it was sized at), not zero -- but Full-Kelly was sized as if " + "the edge were 8-plus points thicker than it actually is, and that overbetting shows up " + f"directly in the numbers: median terminal multiple collapses from {_fk_correct:.0f}x " + f"(\"p correct\") to {_fk_over:.0f}x (\"p over-estimated\"), and a ruin rate that was " + f"0.0% becomes {fmt_pct(_fk_over_ruin)}. Half- and " + "Quarter-Kelly degrade far more gracefully under the identical misestimation (their ruin " + "rates stay at 0.0%), because they were never betting the full assumed edge in the first " + "place -- the classic argument for sub-Kelly sizing is that it functions as a margin of " + "safety against exactly the kind of parameter error a live trading system cannot avoid " + "(p and b are estimated from a finite, noisy backtest sample, not known constants). " + "keel's actual 1%, while far more conservative than even Quarter-Kelly, sits on the same " + "side of that argument as the fractional-Kelly strategies: it is far more robust to an " + "over-optimistic edge estimate than Full-Kelly is, just at a much larger cost in forgone " + "growth." + ) + lines.append("") + lines.append( + "**Net read**: the honest conclusion is that keel's 1% is not \"wrong\" -- it is an " + "extreme point on the same sub-Kelly safety spectrum that Half- and Quarter-Kelly occupy, " + "just pushed much further toward safety than the math alone would require. If keel's " + "backtested p and b estimates were trustworthy point estimates with no correlation " + "between trades, something in the Quarter-Kelly neighborhood (order of 5-6% at the floor " + "edge) would capture most of the available growth while still being far more robust to " + "estimation error than Full- or Half-Kelly. keel's actual 1% leaves a substantial amount " + "of that growth unclaimed. Whether closing some of that gap is worth it depends on " + "factors this simulation does not model (see Assumptions below) -- most importantly, real " + "trades are not independent, identically-distributed coin flips, and a backtest's p/b " + "point estimates carry real sampling uncertainty that a single-scenario stress test can " + "only gesture at." + ) + lines.append("") + lines.append("## Assumptions and honest limitations") + lines.append("") + lines.append( + "- **Independent, i.i.d. trades.** The simulation treats every trade as an independent " + "Bernoulli draw with fixed p and b. Real crypto trades from correlated strategies " + "(e.g. multiple concurrent BTC/ETH positions moving together in a market-wide drawdown) " + "violate this; correlated losses compound faster than this model's math accounts for, " + "which understates the real risk of any of the higher-fraction strategies (Full/Half " + "Kelly, CPPI at m=3)." + ) + lines.append( + "- **Known b, no fees/slippage.** `b` (R:R) is treated as a known constant per trade; " + "trading fees, slippage, and spread are not modeled. Real R:R realized on a live book is " + "noisier and typically worse than backtested R:R." + ) + lines.append( + "- **A single, fixed estimation-error stress test.** The \"p over-estimated by 0.05\" " + "world tests one specific magnitude of misestimation, not a distribution over possible " + "estimation errors. It illustrates the *direction* of the Full-Kelly fragility argument, " + "not a calibrated probability of it occurring." + ) + lines.append( + "- **No position limits, correlation caps, or per-order/per-day caps.** keel's real " + "guards (order caps, day caps, portfolio-level exposure limits) are not modeled here; " + "they are additional risk controls that a real deployment would layer on top of whatever " + "risk_pct is chosen, and they change the practical consequences of raising risk_pct." + ) + lines.append( + "- **Float money, not Decimal.** This sim uses `float` for bankroll math for simplicity; " + "keel's real sizing code (`keel/execution/sizing.py`) is Decimal-only by design, because " + "money should never touch float in the production path. That distinction does not change " + "the qualitative conclusions here but is worth flagging." + ) + lines.append( + "- **This is not a recommendation to change keel's risk_pct.** The result is a " + "mathematical observation about the growth/safety tradeoff at different Kelly fractions, " + "not a specific proposed new value; any change to keel's actual risk_pct would need its " + "own review against keel's real guard rails, correlation across live positions, and " + "backtest confidence -- none of which this script attempts to quantify." + ) + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + print("Running Experiment 1 (keeks binary comparison reproduction)...") + exp1_summary = experiment_1() + for name, s in exp1_summary.items(): + print( + f" {name}: median=${s['median_terminal']:.2f} mean=${s['mean_terminal']:.2f} " + f"stdev=${s['stdev_terminal']:.2f} median_dd={s['median_max_dd']:.4f} " + f"ruin_rate={s['ruin_rate']:.4f}" + ) + + print("Running Experiment 2 (keel-relevant risk_pct vs Kelly family)...") + exp2_results = experiment_2() + for profile_name, profile_out in exp2_results.items(): + print(f" Profile {profile_name}") + for world_name, summary in profile_out["worlds"].items(): + print(f" World: {world_name}") + for name, s in summary.items(): + print( + f" {name}: median_multiple={s['median_multiple']:.4f}x " + f"median_dd={s['median_max_dd']:.4f} worst_dd={s['worst_max_dd']:.4f} " + f"ruin_rate={s['ruin_rate']:.4f}" + ) + + report = build_report(exp1_summary, exp2_results) + REPORT_PATH.parent.mkdir(parents=True, exist_ok=True) + REPORT_PATH.write_text(report) + print(f"\nReport written to {REPORT_PATH}") + + +if __name__ == "__main__": + main() diff --git a/docs/superpowers/analysis/bankroll_sizing/sizing_strategies.py b/docs/superpowers/analysis/bankroll_sizing/sizing_strategies.py new file mode 100644 index 00000000..aca154fa --- /dev/null +++ b/docs/superpowers/analysis/bankroll_sizing/sizing_strategies.py @@ -0,0 +1,191 @@ +"""Bankroll-sizing strategies: pure functions returning a FRACTION OF BANKROLL TO RISK. + +This module is a stdlib-only, educational study of capital-allocation mathematics (the Kelly +Criterion and neighboring formulas from the `keeks` library and classic portfolio theory). It is +NOT betting advice and is not wired into `keel`'s execution path -- it exists to compare the +Kelly-optimal risk fraction against `keel`'s fixed-fractional risk sizing +(`keel/execution/sizing.py::size`, default `risk_pct = 0.01`). + +Convention used throughout: every function returns a fraction `f` in `[0, 1]` meaning "risk `f` +of current bankroll on this trade." Under the simplified binary/R-multiple trade model used by +the accompanying simulation (`simulate.py`): + - a LOSING trade costs the trader `f * bankroll` (the full risked amount), and + - a WINNING trade pays `b * f * bankroll` (b = reward:risk multiple, i.e. R:R). + +All functions clamp their output to `[0, 1]` and guard against degenerate inputs (zero/negative +odds, zero bankroll, etc.) rather than raising, so they are safe to call in a tight simulation +loop. Where guarding a bad input by clamping/zeroing would silently hide a real bug (e.g. +probabilities outside [0, 1]), we raise `ValueError` instead. +""" + +from __future__ import annotations + + +def _clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float: + """Clamp `x` into `[lo, hi]`.""" + return max(lo, min(hi, x)) + + +def kelly_fraction(p: float, b: float) -> float: + """Full Kelly criterion: the growth-optimal fraction of bankroll to risk per bet. + + Formula (Kelly, 1956; as used throughout the `keeks` library): + + f* = (b*p - q) / b, where q = 1 - p + + `p` is the win probability, `b` is the reward:risk multiple (a win pays `b` units per unit + risked; a loss costs 1 unit risked -- i.e. `b` is the trade's R:R). This is the fraction of + bankroll that maximizes the expected logarithm of terminal wealth (long-run geometric growth) + for a sequence of independent, identically-distributed bets with known `p` and `b`. + + Returns 0 if the edge is non-positive (`b*p - q <= 0`, i.e. no edge or a losing edge) rather + than a negative fraction (negative Kelly would mean "bet the other side," which does not + apply to a long-only, no-shorting instrument). Result is always clamped to `[0, 1]`. + + Raises `ValueError` if `p` is not in `[0, 1]` or `b <= 0`. + """ + if not (0.0 <= p <= 1.0): + raise ValueError(f"kelly_fraction: p must be in [0, 1], got {p}") + if b <= 0.0: + raise ValueError(f"kelly_fraction: b must be > 0, got {b}") + + q = 1.0 - p + edge = b * p - q + if edge <= 0.0: + return 0.0 + return _clamp(edge / b) + + +def fractional_kelly(p: float, b: float, lam: float) -> float: + """Fractional ("lambda") Kelly: `lam * kelly_fraction(p, b)`. + + Betting a fixed fraction `lam` (e.g. 0.5 for "half-Kelly", 0.25 for "quarter-Kelly") of the + full-Kelly stake trades away some geometric growth for a large reduction in variance and + drawdown depth -- the classic risk-management refinement of Kelly betting, since full Kelly + is highly sensitive to estimation error in `p`/`b` and produces violent equity swings even + when `p`/`b` are known exactly. + + Raises `ValueError` if `lam < 0` (a negative multiplier is not a fractional-Kelly scheme). + Result is clamped to `[0, 1]`. + """ + if lam < 0.0: + raise ValueError(f"fractional_kelly: lam must be >= 0, got {lam}") + + return _clamp(lam * kelly_fraction(p, b)) + + +def drawdown_adjusted_kelly(p: float, b: float, current_dd: float, max_dd: float) -> float: + """Dynamic, drawdown-scaled Kelly: shrink the Kelly stake as the current drawdown approaches + a tolerance ceiling `max_dd`. + + f = clamp(1 - current_dd/max_dd, 0, 1) * kelly_fraction(p, b) + + At `current_dd = 0` (no drawdown) this equals full Kelly. As `current_dd -> max_dd` the + multiplier linearly decays to 0, so a path that has already suffered its maximum tolerated + drawdown stops risking anything until it recovers. `current_dd >= max_dd` returns exactly 0. + + `current_dd` and `max_dd` are both fractions of peak equity (e.g. `current_dd = 0.10` means + "10% below the equity high-water mark"). Raises `ValueError` if `max_dd <= 0` or + `current_dd < 0`. + """ + if max_dd <= 0.0: + raise ValueError(f"drawdown_adjusted_kelly: max_dd must be > 0, got {max_dd}") + if current_dd < 0.0: + raise ValueError(f"drawdown_adjusted_kelly: current_dd must be >= 0, got {current_dd}") + + if current_dd >= max_dd: + return 0.0 + + scale = _clamp(1.0 - current_dd / max_dd) + return _clamp(scale * kelly_fraction(p, b)) + + +def fixed_fraction(c: float = 0.01) -> float: + """Constant fixed-fractional risk: always risk `c` of current bankroll. + + This is `keel`'s current live sizing scheme (`keel/execution/sizing.py::size`, config default + `risk_pct = 0.01` i.e. `c = 0.01`). It ignores `p` and `b` entirely -- the same fraction is + risked win-streak or lose-streak, edge-rich regime or edge-poor regime. Included here purely + as the baseline every Kelly-family strategy in this study is compared against. + + Raises `ValueError` if `c` is negative. Result is clamped to `[0, 1]`. + """ + if c < 0.0: + raise ValueError(f"fixed_fraction: c must be >= 0, got {c}") + + return _clamp(c) + + +def cppi_fraction(bankroll: float, floor: float, multiplier: float) -> float: + """Constant Proportional Portfolio Insurance (CPPI): risk a multiple of the "cushion" above a + protected floor. + + f = clamp(multiplier * (bankroll - floor) / bankroll, 0, 1) + + `floor` is a dollar level the strategy tries never to breach (in the accompanying simulation + this floor ratchets upward with new equity highs -- that ratcheting is external state managed + by the caller/simulation loop, not by this pure function). `multiplier` ("m") controls how + aggressively the cushion (`bankroll - floor`) is levered into risk; CPPI research/practice + commonly uses m in the 2-5 range. If `bankroll <= floor` (cushion exhausted or breached) this + returns 0 -- there is nothing left to safely risk. + + Raises `ValueError` if `bankroll <= 0` or `multiplier < 0`. + """ + if bankroll <= 0.0: + raise ValueError(f"cppi_fraction: bankroll must be > 0, got {bankroll}") + if multiplier < 0.0: + raise ValueError(f"cppi_fraction: multiplier must be >= 0, got {multiplier}") + + cushion = bankroll - floor + if cushion <= 0.0: + return 0.0 + return _clamp(multiplier * cushion / bankroll) + + +def naive_flat_fraction(fixed_stake: float, bankroll: float) -> float: + """Flat-stake baseline: risk a constant DOLLAR amount `fixed_stake`, expressed as a fraction + of current `bankroll`. + + f = fixed_stake / bankroll + + Unlike `fixed_fraction` (which risks a constant *percentage* and therefore compounds), a flat + dollar stake risks a *shrinking* fraction of bankroll as bankroll grows, and a *growing* + fraction as bankroll shrinks -- the opposite of geometric (fractional-fractional) sizing. It + is a common naive baseline ("just risk $10 a trade") worth contrasting against the + Kelly family. + + Raises `ValueError` if `fixed_stake < 0` or `bankroll <= 0`. Result is clamped to `[0, 1]` + (a stake larger than the whole bankroll is capped at "risk everything"). + """ + if fixed_stake < 0.0: + raise ValueError(f"naive_flat_fraction: fixed_stake must be >= 0, got {fixed_stake}") + if bankroll <= 0.0: + raise ValueError(f"naive_flat_fraction: bankroll must be > 0, got {bankroll}") + + return _clamp(fixed_stake / bankroll) + + +def merton_fraction(exp_return: float, variance: float, gamma: float) -> float: + """Merton portfolio fraction: the continuous-time analogue of Kelly for an investor with + constant relative risk aversion `gamma`, allocating to a single risky asset with expected + excess return `exp_return` and return `variance` against a riskless asset. + + f* = exp_return / (gamma * variance) + + (Merton, 1969/1971.) `gamma = 1` recovers the log-utility / Kelly-equivalent investor; + `gamma > 1` is more risk-averse and allocates less than Kelly-equivalent; `gamma < 1` (but + still > 0) allocates more. Included here as a second, independently-derived formula for + "how much should a growth-optimizing (or risk-averse) investor allocate," to cross-check the + discrete-bet Kelly formula above. + + Returns 0 if `exp_return <= 0` (no edge -> allocate nothing) rather than a negative fraction. + Raises `ValueError` if `variance <= 0` or `gamma <= 0`. Result is clamped to `[0, 1]`. + """ + if variance <= 0.0: + raise ValueError(f"merton_fraction: variance must be > 0, got {variance}") + if gamma <= 0.0: + raise ValueError(f"merton_fraction: gamma must be > 0, got {gamma}") + + if exp_return <= 0.0: + return 0.0 + return _clamp(exp_return / (gamma * variance)) diff --git a/docs/superpowers/analysis/bankroll_sizing/test_sizing_strategies.py b/docs/superpowers/analysis/bankroll_sizing/test_sizing_strategies.py new file mode 100644 index 00000000..df3a8c50 --- /dev/null +++ b/docs/superpowers/analysis/bankroll_sizing/test_sizing_strategies.py @@ -0,0 +1,225 @@ +"""Unit tests for sizing_strategies.py (bankroll-sizing math study).""" + +from __future__ import annotations + +import pytest +from sizing_strategies import ( + cppi_fraction, + drawdown_adjusted_kelly, + fixed_fraction, + fractional_kelly, + kelly_fraction, + merton_fraction, + naive_flat_fraction, +) + +# --- kelly_fraction ---------------------------------------------------------------------------- + + +def test_kelly_fraction_even_money_60pct(): + # f* = (b*p - q)/b = (1*0.6 - 0.4)/1 = 0.2 + assert kelly_fraction(0.6, 1.0) == pytest.approx(0.2) + + +def test_kelly_fraction_floor_win_rate_even_money(): + # p=0.55, b=1.0 -> (0.55 - 0.45)/1 = 0.10 + assert kelly_fraction(0.55, 1.0) == pytest.approx(0.1) + + +def test_kelly_fraction_floor_rule(): + # p=0.55, b=1.5 (keel's promotion floor: min_win_rate=0.55, min_rr=1.5) + # f* = (1.5*0.55 - 0.45)/1.5 = (0.825 - 0.45)/1.5 = 0.375/1.5 = 0.25 + assert kelly_fraction(0.55, 1.5) == pytest.approx(0.25) + + +def test_kelly_fraction_no_edge_returns_zero(): + # p=0.5, b=1.0 -> edge = 0.5 - 0.5 = 0 -> no edge + assert kelly_fraction(0.5, 1.0) == 0.0 + + +def test_kelly_fraction_losing_edge_returns_zero(): + # p=0.4, b=1.0 -> edge = 0.4 - 0.6 = -0.2 -> negative, clamp to 0 + assert kelly_fraction(0.4, 1.0) == 0.0 + + +def test_kelly_fraction_clamped_to_one(): + # Strong edge with high b can imply f* > 1; must clamp. + assert kelly_fraction(0.99, 100.0) <= 1.0 + assert kelly_fraction(0.99, 100.0) >= 0.0 + + +def test_kelly_fraction_invalid_p_raises(): + with pytest.raises(ValueError): + kelly_fraction(1.5, 1.0) + with pytest.raises(ValueError): + kelly_fraction(-0.1, 1.0) + + +def test_kelly_fraction_invalid_b_raises(): + with pytest.raises(ValueError): + kelly_fraction(0.55, 0.0) + with pytest.raises(ValueError): + kelly_fraction(0.55, -1.0) + + +# --- fractional_kelly --------------------------------------------------------------------------- + + +def test_fractional_kelly_half(): + full = kelly_fraction(0.55, 1.5) + assert fractional_kelly(0.55, 1.5, 0.5) == pytest.approx(0.5 * full) + assert fractional_kelly(0.55, 1.5, 0.5) == pytest.approx(0.125) + + +def test_fractional_kelly_quarter(): + full = kelly_fraction(0.55, 1.5) + assert fractional_kelly(0.55, 1.5, 0.25) == pytest.approx(0.25 * full) + assert fractional_kelly(0.55, 1.5, 0.25) == pytest.approx(0.0625) + + +def test_fractional_kelly_full_lambda_equals_full_kelly(): + assert fractional_kelly(0.55, 1.5, 1.0) == pytest.approx(kelly_fraction(0.55, 1.5)) + + +def test_fractional_kelly_zero_lambda_is_zero(): + assert fractional_kelly(0.55, 1.5, 0.0) == 0.0 + + +def test_fractional_kelly_negative_lambda_raises(): + with pytest.raises(ValueError): + fractional_kelly(0.55, 1.5, -0.1) + + +# --- drawdown_adjusted_kelly --------------------------------------------------------------------- + + +def test_drawdown_adjusted_kelly_zero_dd_equals_full_kelly(): + assert drawdown_adjusted_kelly(0.55, 1.5, current_dd=0.0, max_dd=0.20) == pytest.approx( + kelly_fraction(0.55, 1.5) + ) + + +def test_drawdown_adjusted_kelly_at_max_dd_is_zero(): + assert drawdown_adjusted_kelly(0.55, 1.5, current_dd=0.20, max_dd=0.20) == 0.0 + + +def test_drawdown_adjusted_kelly_beyond_max_dd_is_zero(): + assert drawdown_adjusted_kelly(0.55, 1.5, current_dd=0.30, max_dd=0.20) == 0.0 + + +def test_drawdown_adjusted_kelly_halfway_scales_by_half(): + full = kelly_fraction(0.55, 1.5) + result = drawdown_adjusted_kelly(0.55, 1.5, current_dd=0.10, max_dd=0.20) + assert result == pytest.approx(0.5 * full) + + +def test_drawdown_adjusted_kelly_invalid_max_dd_raises(): + with pytest.raises(ValueError): + drawdown_adjusted_kelly(0.55, 1.5, current_dd=0.0, max_dd=0.0) + + +def test_drawdown_adjusted_kelly_negative_current_dd_raises(): + with pytest.raises(ValueError): + drawdown_adjusted_kelly(0.55, 1.5, current_dd=-0.01, max_dd=0.20) + + +# --- fixed_fraction ------------------------------------------------------------------------------- + + +def test_fixed_fraction_default_is_one_percent(): + assert fixed_fraction() == pytest.approx(0.01) + + +def test_fixed_fraction_passthrough(): + assert fixed_fraction(0.05) == pytest.approx(0.05) + + +def test_fixed_fraction_clamped_above_one(): + assert fixed_fraction(2.0) == 1.0 + + +def test_fixed_fraction_negative_raises(): + with pytest.raises(ValueError): + fixed_fraction(-0.01) + + +# --- cppi_fraction ---------------------------------------------------------------------------- + + +def test_cppi_fraction_basic_math(): + # bankroll=1000, floor=800, m=3 -> 3*(1000-800)/1000 = 3*0.2 = 0.6 + assert cppi_fraction(bankroll=1000.0, floor=800.0, multiplier=3.0) == pytest.approx(0.6) + + +def test_cppi_fraction_clamped_to_one_when_cushion_large(): + # cushion large relative to bankroll * multiplier -> clamp to 1 + assert cppi_fraction(bankroll=1000.0, floor=100.0, multiplier=3.0) == 1.0 + + +def test_cppi_fraction_zero_when_at_or_below_floor(): + assert cppi_fraction(bankroll=800.0, floor=800.0, multiplier=3.0) == 0.0 + assert cppi_fraction(bankroll=700.0, floor=800.0, multiplier=3.0) == 0.0 + + +def test_cppi_fraction_invalid_bankroll_raises(): + with pytest.raises(ValueError): + cppi_fraction(bankroll=0.0, floor=0.0, multiplier=3.0) + with pytest.raises(ValueError): + cppi_fraction(bankroll=-100.0, floor=0.0, multiplier=3.0) + + +def test_cppi_fraction_negative_multiplier_raises(): + with pytest.raises(ValueError): + cppi_fraction(bankroll=1000.0, floor=800.0, multiplier=-1.0) + + +# --- naive_flat_fraction ------------------------------------------------------------------------ + + +def test_naive_flat_fraction_basic_math(): + assert naive_flat_fraction(fixed_stake=10.0, bankroll=1000.0) == pytest.approx(0.01) + + +def test_naive_flat_fraction_clamped_when_stake_exceeds_bankroll(): + assert naive_flat_fraction(fixed_stake=2000.0, bankroll=1000.0) == 1.0 + + +def test_naive_flat_fraction_zero_stake_is_zero(): + assert naive_flat_fraction(fixed_stake=0.0, bankroll=1000.0) == 0.0 + + +def test_naive_flat_fraction_invalid_bankroll_raises(): + with pytest.raises(ValueError): + naive_flat_fraction(fixed_stake=10.0, bankroll=0.0) + + +def test_naive_flat_fraction_negative_stake_raises(): + with pytest.raises(ValueError): + naive_flat_fraction(fixed_stake=-10.0, bankroll=1000.0) + + +# --- merton_fraction ---------------------------------------------------------------------------- + + +def test_merton_fraction_basic_math(): + # exp_return=0.08, variance=0.16, gamma=1 -> 0.5 + assert merton_fraction(exp_return=0.08, variance=0.16, gamma=1.0) == pytest.approx(0.5) + + +def test_merton_fraction_clamped_to_one(): + assert merton_fraction(exp_return=5.0, variance=0.01, gamma=1.0) == 1.0 + + +def test_merton_fraction_no_edge_is_zero(): + assert merton_fraction(exp_return=0.0, variance=0.16, gamma=1.0) == 0.0 + assert merton_fraction(exp_return=-0.05, variance=0.16, gamma=1.0) == 0.0 + + +def test_merton_fraction_invalid_variance_raises(): + with pytest.raises(ValueError): + merton_fraction(exp_return=0.08, variance=0.0, gamma=1.0) + + +def test_merton_fraction_invalid_gamma_raises(): + with pytest.raises(ValueError): + merton_fraction(exp_return=0.08, variance=0.16, gamma=0.0) diff --git a/docs/superpowers/references/trading-knowledge-base/README.md b/docs/superpowers/references/trading-knowledge-base/README.md index d572afbf..3bb76886 100644 --- a/docs/superpowers/references/trading-knowledge-base/README.md +++ b/docs/superpowers/references/trading-knowledge-base/README.md @@ -136,6 +136,7 @@ reference valid across the split without rewriting them. | 81 | "Technical Analysis" (Zerodha Varsity **Module 2**, 177pp, free; Indian-equities beginner textbook, 20 chapters) | extracted (thin; **the FOURTH saturation call on the TA stream, and it holds**) — chapters 1–18 near-duplicate §1/§3/§24/§25/§54/§55/§59/§70/§76, and the book **aims the wrong way for us by its own §1.3** (*TA is "best used to identify short term trades"*, holds *"few minutes and few weeks"*, *"do not use TA to identify long term investment opportunities"*) against §74.10's crypto finding that buy-and-hold BEATS technical rules intraday while daily SMAs win. ⭐ **Both keepers come from a 17pp appendix of copy-pasted vendor indicator notes, not the 160pp of textbook: §81.1 AROON** (grep `aroon` → 0 hits; `time relative to price` → 0 hits) — `Aroon-Up(n) = 100×(n − bars_since_n_day_high)/n`, *time relative to price* where every other KB oscillator is price relative to time; ⭐⭐ **it is the CONTINUOUS form of the rule we already trade** (our entry is exactly `Aroon-Up(40) == 100`) ⇒ a candidate trend gate built from **the entry's own statistic** — live because **§58.2 contests the shipped ADX>25 gate** while §74.7 says crypto wants *some* gate, and sweeping its `n` **rides the existing §74.2/§79.5 lookback axis instead of opening a new one** (decisive under `N ≤ 3`). ⭐ **§81.2 SUPERTREND** (grep `super.?trend` → 0 hits) — `(H+L)/2 ± m·ATR(p)`, ratchets one-way, flips on a CLOSE cross; defaults `p=7, m=3`. Anchored on the **bar midpoint**, which none of the four existing volatility trails use (Kase §54.6, ER-adaptive §54.8, PSAR §54.8, MEMA §58.13b), and **satisfies rail 10 by construction**. ⚠️ **Widths are NOT naively comparable** — §58.12's ~1.5·ATR(**50**), our 2·ATR(**20**) and this 3–4·ATR(**7**) differ in lookback; fix the ATR period first and **score on expectancy, not win rate**. §79.7 already re-scoped this work to a single `stop-off` **ablation**. ⭐ **§81.3 supplies the missing averaging window for §54.23's above-average-volume breakout filter — `10-day`** (grep `10.day average volume` → 0 hits; only §60.9's 20-day *liquidity* floor existed, a different job) ⇒ adopt as **`a_priori`, zero trials cost** (§73.13a/§74.13). ⭐ **§81.4 HARD-GATE vs SIZE-MODIFIER tiering** (grep `size.modif|never a gate` → 0 hits) — *"when indicators confirm I increase my bet size, when they don't I still buy but scale down"*: **a scalar CTS score cannot express "may raise conviction but may never veto"** ⇒ mark each factor `gate` or `weight`. Take the taxonomy, **NOT its assignments** (it files RSI/MACD in the soft tier; both refuted §58.10a/§74.3/§80.14). **§81.5 minimum TEMPORAL separation between level touches** (grep `well spaced|minimum separation` → 0 hits) — S&R zones *"well spaced in time"*, double bottoms *"at least 2 weeks"*; `analysis/levels.py` validates ≥3 touches with **no time constraint**. ⚠️⚠️ **§81.6 THE HEADLINE NEGATIVE — the book does NOT define MACD divergence; the concept is ABSENT, not vague.** All **7** occurrences of `divergen` in 177pp are the indicator's own *name* (two MAs separating); price-vs-oscillator divergence never appears in any form ⇒ **there is not even a qualitative version to sharpen; §58.10c remains the KB's ONLY specified detector, building it requires INVENTING the specification (each choice a trial), and §80.14's demotion of `macd_divergence` stands untouched.** ⚠️ Care-level flag: the MACD chapter body says the signal line is a 9-day **EMA**, its own Key Takeaway says **SMA** — unreconciled (cf. §77) ⇒ §81.1/§81.2 logged as candidates, not builds. **REINFORCES:** §81.7a the book itself abandons RSI mean-reversion in trends (*"if RSI is fixed in an overbought region… look for BUYING opportunities instead"*), corroborating §58.10a/§74.3 — but *"prolonged"* has no threshold ⇒ **COMPUTABILITY: NO**; §81.7b it states the **MECHANISM** behind §74.4's −5.34bp Bollinger result (**envelope expansion**: *"BB works well in sideways markets, and fails in a trending market"*), explaining rather than contradicting it and re-justifying §54.15's demotion. ⛔ discarded: all 16 candlestick patterns (§1.3/§2.1/§59.5/§70.7), **morning/evening star + gaps = structurally N/A to 24/7 spot** (§59/§76.4), all short setups → exit/don't-buy only, the Metastock/Amibroker vendor tour (advertises **AI + genetic-algorithm optimisers** — excluded black-box class), the **scalper section** (1–5min, RRR 0.5–0.75 vs §35.2, *"use margins effectively"* ⇒ triply excluded), the **Alligator** (needs *"all three MAs separated"*, **no threshold** ⇒ COMPUTABILITY: NO), the Dow accumulation/mark-up/distribution phase model (qualitative, no thresholds — §61.1 disposition; subsumed by §54's ER/ADXR/CSI), tenet 6 "all indices must confirm" (breadth basket, N/A §25.6), Fibonacci (§59.6/§70), flags (hand-placed parallel lines ⇒ COMPUTABILITY: NO), the Nifty-50 universe filter (§75.1/§82.3 — ranking is a no-op at `|allowlist| = 3`), and all Indian-session specifics incl. **the 3:20 PM entry convention every candlestick setup is anchored to** (§76.4 — a 24/7 market has no session). **RECOMMENDATION — treat the TA-primer stream as CLOSED. The confirmation is SHARPER than §24/§59/§76/§77 precisely because Varsity is a *good* example of the genre** (free, well-regarded, worked arithmetic) **and still yielded only two unvalidated appendix indicators: THE CEILING IS STRUCTURAL, NOT A QUALITY PROBLEM.** A primer teaches what an indicator *is*; this KB now only needs what an indicator *earns* — controlled tests (§58), crypto-specific samples (§74), or validation theory (§73/§78/§79) | [source-81](./sources/source-81.md) | | 82 | "Trading Systems" (Zerodha Varsity **Module 10**, Karthik Rangappa, 2017–2019, 16 chapters; free broker educational content) | extracted (thin; **heavily saturated — two keepers, both NEGATIVES**) — ⚠️ **the title is MISLEADING: this is a PAIR-TRADING tutorial (Ch 2–14) + calendar spreads (Ch 15) + one momentum-portfolio chapter (Ch 16), NOT a system-construction/validation course.** No chapter on walk-forward, IS/OOS, parameter counts or curve-fitting; **14 of 16 chapters excluded on the short leg** before any futures/leverage reasoning. ⚠️⚠️ **§82.1 the module contains NO backtesting BY DECLARATION** — *"this module will not include the 'backtest' bit… The only reason why I'm not including the backtesting part is that I lack programming skills"*, and Ch16's core premise is offered as *"**This is a claim. I do not have data to back this up**, but I have successfully used this technique for several years"* (sole support: an anecdote + an *Economist* article) ⇒ **its validation methodology is not looser than §73/§78, it is ABSENT; nothing here may be cited to relax MinBTL / trials-budget / PBO / `a_priori` parameters.** Value = a clean, freshly-dated specimen for the §64.1/§68.6/§58.11 untested-system exhibit series, **notable because the author VOLUNTEERS the omission.** ⚠️ **§82.2 one actively OVERFITTING-ENCOURAGING instruction, flagged not neutrally recorded:** *"backtest various capital allocation techniques to figure out which works well for you"* + a Ch16.4 menu spanning ~5 holding periods × 5 ranking variables × 4 weighting schemes ≈ **100 configurations, no OOS, no deflation** (§73.3 affords **N ≤ 3**); its *"contrarian, overweight the bottom 5"* variant **inverts the strategy's own premise mid-chapter with no test — a search over SIGNS, the costliest kind.** ⭐ **§82.3 THE ONE CONSTRUCTIVE KEEPER — the NUMBER behind §75.1's qualitative finding: a practitioner floor of 150–200 tracking universe for a 12–15 name portfolio (~12:1–16:1). Ours is 3 assets into 3 slots = 1:1, short by >1 ORDER OF MAGNITUDE and structurally unclosable under the halal allowlist ⇒ this RETIRES the ranking half of §60.2 as a build candidate** (§75.1 demoted it; §82.3 closes it). The **concurrent-slot half is untouched and still live.** §60.2's own text already conceded the 1/15 constant *"presumes a broad multi-hundred-stock universe"* — the ratio was the missing piece. ⚠️ **§82.7 NEGATIVE, exhaustively verified: the chapter titled "Position Sizing" is beta-neutral futures LOT MATCHING, not money management — the module contains no risk fraction, no ATR/vol sizing, no Kelly/optimal-f, and NO STOP-LOSS anywhere** ⇒ open question 5 yields **zero**. §82.4 Ch16's momentum portfolio (12-month formation → rank → top-10-15 equal-weight → month-end rebalance) is textbook Jegadeesh–Titman, **fully subsumed by §79 (J×K grid, cross-frequency correlations, costs) and §80.10/§80.11 (crypto-specific, weekly not annual)**; unlike §80's excluded cross-sectional work it IS long-only, so the shorting objection lapses — **the universe objection (§82.3) is fatal on its own.** §82.5 its 12-month/monthly default agrees with §79.1 but as untested *practice* (zero evidentiary weight); the ~250d formation window's loose coincidence with §74.2/§79.5's plateau is **noted and explicitly DISCOUNTED, not counted as confirmation.** §82.6 momentum *"bleeds heavier than the markets itself"* in downturns — real phenomenon, anecdote-only, reinforces §74.7 regime-gating, adds no threshold. §82.9 an intercept/explanatory-power veto is generic `R²` discipline, **weaker than §78's PBO and §58.11's random-entry null**, which ask the harder *"better than nothing?"*. ⛔ **§82.8 the whole pair-trading system (Ch 2–14) — short leg by construction, stock futures throughout; the KB's FIRST encounter with cointegration/stat-arb, recorded so the judgement is recoverable (§69 precedent).** One forward pointer kept: **ADF is the named standard instrument for the measured `a<0` verification §62.2 says was never done** before the dip-buy family was trusted — but **this source cannot supply it and admits so** (*"I could not find a single source online which will help you run an ADF test for free"*); statsmodels is declined, a stdlib ADF (OLS + hardcoded Dickey–Fuller critical values) is feasible but **none of the machinery is here** ⇒ **research pointer, NOT a lead.** ⚠️ one outright statistical error flagged: *"the two stocks will be stationary if they are normally distributed"* (false — a Gaussian random walk is normal and non-stationary). ⛔ Ch15 calendar spreads **quadruply excluded** (futures + short leg + *"top up the leverage"* + cost-of-carry = §18 riba); ⛔ Ch1's opening trade is a **naked short strangle**. **Stream verdict: stop feeding Varsity strategy modules** — the rest of the catalogue is derivatives-heavy (⛔ by instrument) and the house style attaches no empirical validation | [source-82](./sources/source-82.md) | | 83 | **"Risk Management and Trading Psychology"** (Zerodha Varsity **Module 9**, Karthik Rangappa, 16 chapters; free online textbook) | extracted (**heavily saturated — 3 keepers, nothing reshapes the risk model**) — arrives ~30 sources late: its position-sizing half is **Van Tharp**, whom §54 covers deeper and with tables, and its portfolio half runs toward MPT. ⭐⭐ **§83.2 THE BEST ITEM — portfolio σ WITHOUT a covariance matrix:** ch. 6 shows the matrix route (`sqrt(wσᵀ · CorrMatrix · wσ)`) and the plain stdev of a **synthetic weighted equity series** give **the identical number** (*"the STDEV function gives us the exact same value!"*) ⇒ **a portfolio-level risk figure for BTC/ETH/PAXG in PURE STDLIB, zero linear algebra, correlation absorbed by construction** — and it **CLOSES A LOOP §79.8 EXPLICITLY LEFT OPEN** (*"re-derive the constant from a portfolio-vol target"*) when **nothing in the KB could compute that target** (§54.22 gives pairwise correlations, §54.7 per-position parity; **neither aggregates**). A second instance, after §58.3/§74.6, of a project constraint (no NumPy) costing nothing. ⚠️ inherits §54.22's GASP objection ⇒ compute over **exposed days only** (§73.4); it is a **measurement, never an allocator** (MPT stays declined). ⭐ **§83.1 the equity BASE for the next trade is a CHOICE WE MADE BY DEFAULT** — ch.12's `core` / `total` / `reduced-total` equity models are absent from the KB (§54.19's reserves model answers *whether to compound*, not *what the base is*). **Verified in code: `execution/sizing.py` computes `qty=(equity×risk_pct)/stop_dist` on mark-to-market equity — the Total model, the one this source rates RISKIEST** (*"counting the chicken before they hatch"*) ⇒ **any unrealised gain inflates the sizing base, hardest exactly when a trend has run and the next breakout fires.** Generalises §65.9's narrow riba-compounding finding. **Reduced-total** (`free_cash + Σ committed + Σ max(0,(stop−entry)·qty)`) is **monotone-safe by construction under rail 9** — the §58.13b "satisfies the rail rather than needing checking" property — and every term is a PR #96 `positions` column. ⚠️⚠️ **must NOT touch rail 11's equity**, which includes unrealised P&L **deliberately** (`equity.py`: *"a drawdown breaker that saw only realized P&L would sit at 0% while a position bled"*) ⇒ **TWO equity definitions for TWO jobs** (§73.4 move). ⭐ **§83.3 aggregate VOLATILITY-exposure cap** (`Σ qty·ATR ≤ V%·equity`) — ⚠️⚠️ **CODE DEFECT FOUND AND VERIFIED: rail 4's comment reads "sum of at-risk capital across all open positions" but the code compares `total_exposure + intent.notional` to `max_exposure_usd` — it is NOTIONAL, i.e. VOLATILITY-BLIND.** A $5,000 position with a 2% stop and one with a 20% stop are identical to it. **Fix the comment regardless of whether the cap is built.** ⚠️ honestly bounded: at one tranche/asset the 1% rail already pins the aggregate near 3%, so the new cap **only bites once §75.1 concurrent slots or §26.1 pyramiding multiply position count** ⇒ build as a **precondition of those levers, not before.** ⚠️ **§83.4 VaR ASSESSED AND DECLINED — as taught it WOULD MISLEAD on crypto:** its gating step is **eyeballing a 126-point histogram for a bell curve**, which passes routinely and is wrong routinely against §54.20's ≥5·ATR shock; the 95% cut **discards exactly the observations §54.20 says matter most**; it is a 1-day fully-invested statistic against our intermittent mostly-cash book (§54.22 GASP); and **VaR is not sub-additive**, making it a poor fit for the correlated-basket job it superficially suits. **The irony recorded: the arithmetic is fully NON-PARAMETRIC (sort, index a percentile) so it never uses the normality it spends a chapter establishing — the normality step is pure false reassurance.** CVaR is the better half (coherent, tail-aware) but was already *"noted, not adopted"* at §33 — **not reopened**; at `N ≈ 23` drawdown/Sortino answer it better. **§83.5 Kelly's Criterion** (`W − (1−W)/R`) is **grep-verified absent** (only prior `Kelly` hit is a surname) — but a new *formula*, **not a new decision**: §54.18 already holds optimal-f + *"use fractional f"*, and this source independently reaches the same never-full conclusion; ⚠️ `W`/`R` off ~23 trades is **noise wearing a formula** (§58.11: highest z = 0.88). **§83.11 expectancy CONFIRMS, no refinement** — `Kelly% > 0 ⟺ W > 1/(1+R)` is **algebraically identical to `win_rate > 1/(1+R:R)`** (§23.1/§25.5/§35.2), a fourth independent derivation; ⚠️ but the source computes `W`,`R` from a **10-trade** table — the §58.11/§73.3 error in miniature. ⚠️ **§83.8 negative exemplar** — a 1-yr forward band from a 126-day bull-market mean ×252 gives *"+2.23% to +108.07%"* at 3σ, **a 99% band containing no loss**; joins §68.6/§73.8/§58.17 (no-oracle §6.4). ⚠️ **§83.10 states plainly what portfolio language can obscure: at `|allowlist| = 3` we sit on the STEEPEST part of the diversification curve and CANNOT move right** (§51: redundancy doesn't diversify) — **we run an essentially UNDIVERSIFIED book** and rely on stops / the 1% rail / drawdown breakers; ⛔ the chapter's remedy for the residual, **hedging, is excluded** (§4.9/§10.10/§18) — **an accepted cost, logged not worked around**; it is also the textbook justification for `UNCORRELATED_ASSETS = {"PAXG"}` existing. ⛔ **chs. 8–9 MPT/efficient frontier via Excel Solver — DECLINED, not reopened** (§33/§50.1/§54.22/§68); salvage thin: unusually **riba-clean** (no risk-free rate, no CML, no Sharpe tangency — pure min-variance) so the *riba* objection is weaker here than §50.1 while the declined-stack objection is untouched, but **it never imposes `w ≥ 0`** so its own frontier admits shorts ⇒ nothing adopted. ⛔ percentage-margin sizing (margin as the sizing unit = leverage/riba; non-margin residue = rails 2 and 6), futures/options/lots context, short setups → don't-buy filters. ⧉ §83.9 recovery-trauma table **exactly duplicates** §54.18's required-gain asymmetry; §83.6 percentage-risk sizing is `sizing.py::size()` re-derived verbatim (mild evidence the rail isn't a one-lineage artifact; **our 1% is stricter than its 1–3% band**); §83.7 unit-per-fixed-amount rejected by the source itself, percentage-volatility = §54.7/§27.1. **Discarded: chs. 15–16 Trading Biases NOT OPENED** — psychology assessed saturated at §69 with a standing stop-feeding recommendation, and **rails 8/9/11/16 make averaging-down, stop-widening, revenge-sizing and streak-chasing IMPOSSIBLE rather than discouraged**; the gambler's-fallacy material is structurally inapplicable to an agent with no streak memory, and its mechanical residue is already the correct inversion inside rail 16 (halt on **edge-decay** suspicion, explicitly not because a reversal is "due"). **Stream exhausted at one source — Varsity's remaining modules are saturated TA or excluded instruments; do not feed more** | [source-83](./sources/source-83.md) | +| 84 | **`keeks` bankroll-management library (v0.3.0) + its 9-post "Bankroll Management with Keeks" series** — the Kelly Criterion *family* of capital-allocation sizers (full/fractional/drawdown-adjusted Kelly, optimal f, Merton/CRRA, fixed fraction, CPPI, dynamic, naive). | extracted — ⚠️ **maysir-framed source; adopted ONLY as the mathematics of optimal capital allocation, never betting** (§84.0 disclaimer). **HEADLINE — like §83, CONFIRMS the risk model, does not reshape it.** ⭐ **keel's `execution/sizing.py` fixed-fractional `risk_pct=0.01` IS literally the Fixed Fraction strategy (§84.7).** Feeding keel's OWN promotion floor (`min_win_rate 0.55`, `min_rr 1.5`) into Kelly gives **full-Kelly f\* = (1.5·0.55−0.45)/1.5 = 0.25 → 25% per trade; half 12.5%, quarter 6.25% — so keel's 1% ≈ 4% of full Kelly, BELOW even quarter-Kelly.** A **fourth independent derivation of "use fractional f, never full"** (§54.18 optimal-f + fractional; §83.5/§83.11 Kelly `W−(1−W)/R`), vindicated by §58.11's *"W/R off ~23 trades is noise wearing a formula"* (highest z=0.88): full Kelly needs a KNOWN edge; ours is estimated, so deep-sub-Kelly is the correct posture, not timidity — **the estimation-error simulation (§84.14, report `2026-07-22-bankroll-sizing-comparison.md`) shows over-estimating p by 0.05 collapses full-Kelly growth while the 1%/¼-Kelly levels barely notice.** ⭐ **TWO genuinely-new-to-KB concrete items:** (a) **§84.4 dynamic drawdown taper** `f=(1−d/D)·f\*` — a CONTINUOUS size taper that bleeds exposure down *before* the hard account-DD breaker (rail 11 halts; this tapers); the one candidate not already ours; (b) **§84.6 Merton share** `f=μ/(γσ²)` makes "why sub-Kelly" an explicit, defensible **risk-aversion knob** (`γ≈2`) rather than an ad-hoc λ. ⧉ **Already ours:** §84.7 fixed fraction = `sizing.py::size()`; §84.8 CPPI floor/cushion ≈ total-exposure cap + DD breaker (the post's "Kelly-CPPI hybrid" = what keel already does structurally); §84.10 naive flat-stake = `dca_size()` for the stopless DCA rule. ⛔ **Declined:** §84.9 Dynamic streak-scaling (soft **anti-martingale — collides with the no-martingale rail** and the README "settled-by-measurement" lesson that streak-chasing adds *correlated* trades); §84.5 Optimal f as a LIVE sizer (over-fits the single worst loss on 31-trade samples — the PBO/MinBTL failure mode). ✎ **Fractional-Kelly / Merton edge-aware `risk_pct=λ·f\*` is a HUMBLE candidate only** — §58.11/§83.5 already ruled per-rule W/R too noisy to size on; the honest use is a *ceiling / sanity-check* on any hand-set `risk_pct`, not an autonomous sizer. keeks API + all 9 formulas §84.2–§84.12; halal screen §84.13; educational commands §84.15. **Nothing wired live by being written — paper-gate + backtest floor still bind.** | [source-84](./sources/source-84.md) | | — | "GBP/JPY top-down analysis" (Greystone/tier1trading video transcript) | ⧉ **near-total duplicate of Source 17** (same author/method, worked example) — kill zone, ATR-stop-below-pullback-low, HH+HC confirmation, last-chance double-bottom + its exhaustion rationale, IPDE, multi-TF bias, prior-resistance-becomes-support, half-off→breakeven→trail-higher-lows are all already §17/§2/§3.2/§23.2/§26.2/§54.24; RSI-80 threshold already in `indicators.is_overbought`. **Not re-extracted.** Two genuine refinements folded into §17.1a (how to DRAW the zones: shallow = highest-high→highest-close, deep = lowest-low→lowest-close, and the zones are *shifted to clear the R:R floor* — so the kill zone POSITIONS them, not just validates them) and §17.1b (RSI>80 as a don't-chase entry-timing veto, distinct from RSI-as-confluence). ⛔ head-and-shoulders short zone; N/A London-session timing (24/7 crypto) | see [source-17](./sources/source-17.md) | | — | "Mistakes traders make after 10 years of coaching" (tier1trading video transcript) | ⧉ **psychology — saturated (§23/§24/§26), NOT extracted.** Notable only as **independent validation that the rails already encode, mechanically, the disciplines it spends 7 minutes teaching**: trade-too-early→paper gate · switch-strategy-after-5-losses / "need 100–200 trades"→`min_trades: 100` · move-stops & average-into-losers→**rails 9 and 8** · risk-5–20%→`risk_pct: 0.01` · "boring routine"→the agent *is* the routine. Its four must-know numbers (win rate, avg R:R, **largest losing streak**, expectancy) are all already `BacktestResult` fields. **One line kept:** `max_losing_streak` is the sizing input for the §57.1 breaker — threshold must sit ABOVE the tested max streak or it fires on normal variance (folded into §57.1) | see [source-57](./sources/source-57.md) | | — | "Trading Terminology Explained" (re-paste) | ⧉ duplicate of Source 4 | see [source-04](./sources/source-04.md) | @@ -158,6 +159,7 @@ Thematic view — which agent module each theme feeds, and where it's sourced. | `strategy/backtest.py` | Backtest, intrabar order-of-events, spread/slippage modeling, MFE/MAE, no-overlap realism; ⭐⭐ **§73.1 the expected maximum performance under the NULL rises with trial count** — N=10 configs of strategies whose TRUE Sharpe is **zero** yields an expected best-of-sweep Sharpe of **1.57**, N=128 yields 2.6 ⇒ **a best-of-N backtest number is edge PLUS a selection bias whose size is `E[max_N]` and is computable IN ADVANCE.** ⚠️ **§73.13 this changes how we SWEEP:** sweep-and-pick-the-best is the exact procedure this paper was written to discredit, and our 5yr window cannot support it (budget: N ≤ 3) ⇒ **(a)** reclassify most parameters as `a_priori` from the KB's own literature — **the knowledge base is a TRIALS-BUDGET SUBSIDY**, its best justification; **(b)** report **plateau width** beside any chosen value and treat a narrow peak as disqualifying (§54.10 formalised); **(c)** **sweep to characterise SENSITIVITY, never to SELECT** — same computation, different epistemic status, and only the second increments `N`; **(d)** **`MinBTL ∝ 1/(SR_trade² × trades_per_year)`** ⇒ doubling trade frequency **HALVES** MinBTL — ⭐⭐ **§78 SUPPLIES THE INSTRUMENTS §73 ONLY DIAGNOSED — build order: PBO/CSCV (§78.6, ~60 lines, deterministic, no distributional assumption) FIRST, then DSR (§78.3), with implied-independent-trials `N̂ = ρ̂+(1−ρ̂)M` (§78.2) making the ledger honest about correlated sweeps.** ⚠️⚠️ **§78.4: a sweep run BEFORE the ledger exists DESTROYS the `V[{SRₙ}]` needed to score it** ⇒ ledger is a PRECONDITION, not good practice. ⛔ **§78.7 Strathern rail: PBO/DSR/haircut may GATE or REPORT, NEVER rank a sweep.** ⚠️ **§79.12: `report.py:175`'s `__pooled__` statistic concatenates BTC/ETH/PAXG with no fixed effects — Huang eq.(7) proves that estimator is biased UPWARD; stop headlining pooled figures.** ⚠️ **§79.11: `sim/benchmark.py` runs DCA-accumulation only — add a SAME-CAPITAL buy-and-hold arm as a third null (over the ALTERNATIVE) beside §58.11 (over the data) and §73.2 (over the search).** a new arithmetic argument for **§60.2 rank-and-fill** (⚠️ **but see §75.1 — §60.2's ranking step cannot supply that frequency at our allowlist size; the arithmetic stands, the proposed mechanism for delivering it does not**), and independent confirmation of the 2026-07-20 experiment's Conclusion 2 that dropping the ADX gate to buy frequency is a bad trade, since `SR_trade` enters **squared** while frequency enters only linearly. ⚠️ **§73.8 negative exemplar:** a 4-parameter, 8,800-node mesh over **1,000 bars of a pure random walk** produced annualized SR **1.27** — our sweep is smaller but the same shape, and our 5yr window is barely longer than their 4yr; joins §68.6 and §58.17 as the third documented spectacular-backtest-by-selection in the KB, and the only one with a proof attached. **§73.5** `N = 2^(#binary params)` — `TurtleBreakout`'s 6 parameters at 2 values each is already **N = 64 ⇒ `E[max_64]` ≈ 2.35 from a rule with NO edge**; quantifies KISS (§26) | §1.7, §2.3, §4.2, §20.2, §20.5, **§73.1, §73.5, §73.8, §73.13** | | `strategy/promotion.py` | Paper gate, promotion/demotion, expectancy + R:R≥1.5–2 & win≥55%, min-sample (100 trades/5yr), edge decay; **per-rule-class floor via breakeven-winrate formula `win_rate > 1/(1+R:R)` — R:R 3 ⇒ 25% suffices; replaces the flat 55% bar for trend-followers (§23.1, §25.5, §35.2)**; **Kaufman testing rigor §54.10: expectations-first, information ratio, walk-forward + OOS/feedback firewall, robustness-plateau (not the max), drawdown-probability check, `% profitable tests`≥~70% robustness bar §54.11; risk-of-ruin metric (unequal-wins form §54.18); GASP insight §54.22 — do NOT use whole-period Sharpe/covariance on the Turtle's intermittent mostly-cash returns (understates risk); keep the drawdown/Sortino verdict (textbook-endorsed over Sharpe)**; **§64.1 empirical warrant for that rigor** — published AI/ML superiority claims inverted under realistic-scale re-testing, so no proposer (human or LLM) gets a shortcut; **NEW diagnostic §63.2: benchmark realized P&L against a HINDSIGHT ceiling computed under our actual caps** (long-only MPS reformulation, sweep `W`) — separates "caps too tight" from "allocation logic naive" on the under-deployment defect, which self-referential backtesting cannot do; ⭐⭐ **§58.11 RANDOM-ENTRY CONTROL ARM**: benchmark every rule against ≥20 trade-frequency-matched RANDOM long-entry sequences run through the SAME exit, and promote only on ≥2σ over that null. It attributes edge to the **ENTRY vs the EXIT**, which a whole-rule sweep cannot. ⚠️ **RUN 2026-07-20 — and it CORRECTED this row's original claim that it is "valid at low trade counts where `min_trades: 100` cannot be met."** That was wrong: **a control arm does not manufacture statistical power, it reveals how little there is.** Measured on the Turtle (`docs/experiments/2026-07-20-adx-ablation-and-random-entry-control.md`): nothing cleared z≥2, **highest z = 0.88**, because BTC's null stdev is **$2,017/trade at n≈13** (implied per-trade σ ≈ $7,365). For the observed edge to clear z≥2 needs **~68 trades; we produce 2.6/yr ⇒ ~26 YEARS.** ⇒ **`min_trades: 100` is VINDICATED on the sample-size axis** (~68 lands close to it); only the **win-rate** axis needed the per-class relaxation (§25.5), and the two axes are independent. **Under-deployment is therefore an EPISTEMICS problem, not only a returns one — a rule trading ~6×/yr can never accumulate the evidence to prove it works, however good it is.** Their finding that many published entries were *no better than random* is the reason this matters; ⭐ **§58.0 component isolation** (pin a standard exit, sweep entries; then pin an entry, sweep exits — our harness sweeps whole rules and confounds the two); ⚠️ **§58.17 selection-bias exemplar — per-asset rule selection must sit INSIDE the walk-forward loop**; ⭐⭐ **§73.2 MinBTL GATE — the first quantitative answer to "do we have enough data to validate AT ALL?"**: `MinBTL_years ≈ (E[max_N]/SR_annualized)²`. **Ship REPORTING-ONLY first** — at current values it fails for every rule we have, and a gate that blocks everything on day one gets disabled rather than heeded. **§73.3: our SR ≈ 0.395 ⇒ MinBTL = 26.0yr at N=26, reproducing the 2026-07-20 experiment EXACTLY; at our real N≈336 it is ~55yr / 143 trades.** ⚠️ **Trials-budget inversion: N ≤ 3 on 5yr BTC/ETH; N < 2 on PAXG ⇒ PAXG INHERITS parameters and never receives its own fit.** ⭐⭐ **§73.4 gate statistic = `rf = 0` PER-TRADE Sharpe** — riba-free by substitution, and per-trade **dissolves §54.22's GASP objection** (flat days never enter); **Sortino/drawdown stays the VERDICT statistic**. This does NOT reopen the four prior declinations, which rejected Sharpe as an *optimization objective* — here it is a *null-test statistic*, a different job. ⭐ **§73.6 `trials_attempted` on every artifact, from a persisted append-only ledger counting DECISIONS** (ablations, rule retirements, asset prunes) — without `N` there is no threshold, so a backtest without one is not weak evidence but **no** evidence. ⭐ **§73.12 `parameter_provenance` (`a_priori` vs `fitted`)** — a_priori costs no `N`; `atr_period=20` is a_priori (Turtle canonical §54.14) ⇒ **freeze it**, while `entry_lookback=40` is currently fitted and per §73.3 **cannot be justified that way** ⇒ re-derive from §54.11/§58.6, both pointing *longer* anyway. ⚠️ **§73.1 an OOS split does NOT neutralise multiplicity** ⇒ **§54.10's firewall is necessary but NOT sufficient**; §54.10's robustness-plateau is now formally motivated (plateau = low *effective independent* N; a lone spike = the signature of a best-of-N null draw). ⚠️ **§73.7 in-sample optimization on a memory-bearing series is DETRIMENTAL, not neutral** (§62.2 established our AR(1) shape) ⇒ **edge decay is now ambiguous between a dying edge and a selection artifact**, disambiguated only by `N`; upgrades §58.17 from "wasteful" to "systematically negative OOS." ⚠️ **§73.8 do NOT add per-config p-values** (8,800-node mesh on a pure random walk ⇒ PSR-Stat 2.83, ">99% confident" and wrong). **§58.16's multiple-comparison note is SUPERSEDED by §73.2**, its closed form. **§73.11 §58.11 and §73.2 are COMPLEMENTARY** — §58.11 is a null over the DATA ("better than chance?"), §73.2 a null over the RESEARCHER'S SEARCH ("could I have manufactured this by sweeping?"); **§63.2 + §73.2 bracket a result from both sides** — hindsight ceiling above, luck floor below | §1.6, §4.5, §5.2, §6.3, §20.6, §23.1, §25.5, §35.2, §54.10, §54.11, §54.18, §54.22, §58.0, §58.11, §58.16, §58.17, §63.2, §64.1, **§73.1–§73.4, §73.6–§73.8, §73.10–§73.12** | | `strategy/money_mgmt.py` | Smooth-ratio sizing ramp (profit-trigger + acceleration) bounded by total & weekly DD caps; fixed-fractional on current equity; **ATR ("N") volatility-based sizing — smaller size when more volatile, crypto-essential (Turtle, §27.1)**; **pyramiding / scale-into-winners (position-level, never losers; rail-bounded, default off) §26.1**; **NEW Kaufman §54.7: volatility-parity / target-volatility sizing (`invest = ann-stdev / target-vol`, ~15%) via `cash/(ATR·√252)`; volatility stabilization = shrink size as vol rises (risk control WITHOUT stops)**; **pyramid on PROFITS only / NEVER average down (§54.19); optimal-f but use fractional-f / the 1% rail, never full optimal-f (§54.18); reserves/equity model — hold investment constant, accumulate reserves, redistribute (§54.19); equal-risk-by-ATR allocation across allowlist (§54.22)**; **NEW — the first mechanism aimed squarely at the UNDER-DEPLOYMENT defect: rank-and-fill deployment cadence §60.2** (divide capital into N equal slots; generate a ranked candidate list each bar; fill empty slots best-first; accept partial fills). Reframes deployment from "wait until one asset qualifies" to "always be filling slots from a ranked list" — rule-agnostic, composes with the validated Turtle entry, and testable without first resolving any entry-rule question. ⚠️⚠️ **§75.1 BOUNDS THIS CLAIM — split §60.2 into its two halves before building it.** (a) The **RANKING** half is a **NO-OP for us**: examined in its native habitat (a ~5,000-name equity scanner), rank-and-fill's frequency comes from **universe breadth**, not from ranking. §60.2's "allowlist-size-independent" claim does not survive — ranking only bites when qualifying candidates **outnumber** slots, and with `|allowlist| = 3` we are never candidate-constrained, we are **signal-constrained** (the rule fires ~2.6×/yr/asset, §73.3). **Under-deployment is a signal-PRODUCTION problem; ranking cannot produce signals.** (b) The **CONCURRENT-SLOT** half is **not** a no-op but is currently **UNTESTABLE**: PR #96 shipped the per-tranche `positions` table and lifted the one-tranche-per-product limit **on the live path**, while `keel/sim/portfolio_sim.py:600` still enforces `# only one RULE position per asset at a time` ⇒ **the harness cannot measure a behaviour the executor can already perform.** ⭐⭐ **Consequence: the S1+S2 ensemble rejection is an ARTIFACT** — it was rejected on account P&L with the reason "S1 & S2 compete for the slot instead of compounding", which is line 600 speaking, not the ensemble. **Cheap high-value lever: lift the sim cap to match live, then re-run the ensemble** (touches no rails). ⚠️ But those added trades are **correlated** (same family/asset) ⇒ helps DEPLOYMENT, not **KNOWABILITY**; §74.5/§58.10c MACD-family remains the only lever adding *uncorrelated* trades. ⚠️⚠️ **SUPERSEDED 2026-07-20 BY §79 + §80 — the frequency plan is redirected to BREADTH.** **§79.1: trading each asset MORE OFTEN does not help** (Sharpe 1.25/1.26/1.21 monthly/weekly/daily at 23.5%/77.1%/238.1% turnover, costs unmodelled; end-of-month beats daily in all three families incl. breakout 0.62 vs 0.59 with 0.2%/trade charged) — so BOTH halves of the plan are down. **§79.2 the replacement: cross-frequency correlation is only 0.22 for the SAME rule at different horizons ⇒ run an `a_priori` HORIZON LADDER — horizon is the one breadth axis the halal 3-asset allowlist does not cap.** **§79.3 names the mechanism: Jegadeesh–Titman overlapping portfolios (K staggered tranches, rebalance 1/K per bar).** ⚠️⚠️ **§80.14 the MACD candidate is CLOSED** (never cleared even the nominal level; `macd_divergence` demoted — and neither study tested divergence) ⇒ **§80.10 [C]'s WEEKLY time-series momentum is the new leading second-class candidate, as a HYPOTHESIS.** ⚠️⚠️ **§80.16: rule-class independence is NOT CITABLE and must be MEASURED IN-HOUSE (Jaccard/position-correlation/trade-timing, pure stdlib) — §73.5 makes it non-optional, since a CORRELATED second class is strictly WORSE than none.** ⛔ **§82.3 CLOSES the ranking half of §60.2** — the practitioner floor is a **150–200 tracking universe for a 12–15 name portfolio (~12:1–16:1); ours is 3-into-3 = 1:1**, short by >1 order of magnitude and structurally unclosable under the halal allowlist. §75.1 demoted it, §82.3 retires it as a build candidate. **The concurrent-slot half remains live.** ⭐⭐ **§83.2 gives the portfolio-vol TARGET §79.8 left open — portfolio σ WITHOUT a covariance matrix**, as the plain stdev of a synthetic weighted equity series (identical to the matrix route), **pure stdlib, correlation absorbed by construction**; compute over EXPOSED DAYS ONLY (§73.4/§54.22 GASP); a **measurement, never an allocator**. ⭐ **§83.1: the sizing EQUITY BASE is a default we never chose** — `sizing.py` uses mark-to-market equity (the "Total" model, rated riskiest) ⇒ **unrealised gains inflate the base hardest right when a trend has run**; `reduced-total` is monotone-safe under rail 9. ⚠️ **Must NOT touch rail 11's equity, which includes unrealised P&L deliberately — TWO equity definitions for TWO jobs.** **§62.2 supplies the THEORETICAL account of the refuted dip-buy family** — scale-in/DCA is variance-optimal only under genuine mean-reversion (`a<0`); that regime was never verified on crypto before those rules were trusted, so the refutation was predictable rather than surprising. Corollary worth acting on: **any future scale-in/DCA rule should gate on a measured `a<0` (or low ER / H<0.5), not be assumed** | §4.7, §20.3, §20.4, §26.1, §27.1, §54.7, §54.14, §54.18, §54.19, §54.22, §60.2, §62.2 | +| `execution/sizing.py` | Position sizing: **fixed-fractional risk** (`size`, `risk_pct=0.01` default) + no-stop DCA accumulation (`dca_size`). **§84 places it in the Kelly family: it IS the Fixed Fraction sizer (§84.7); the full-Kelly ceiling at keel's own promotion floor (p .55 / R:R 1.5) is ~25%, so 1% ≈ 4% of Kelly — below even ¼-Kelly.** A fourth independent *"use fractional f"* confirmation (§54.18/§83.5/§83.11), and the deep-sub-Kelly posture is the CORRECT answer to estimation error (§58.11 *"W/R off ~23 trades is noise"*; sim §84.14). Candidate leads (humble, gated): edge-aware `risk_pct=λ·f*` as a *ceiling/sanity-check* not an autonomous sizer (§84.3/§84.6), dynamic drawdown taper before the hard breaker (§84.4). ⛔ declined: streak-scaling §84.9 (no-martingale rail), optimal-f live sizer §84.5 (overfit). Also feeds §83.1 equity-base choice (Total vs reduced-total) + §83.6. | §54.7, §54.18, §83.1, §83.5, §83.6, §84.1, §84.3, §84.4, §84.7 | | `execution/executor.py` | Order lifecycle (close-validate, one-candle validity), OCO/bracket, partial exits, buffers, ATR stops, trailing-stop algorithm (+ channel-low trail §23.2, **20-SMA close-below & trail-to-breakeven §26.2**); **`stop_trigger=close\|intraday` — close-based stop confirmation cuts crypto whipsaw/shakeout, targets "stops too tight" defect §34.1**; target methods (1:1 / swing-high / Fib ext / **pattern-height §24.2**); **NEW Kaufman volatility-adaptive trailing stops §54.6/§54.8: Kase Dev-Stop (`ATR + f·STDEV`), ER-adaptive ATR stop (6·ATR initial, tightens as ER rises — fixes "stops too tight"), Parabolic SAR as long-only trail; stops trigger on the CLOSE (noise), profit-taking on the intraday spike**; **entry/exit bands (wide entry, narrow exit) + entry-timing "buy after 0.50·ATR reverse or next close" (don't naively delay — misses the fat tail) §54.15; scale-in / wait-for-better-price (min-threshold+max-window) §54.19; VWAP/TWAP order execution to cut slippage on larger orders §54.23; Elder Triple-Screen 3-step stop (entry-day low → breakeven → trail-50%-of-peak) §54.24**; **NEW exit_method sweep candidates with NO current equivalent: time-based exit / `max_hold` (nothing in the codebase caps holding period — relevant to the sim's 24-day avg hold and dead positions occupying rule slots) + close-strength exit (bar closes weakly within its own range → exit even with no stop hit; computable from `analysis/candles.py` range geometry; complements §34.1's close-based stop confirmation — both make the CLOSE the decision point) §57.2**; **2-bar channel trail as the TIGHT LOWER BOUND of the trail sweep — expected to lose on crypto (it is the shakeout stop §34.1 exists to fix); informative about asset noise if it ever wins §55.3**; **NEW split-exit shape §60.3** — fixed % target (source uses 7%) on HALF the position + tight ratchet trail (1-day-low minus a tick) on the remainder, with stop = the tighter of `entry×0.96` / `swing_low−tick`. Composes with any entry incl. the validated Turtle, so it is testable independently of §60.1's refuted-family entry; ⭐ **§65.7 price-determinacy is an independent COMPLIANCE argument for LIMIT over market entry** — converging with §58.1's empirical result and the maker-fee advantage, so **three independent arguments now point at one change**; **§65.8 the OCO/native bracket is CLEARED** (exactly one leg ever becomes a contract, each fully priced), with the design constraint that **no order structure may oblige a second trade at a price not stipulated in advance**; ⭐⭐ **§58.1 LIMIT entry at the breakout bar's midpoint** (one-bar validity) — best order type across ~80 controlled tests, *"the single most important thing one can do to improve a system's profitability"*; safe under the rails (an unfilled limit = no trade) and cuts BOTH slippage and Coinbase fees (maker < taker). Reconciles §54.15's entry-timing band with §58.1 as one sweep family over the offset; ⭐ **§58.12 stop width has an INTERIOR optimum (~1.5·ATR(50)) — too wide is ALSO bad**, correcting the one-directional "widen the stops" framing; sweep `stop_trigger` jointly (their intrabar MSES beat the close-only SES, which **tensions §34.1** — do not assume close-only wins); ⭐ **§58.13b MEMA one-way-EMA trailing stop** (init 2.5·ATR, offset 1.0·ATR, coeff 0.30; ranked #1 in a controlled 4-way stop test; **never widens, so it satisfies rail 10 by construction** rather than needing to be checked against it); **§58.14 shrinking profit target** (start 5.5·ATR, creep 0.10/bar); **§58.15 `max_hold`** — a second independent endorsement of §57.2, but their 6–10-bar scale is ~10× faster than our ~24-day hold, so **recalibrate, never port**; ⛔ **§58.13a the 2-bar HHLL trail tested "consistently too tight" (28% wins, holding period halved) → deprioritize §55.3 from the sweep** (its predicted failure, confirmed externally) | §2.2, §3.5, §7.4, §8.2, §17.3, §19.1, §23.2, §24.2, §26.2, §34.1, §54.6, §54.8, §54.15, §54.19, §54.23, §54.24, §55.3, §58.1, §58.12, §58.13, §58.14, §58.15, §60.3 | | `execution/guards.py` | **The hard rails** (see below); crypto-volatility calibration; API-key security / no-withdrawal scope / custody risk; **data-spike guard (implausible-vs-ATR bad-tick §24.3)**; **NEW Kaufman §54: price-shock detector (1-day range ≥ ~5·ATR) → crisis-management mode (take windfalls / hold-for-reversal / pause new entries / re-baseline trend after vol drops) §54.20; correlation rail should use a ROLLING 60-day correlation (single/avg hides the correlations-→1 crisis) §54.22; correlation-based unit caps (Turtle 4/6/10/12) §54.14**; **NEW §65.4 withdrawal-capability guard — treat broker-reported withdrawal suspension / account restriction / asset freeze as a COMPLIANCE-grade event, not merely operational** (constructive possession requires that nothing prevent taking delivery at will, so losing withdrawability breaks the *qabd* on which our spot-settlement claim rests); **blocks ENTRIES only** — blocking exits would trap capital, per §57.1's reasoning ⚠️⚠️ **CODE DEFECT (verified 2026-07-20, §83.3): rail 4's comment says "sum of at-risk capital across all open positions" but the code compares `total_exposure + intent.notional` to `max_exposure_usd` — it is NOTIONAL, i.e. **VOLATILITY-BLIND** ($5k at a 2% stop == $5k at a 20% stop). **Fix the comment regardless.** The proposed aggregate volatility cap `Σ qty·ATR ≤ V%·equity` is a genuine gap but **inert at one tranche/asset** (the 1% rail pins the aggregate near 3%) ⇒ build it as a **precondition of §75.1 concurrent slots / §26.1 pyramiding, not before.** | §4.1, §5.1, §8.1, §10.3, §10.4, §22.1, §22.3, §24.3, §54.14, §54.20, §54.22, §65.4 | | `CompliancePolicy` (HalalPolicy) | Shariah grounding of the exclusion set: riba (al-nasee'ah + **al-fadl §30.1**)/gharar/maisir → no leverage/derivatives/options; ownership+profit-loss-sharing → spot long-only permissible; **spot/immediate settlement mandatory (deferred same-commodity/currency exchange = riba); same-asset/stablecoin swaps only at parity §30.1**; **`haram_sector` screen at allowlist admission (✅ BUILT, PR #104: `keel/compliance/screen.py` + `keel assets screen|attest`; market facts COMPUTED, shariah classification ATTESTED, absent attestation FAILS CLOSED) — catch on-chain FUNCTION not just marketing: reject riba/lending/yield tokens (Aave/Compound/Maker/yearn §41.1) + maisir/prediction-market tokens (Augur §40.1)**; ⚠️ ~~low-turnover as compliance value~~ **CORRECTED by §65.6 — OVERSTATED**: *"speculation per se… is not prohibited"*; what makes speculation *maisir* is non-ownership / non-delivery / difference-settlement, **not frequency** ⇒ the **anti-scalping rail is PRUDENTIAL, not shariah-mandated** (it keeps its trading justification, loses its compliance claim); **AAOIFI = authoritative screening-standards reference (2 sources); keep policy pluggable + document our conservative interpretation §29.1–29.2**; **NEW positive obligation: optional zakat-estimate report (~2.5% mkt value/lunar yr, report-only) §33.1**; **NEW PROHIBITIVE account-level obligation: ensure interest/rewards on IDLE balances are DISABLED — Coinbase pays USDC rewards and rail 13 routes buys through USDC, so riba can accrue on our own cash with no order placed and no rail able to catch it (it is an account setting, not a trade) §56.3 — **ACTIONED:** designed in broker-abstraction spec §3.1 (`CompliancePolicy` needs an account-obligations surface alongside the per-order allow-check, each obligation marked machine-verifiable vs operator-attested) + `docs/operator-runbook.md`; **operator-attested, NOT machine-verifiable — Advanced Trade exposes no rewards/interest endpoint**; **"spot" on a product label is not evidence of spot settlement — retail "spot" FX is T+2, perpetually rolled to avoid delivery, with the interest differential charged for the deferral; the cleanest negative exemplar for the §30.1 immediate-settlement mandate §56.1** — ⚠️ **but §56.1 was stated TOO BLUNTLY and §66.3 CORRECTS it: "T+2 ⇒ riba" is wrong as written.** DSN-MUI treats a T+2 window as compatible with *spot* **provided real delivery concludes**; the actual defect is **perpetual rollover so delivery never happens**, on capital that could not cover delivery anyway ⇒ **delivery-concluding lag ≠ non-delivery-by-design**; **§66.1 `bay' al-sarf` NAMED and TRIANGULATED** (same-type ⇒ equal + spot; different-type ⇒ any rate but still spot) — the spot-settlement mandate now rests on **four** independent sources; ⭐ **§66.2 `qabd`: a direct hadith ("Sale of Food before Possession") forbids reselling before taking possession ⇒ a BUY tranche must be CONFIRMED-SETTLED before it is exit-eligible, on fiqh grounds and not merely operational hygiene** (the per-tranche `positions` ledger + per-cycle reconciliation shipped in PR #96 already implement this; the rationale is now recorded); ✅ **§67.1 CLOSES what §66.2 left open on custodial/DIGITAL qabd: OIC Fiqh Academy Res. 53/4-6 — electronic constructive possession via a custodial account that promptly and genuinely credits the asset SATISFIES possession** (upheld there even against a 2–7 day card-settlement lag; Coinbase's near-instant settlement clears it comfortably) ⇒ **exchange custody is now SOURCED, not an interpretive stance**; **§67.2 extends the sarf/spot grounding from currency to GOLD — closes a PAXG-specific gap §30.1 never covered** (gold being one of the six classical *ribawi* commodities); §67.4 gift-vs-disguised-riba test (regular/fixed/volume-linked ⇒ riba) sharpens the actioned §56.3; **§66.3 litmus test for "real spot": *"transfer of property, substantively or constructively"*** (from the Malaysian fatwa condemning retail FX) — adopt as the citable check; **§66.6** sharpens §29.2's divergence caveat with a live example (identical retail FX ruled haram by Malaysia/Perlis 2016, halal by Indonesia/DSN-MUI); ⚠️ **§66.7 NEGATIVE FINDING — not one compliance source in this KB addresses crypto/Bitcoin/blockchain/stablecoins directly; SILENCE IS NOT PERMISSION.** Next compliance source should be a targeted AAOIFI/OIC digital-asset resolution. **⭐ §65 (Ayub, 544pp) IS NOW THE PRIMARY CITABLE AUTHORITY — cite it over §28 wherever both cover a point.** `qabd` is now **triangulated by THREE independent sources** (§65.4 Ayub's OIC/AAOIFI constructive-possession forms · §66.2 the resale-before-possession hadith · §67.1 OIC Fiqh Academy Res. 53/4-6) ⇒ **exchange custody is settled, self-custody NOT required** — but with two live conditions: **withdrawal capability is a compliance PRECONDITION** (§65.4 — ✅ BUILT as guards rail 17, PR #108: entries-only, fails closed, live-read attestation) and the `'ayn`-vs-`dayn` custody question makes it a **per-BROKER attribute re-checked per venue**, not a general truth. **§65.9 income-purification — ✅ BUILT 2026-07-20 (PR #107), report-only.** ⚠️ The "NOT machine-computable" claim below was WRONG: it assumed API balance-delta inference and missed the typed reward rows in the imported transaction ledger (19 in our real history). The auto-purification objection stands and is respected — the shipped report adjusts nothing. Superseded reasoning: P&L half already handled (`analysis/pnl.py` ignores "deposits, rewards, sends"); equity half real but ~$1/trade of sizing effect; **NOT machine-computable here** (Advanced Trade has no rewards endpoint ⇒ operator-declared, same class as §56.3, so no advantage over it); and **auto-purification would violate `equity.py::_warn_on_unexplained_jump`'s deliberate detect-but-never-adjust rule** — infer-and-lower can mask a real drawdown and disarm rail 11. ⇒ **§56.3 (rewards off at the account level) is the PRIMARY control; purification is a go-live checklist item with an operator-declared path.** ⭐ **PAXG/gold §65.5 is stricter than BTC** (unambiguous `sarf`, gold futures forbidden outright, a 72-hour settlement tolerance) + a **new asset-backed-token admission check** (allocated/redeemable `'ayn` vs unsecured issuer claim `dayn`). ⚠️ Further §65 corrections: **riba ≠ gharar in kind** (§65.1 — riba strict-liability/no de minimis; gharar a materiality threshold; **ordinary price volatility is NOT gharar**); §30.1 restated correctly as the three-branch `'illah` test (§65.2); our screen is **stricter than DJIM/AAOIFI's ~5% tolerance** — keep but document as deliberate over-compliance (§65.10); **staking is CONTESTED, not settled riba** — refuse on §29.2 conservatism, not on a ruling we hold (§65.14). ⭐ §65.6 **trading for price appreciation is itself permissible** (never previously cleared). **The admission surface needs per-INSTRUMENT and per-BROKER attributes, not just a per-order check.** ⚠️⚠️ **§71.1 — THE FOUNDATIONAL PREMISE IS NOT SETTLED; see the callout above the §28 grounding note.** IIFA Res. 237 declined to affirm that crypto is Shariah-recognised tradable property ⇒ record it as an **interpretive position under §29.2, not a ruling.** ⭐⭐ **§71.4a ADD A PER-INSTRUMENT `ribawi`-BACKING CLASSIFIER AT ADMISSION — the allowlist is NOT juristically homogeneous:** gold/silver/fiat-backed ⇒ **CURRENCY ⇒ `bay' al-sarf` regime (⇒ PAXG and USDC)**; unbacked ⇒ **`urudh` ⇒ ordinary `bay' mutlaq`, expressly *"exempt from bai' al-sarf"* (⇒ BTC/ETH)**. Upgrades §65.5/§67.2/§30.1 from analogy to citation, and means **one policy cannot correctly govern PAXG and BTC**. ⭐ **§71.4b/§71.5 AAOIFI Shari'ah Standard No.18 §3/5 is the citation behind §65.5's asset-backed check** — its two conditions (named/acknowledged interest · **ability to transact in the underlying**) are the allocated-vs-unsecured-claim test, and applied to a token in a wallet they **triangulate `qabd` a FOURTH time, digital-specifically** (§65.4 · §66.2 · §67.1 · §71.5 all converging on *possession = ability to dispose*). **NEW per-BROKER attribute: registered/licensed with a recognised regulator** — the condition the permitting fatwas actually attach; **no source anywhere requires self-custody.** ⭐ **§71.6 `haram_sector` GAINS A SECOND AXIS — what the token legally REPRESENTS, not only what it does:** utility = `Haqq`, tradable if the project is compliant; **equity/revenue ⇒ REJECT BY CAPABILITY** (needs share-style business *and financial* screening we do not perform — a strong argument for keeping the allowlist to unbacked `urudh` coins plus one allocated asset-backed token); **buy-back-dependent ⇒ HARD REJECT** (contract-combination; generalises §65.8 from order structure to instrument). **§71.3 `thaman` split three ways, no majority** — keep the conservative reading, it breaks on any SAME-TYPE swap (USDC↔USDT) where *sarf*'s parity branch binds. **§71.7/§71.8 record BOTH camps in our own docs, including the one against us** — prohibitions are **explicitly revisable if regulation/security improve** (conditional, unlike riba's strict liability §65.1) and several target unregulated P2P rather than regulated spot purchase; Malaysia is split against itself at state level. ⚠️ **§71.8 tension with §65.6** — permissions condition on *"not motivated solely by speculative profit"*, reintroducing MOTIVE; **§65.6 prevails, dissent recorded not adopted**, so these fatwas are **not straightforwardly permission for a trading agent**; our defence is substantive (real asset, real delivery, ~24-day holds, no difference-settlement). ⚠️ **§71.1/§71.7 do NOT reopen §65.1** — volatility-as-gharar is recorded as a live disagreement, not a finding; ordinary price volatility is still not gharar. **§72 — the academic literature ALSO lands unresolved**: a thesis panel splits **50/50** on *mal mutaqawwim*; a six-traits review scores Bitcoin above fiat yet concludes against it; a third paper records no unanimity — and **none of the four cites IIFA Res. 237**, so they corroborate §71.1 independently. **Do not read a split, thin literature as settling what the OIC declined to settle.** §72 also adds a second named OIC citation (63rd Res., 1992, options) confirming §65.11; a thin PoS=haram assertion that **reinforces but does not resolve** §65.14; and the **OneGram case corroborating §65.5's `'ayn` test**, with **4 of 5 tokens marketed "shariah-compliant" failing a basic backing/oversight check ⇒ check the SUBSTANCE, never the label** | §28.1–28.4, §29.1–29.2, §30.1, §30.3, §33.1, §40.1, §41.1, §56.1, §56.3, **§65.1–§65.14**, §66.1–66.3, §66.6, §66.7, §67.1–67.2, **§71.1–§71.10**, §72.1–§72.5 | diff --git a/docs/superpowers/references/trading-knowledge-base/sources/source-84.md b/docs/superpowers/references/trading-knowledge-base/sources/source-84.md new file mode 100644 index 00000000..189415fb --- /dev/null +++ b/docs/superpowers/references/trading-knowledge-base/sources/source-84.md @@ -0,0 +1,345 @@ +[← Knowledge Base index](../README.md) + +## Source 84 — `keeks` bankroll-management library + the "Bankroll Management with Keeks" series + +**Provenance:** The [`keeks`](https://github.com/wdm0006/keeks) Python library (v0.3.0, by Will +McGinnis) — an educational implementation of the **Kelly Criterion and its variants** for optimal +capital allocation — read in full from the local checkout at +`/Users/elmehdiaitbrahim/Development/work/CodeGate/keeks` (package `keeks/`, `tests/`, `docs/`, +`examples/`). Plus the author's nine-part blog series that documents each strategy: + +| # | Post | URL | +|---|---|---| +| a | keeks 0.3.0 release (Merton share) | `mcginniscommawill.com/posts/2025-10-15-keeks-0_3_0-release/` | +| b | Fractional Kelly | `.../2026-01-16-fractional-kelly/` | +| c | Drawdown-Adjusted Kelly | `.../2026-01-23-drawdown-adjusted-kelly/` | +| d | Optimal f | `.../2026-01-30-optimalf/` | +| e | Fixed Fraction | `.../2026-02-06-fixed-fraction/` | +| f | CPPI | `.../2026-02-13-cppi/` | +| g | Dynamic Bankroll Management | `.../2026-02-20-dynamic-bankroll-management/` | +| h | Naive Strategy | `.../2026-02-27-naive-strategy/` | +| i | Strategy Comparison | `.../2026-03-06-strategy-comparison/` | + +> ### ⚠️ Disclaimer — read first (halal framing + educational-only) +> +> **Betting/gambling (*maysir*) is forbidden in Islam, and this project does not bet.** The +> `keeks` library is written in the vocabulary of wagering (bankroll, odds, payoff, ruin). We +> extract it for **one reason only: the underlying mathematics of optimal *capital allocation* +> under uncertainty is the same mathematics that governs how much of our cash to commit to a +> spot position.** The Kelly Criterion is a growth-rate optimiser over log-wealth — a portfolio +> result (Merton 1969; Thorp) — not a betting trick. We adopt the **sizing math**, never the +> betting context. Everywhere the source says "bet fraction," read it as **"fraction of trading +> capital risked on a long-only spot entry with a defined stop."** +> +> `keeks`'s own README carries the parallel caveat: *"for educational purposes only … not +> investment, legal, or tax advice … consult a professional."* Same here. **Nothing in this +> source is wired into the live agent by being written down** — every candidate must clear the +> paper-trading proving gate and a backtest floor before it sizes a single real order (§84.16). + +--- + +### §84.1 — Why this matters for keel (the anchor: we already ship one of these) + +keel sizes every stop-bearing entry with **fixed-fractional risk sizing**: +`keel/execution/sizing.py::size(equity, risk_pct, entry, stop)` risks `risk_pct` of equity over +the entry→stop distance, `qty = equity·risk_pct / |entry − stop|`. The config default is +**`risk_pct = 0.01`** (`keel/templates/config.yaml`). That is *exactly* the **Fixed Fraction** +strategy of §84.7 — the simplest member of the whole family below. So this source is not exotic: +it is the map of the neighbourhood around the one allocator keel already uses, and it lets us ask +a sharp question — *is 1% the right fraction, and how would the Kelly family answer?* + +**The Kelly baseline computed on keel's own promotion floor.** keel only lets a rule trade once it +clears `min_win_rate = 0.55` and `min_rr = 1.5` (`packages/keel-core/keel_core/config.py`). Feed +that floor into Kelly (§84.2): + +``` +f* = (b·p − q) / b with p = 0.55, q = 0.45, b = 1.5 + = (1.5·0.55 − 0.45) / 1.5 = 0.375 / 1.5 = 0.25 +``` + +**Full Kelly would risk ~25% of capital per trade at our floor edge; half-Kelly 12.5%, +quarter-Kelly 6.25%. keel risks 1% — about 4% of full Kelly, i.e. *below even quarter-Kelly*.** +Whether that is admirable prudence or growth left on the table is the question the simulation in +§84.14 measures on our own terms. (Preview of the answer: sub-Kelly is *correct* here, but the +reasoning — estimation error and correlation — matters more than the number.) + +--- + +### §84.2 — Kelly Criterion (the baseline) — `KellyCriterion` + +**Formula (net-odds binary form):** `f* = (b·p − q) / b`, where `p` = win probability, +`q = 1 − p`, `b` = net payoff-to-loss odds (win pays `b×` the amount risked). Equivalent +"edge/odds" form: `f* = p − q/b`. Maximises the expected log growth rate `E[log W]` — i.e. CRRA +utility at risk-aversion `γ = 1`. keeks adjusts for costs first: `payoff' = payoff − tc`, +`loss' = loss + tc`, then `b = payoff'/loss'`; returns 0 if `p < min_probability` (default 0.5) or +the edge is non-positive; clamps to a max-safe bet so the bankroll can't go negative. + +**Why it's the reference, not the recommendation:** Kelly is growth-optimal *only* when `p` and `b` +are known exactly and trades are independent. Both assumptions fail in trading — our `p` is a +noisy backtest estimate and our positions are correlated crypto. Full Kelly's expected drawdown is +punishing (~50% is routine), and **over-estimating `p` pushes you past the growth peak into +*negative* growth** (§84.14). Every other strategy below is a way of buying robustness back. + +**keel mapping:** a diagnostic ceiling, not a sizer. Given a rule's backtested `win_rate` and +`R:R`, `f*` tells you the *most* any sane fixed-fractional `risk_pct` should ever be. Our 1% sits +far under it — deliberately. + +--- + +### §84.3 — Fractional Kelly — `FractionalKellyCriterion` + +**Formula:** `f = λ · f*`, `λ ∈ (0,1]` (½ and ¼ are standard). The growth curve is *quadratic* in +`λ` (`G(λ) ≈ λK − λ²K/2` about the risk-free rate), so growth is flat near the top: **half-Kelly +keeps ~75% of the growth for ~50% of the variance; quarter-Kelly ~44% growth for ~25% variance.** +That asymmetry is the single most useful fact in the whole source — you give up little growth to +buy a lot of calm, and you buy insurance against having over-estimated your edge. + +Author's practitioner guidance: individuals 25–50% Kelly; professional managers 10–20% (capital +preservation); shift down a fraction when `p` is uncertain or the bankroll is small. + +**keel mapping — the most directly usable idea here.** A principled way to set `risk_pct` per rule: +`risk_pct = λ · f*(win_rate, R:R)` with a small `λ` (¼ or less) and a hard cap. This makes sizing +*edge-aware* (stronger rules risk more) instead of a flat 1% for everyone — a **candidate lead**, +gated in §84.16. + +### §84.4 — Drawdown-Adjusted Kelly — `DrawdownAdjustedKelly` + +Two forms exist and they differ; keep them straight: +- **Blog (dynamic):** `f = (1 − d/D) · f*`, where `d` = current drawdown from peak, `D` = max + acceptable drawdown. Bets shrink to zero as `d → D` — an automatic brake during losing streaks. +- **keeks class (static):** `f = min(1, D/0.5) · f*` — a one-time scale by "your tolerance vs + Kelly's ~50% expected drawdown." Simpler, not state-dependent. + +**keel mapping:** the *dynamic* form is the interesting one and it **overlaps our existing +account-DD breaker** (the design's total/weekly drawdown circuit-breaker). keel already halts on +deep drawdown; drawdown-adjusted Kelly would instead *taper* size continuously before the halt. +Candidate: a graded taper feeding the CTS execution ladder, not a new hard rail. + +### §84.5 — Optimal f (Ralph Vince) — `OptimalF` + +**Idea:** maximise Terminal Wealth Relative `TWR(f) = Π(1 + f·Rᵢ)` over the *historical* return +sequence `{Rᵢ}` — no forward probability needed; it fits `f` to what actually happened. keeks's +binary implementation reduces to `f = p − (1−p)/(reward/risk)` capped by `max_risk_fraction` +(default 0.2). **Tends to size *larger* than Kelly and draws down harder; the author says use +50–70% of the computed value and ≥30–50 trades of history.** + +**keel mapping — mostly a caution.** Optimal f is acutely sensitive to the single worst historical +loss and to over-fitting a short record — exactly the failure mode our KB has fought (PBO, MinBTL, +the §79/§74 "settled by measurement" table). **Deferred**: interesting as a lens on our R-multiple +distributions, dangerous as a live sizer on 31-trade samples. + +### §84.6 — Merton share / CRRA — `MertonShare` + +**Formula:** `f = μ / (γ·σ²)` — expected excess return over `γ` × variance; `γ` = relative +risk-aversion. `γ = 1` recovers Kelly; higher `γ` = smaller size. keeks's 0.3.0 sim: at 55%/1000 +bets, `γ=2` cut volatility 61% while keeping 84% of Kelly's return; `γ=5` cut volatility 85% +keeping 77%. This is the *continuous* generalisation of fractional Kelly (choosing `γ` ≈ choosing +`λ`), and it makes the risk-aversion knob explicit and defensible. + +**keel mapping:** a cleaner theoretical framing for "why sub-Kelly" than an ad-hoc `λ`. Same +candidate as §84.3, expressed as a `γ` we can defend (`γ ≈ 2` is the standard human estimate). + +### §84.7 — Fixed Fraction — `FixedFractionStrategy` ★ this is keel today + +**Formula:** `bet = c · bankroll`, constant `c`, ignoring odds and edge. keeks default and the +blog's guidance land at ~1–3%. "Theoretically impossible to go fully broke (but you can get +close); often leaves money on the table with strong edges." keeks's Monte-Carlo optimal fixed +fractions: 52% edge → ~1.5–2%, 55% → ~2–2.5%, 60% → ~3–4%. + +**keel mapping — identity.** `sizing.size(...)` with `risk_pct` IS this. Note keel's 1% is *below* +even the 55%-edge optimal (~2–2.5%) that keeks found for a **known** 55% edge — consistent with our +edges being *estimated*, not known. The library's own finding "optimal fixed fraction is typically +lower than the Kelly fraction" is our lived reality. + +### §84.8 — CPPI (Constant Proportion Portfolio Insurance) — `CPPIStrategy` + +**Formula:** `exposure = m · (bankroll − floor)`; `cushion = bankroll − floor`; floor ratchets up +at new peaks. Size scales up as you win, and *automatically* toward zero as you approach the floor. +Prioritises capital preservation over growth; **gap risk** is the named failure (a sudden loss can +breach the floor). Parameter menu: conservative floor 90%/`m`=2 … aggressive floor 60%/`m`=5. + +**keel mapping:** conceptually close to keel's **total-exposure cap + account-DD breaker** already +in the rails, but expressed as a smooth allocator instead of a hard clamp. The **Kelly-CPPI hybrid** +the comparison post recommends ("size by Kelly but never let capital fall below a floor") is +essentially *what keel already does structurally* (risk-sized order, clamped by caps). Good +vocabulary for documenting our design; not a new build. + +### §84.9 — Dynamic Bankroll Management — `DynamicBankrollManagement` + +**Formula:** `f = base · (streak × volatility × drawdown × probability factors)`, clamped to +`[min_fraction, max_fraction]`. Adapts size to recent performance. **Author's own caveats: complex, +parameter-heavy, "risks overreacting to normal variance," hard to backtest.** + +**keel mapping — ⛔ mostly declined.** The streak factor is a soft **martingale/anti-martingale**, +and increasing size after wins collides with our **no-martingale rail** and our repeatedly-measured +lesson that streak-chasing adds *correlated* trades (README "settled by measurement" table). The +one defensible sub-component is the *drawdown* factor — which is just §84.4. + +### §84.10 — Naive / flat stake — `NaiveStrategy` + +Two meanings again: the **blog's Naive** = flat dollar stake every trade (`bet = const`), the +crudest baseline; **keeks's `NaiveStrategy` class** = risk-neutral EV-proportional +(`f = EV/payoff` if `EV > 0`). **keel mapping:** the flat-dollar form is our **DCA sizing** +(`dca_size`: fixed USD budget ÷ price, no stop) — so keel *already* runs a "naive" sizer for its +stopless accumulation rule, correctly, for a different job than risk sizing. Useful as the +simulation's floor baseline. + +--- + +### §84.11 — Strategy comparison matrix (from the "Strategy Comparison" post) + +The author's qualitative ranking (1000 bets, p=0.55, even money, seed 43), reframed for keel: + +| Strategy | Growth | Drawdown risk | Complexity | keel verdict | +|---|---|---|---|---| +| Full Kelly | Excellent | High | Moderate | ceiling/diagnostic only | +| Fractional Kelly (½) | Very good | Moderate | Moderate | **candidate sizer (§84.3)** | +| Drawdown-Adj. Kelly | Good | Low–Mod | High | candidate taper (§84.4) | +| Optimal f | Excellent | High | High | deferred — overfit risk | +| Merton (γ≈2) | Very good | Moderate | Moderate | candidate framing (§84.6) | +| Fixed Fraction | Good | Moderate | **Low** | **keel today (1%)** | +| CPPI | Moderate | **Very low** | Moderate | ≈ existing rails | +| Dynamic BM | Good | Moderate | High | ⛔ martingale-adjacent | +| Naive flat | Low | High | Very low | = keel DCA sizing | + +Author's headline: *"there's no one-size-fits-all … even the most mathematically optimal strategy +is only as good as your ability to stick with it."* That psychological-adherence point is the same +one §54 (Kaufman) and §83 (Zerodha) already make — it's why keel is deliberately conservative. + +### §84.12 — The `keeks` API, for reference (if we ever port a formula) + +- **`BankRoll(initial_funds, percent_bettable=1.0, max_draw_down=0.3)`** — stateful funds tracker; + `remove_funds`/`withdraw` raise `RuinError` on bankruptcy or on exceeding `max_draw_down` per + transaction. `history` is the equity curve. +- **Strategies** — all subclass `BaseStrategy`, implement `evaluate(probability, current_bankroll) + → fraction ∈ [0,1]`, and share `get_max_safe_bet()` clamping. Utility strategies add + `calculate_max_entry_price(outcomes, probabilities, wealth)` for one-shot gambles. +- **Simulators** — `RepeatedBinarySimulator` (fixed `p`), `RandomBinarySimulator` (`p ~ N(0.5,σ)`), + `RandomUncertainBinarySimulator` (perceived vs actual `p` — the *estimation-error* model we + borrow in §84.14). All call `evaluate_strategy(strategy, bankroll)` and mutate the bankroll + in-place, stopping gracefully on `RuinError`. +- **`utils`** — `crra_utility(W, γ)` (`log W` at γ=1, else `W^{1−γ}/(1−γ)`), + `expected_utility`, `find_indifference_price` (binary search — resolves St. Petersburg to a + finite price under risk aversion). **Deps: numpy/matplotlib/pandas** — heavier than keel wants, + which is why our own simulation (§84.14) re-implements only the handful of formulas in stdlib. + +### §84.13 — Halal / adaptation screen (summary) + +Nothing here involves *riba* (no borrowing/leverage — Kelly sizes cash only), and we strip the +*maysir* context entirely: these are capital-allocation formulas applied to long-only spot entries +with defined stops. **Usable:** fractional-Kelly / Merton sizing (§84.3/§84.6), drawdown taper +(§84.4). **Already ours:** fixed fraction (§84.7), CPPI-like caps (§84.8), naive/DCA (§84.10). +**Declined:** dynamic streak-scaling (§84.9, martingale-adjacent), optimal f as a live sizer +(§84.5, overfit). No shorting, no leverage, no derivatives touched by any of it. + +### §84.14 — Simulation on OUR terms (measured, not asserted) + +We re-implemented the handful of formulas above in **pure stdlib** (no numpy/pandas) and ran a +seeded Monte-Carlo to answer §84.1's question directly. Code: +`docs/superpowers/analysis/bankroll_sizing/` (`sizing_strategies.py`, `simulate.py`, +`test_sizing_strategies.py` — **38 unit tests, all pass**). Full write-up: +[`docs/superpowers/reports/2026-07-22-bankroll-sizing-comparison.md`](../../../reports/2026-07-22-bankroll-sizing-comparison.md). + +**Experiment 1 — reproduce the keeks binary comparison** (1000 bets, p=0.55, even money, +$1000, 500 seeded paths). The point is the *ordering*, and it reproduces the literature exactly: +growth and drawdown both rise monotonically with the Kelly fraction. + +| Strategy | Median terminal | Median max-DD | Ruin | +|---|---|---|---| +| Full Kelly | $122,449 | **89.6%** | 0% | +| Half Kelly | $38,572 | 61.0% | 0% | +| Quarter Kelly | $8,482 | 35.5% | 0% | +| **Fixed-1% (keel)** | $2,535 | **15.3%** | 0% | +| Drawdown-adj Kelly (D=0.20) | $993 | 20.0% | 0% | +| CPPI (floor 0.8, m=3) | $640 | 60.0% | 0% | +| Naive-flat $10 | $1,980 | 11.4% | 0% | + +**Experiment 2 — risk_pct vs the Kelly family AT keel's floor edge** (Profile A: p=0.55, b=1.5; +200-trade sequences, 500 paths). Terminal shown as a *multiple* of starting capital. The right +two columns are the punchline — the **same sizing, but the true win-rate came in 5 points below +the estimate** (still a positive edge): + +| Sizing (risk fraction) | p correct: median × | p correct: worst DD | p over-est 0.05: median × | over-est: ruin | +|---|---|---|---|---| +| **keel-1%** | 2.0× | 19% | 1.6× | **0%** | +| Quarter-Kelly (6.25%) | 49× | 76% | 12× | **0%** | +| Half-Kelly (12.5%) | 721× | 95% | 46× | **0%** | +| Full-Kelly (25%) | 5077× | **99.9%** | **22×** | **3.6%** | + +*(Multiples are frictionless geometric compounding in an i.i.d. model — illustrations of the +growth/safety gradient, NOT return forecasts; see caveats below.)* + +**What this means for keel — two true things at once:** +1. **1% is mathematically far to the safe side.** Every sub-Kelly level tested compounds a real + edge far faster than 1% does — because 1% barely lets a genuine edge compound geometrically. + *If* our backtested p/b were trustworthy point estimates, something around **quarter-Kelly + (~6%) would capture most of the growth** at a fraction of full-Kelly's ~90% drawdowns. +2. **…but sub-Kelly is the correct posture, and 1% is a defensible extreme of it.** The + estimation-error column is decisive: over-estimating p by 0.05 collapses **full-Kelly** from + 5077× to 22× and lifts its ruin rate from 0% to **3.6%**, while quarter-/half-Kelly *and* keel's + 1% all keep **0% ruin**. Full Kelly assumes a *known* edge; ours is a **noisy backtest + estimate** (§58.11: "W/R off ~23 trades is noise wearing a formula"), so the fractional-Kelly + margin of safety is exactly the right instinct. keel sits on the safe side of that argument — + just pushed further than the math alone requires. + +**Caveats (from the report):** i.i.d. independent trades (real crypto positions are *correlated* — +understates higher-fraction risk); known b, no fees/slippage; a single fixed misestimation +magnitude; keel's real order/day/exposure caps not modelled; float money (keel's real path is +Decimal-only). **This is an observation about the growth/safety tradeoff, NOT a recommendation to +change `risk_pct`** — any change would need its own review against the live rails and correlation. + +### §84.15 — Commands to explore the rules & strategies (educational) + +All read-only; none place an order. Use them to *see* the `p` and `R:R` that feed the Kelly math +above, and to run the sizing study yourself. + +```bash +# --- the rule library & lifecycle (candidate → paper → live → disabled) --- +keel rules list # every rule + status + params +keel rules list --status live # filter by lifecycle stage +keel rules seed # populate one candidate per (kind, product) from defaults + +# --- a rule's edge stats: win_rate (p) and R:R (b) are the Kelly inputs --- +keel rules backtest # → n_trades, win_rate, expectancy, profit_factor, max_drawdown +keel rules promote # re-backtest and advance IF it clears the floor (0.55 / 1.5) + +# --- the bankroll-sizing study from this source (§84.14) --- +uv run python docs/superpowers/analysis/bankroll_sizing/simulate.py # regenerates the report +uv run pytest docs/superpowers/analysis/bankroll_sizing/test_sizing_strategies.py -q # 38 tests +``` + +Kelly ceiling for any rule, from its backtested stats — a sanity-check on a hand-set `risk_pct`, +**not** an autonomous sizer (see §84.16): + +```python +from docs.superpowers.analysis.bankroll_sizing.sizing_strategies import kelly_fraction, fractional_kelly +p, b = 0.55, 1.5 # e.g. keel's promotion floor +kelly_fraction(p, b) # 0.25 → full-Kelly ceiling +fractional_kelly(p, b, 0.25) # 0.0625 → quarter-Kelly reference +# keel's actual risk_pct = 0.01 is ~4% of the full-Kelly ceiling. +``` + +### §84.16 — Takeaways & candidate leads (all gated by the paper-proving floor) + +**Verdict — like §83, this source CONFIRMS the risk model, it does not reshape it.** The Kelly +family is a fourth independent route to "use fractional f, never full" (§54.18, §83.5, §83.11), and +our own simulation shows keel's deeply-sub-Kelly 1% is a *defensible* answer to estimation error, +not mere timidity. + +- ✎ **Candidate (humble): edge-aware `risk_pct` as a ceiling/sanity-check.** `λ·f*(win_rate, R:R)` + with a small `λ` and a hard cap could make sizing edge-aware. **Gate:** §58.11/§83.5 already + ruled per-rule W/R too noisy to size on autonomously — so use it to *flag* a hand-set `risk_pct` + that exceeds a fractional-Kelly ceiling, never to set it. Deterministic; cheap to prototype. +- ✎ **Candidate: dynamic drawdown taper (§84.4)** `(1−d/D)·f*` — taper size continuously as + account drawdown builds, *before* rail 11's hard breaker halts. Feeds the CTS execution ladder, + not a new rail. The one item here with no existing keel analog. +- ⛔ **Declined:** dynamic streak-scaling (§84.9, anti-martingale — collides with the no-martingale + rail + the "correlated trades" lesson); optimal-f as a live sizer (§84.5, overfits the worst + historical loss on 31-trade samples). +- ⧉ **Already ours, now with vocabulary:** fixed fraction (§84.7 = `sizing.size`), CPPI-like caps + (§84.8), naive/DCA (§84.10 = `dca_size`). The "Kelly-CPPI hybrid" the series recommends is, + structurally, what keel already does (risk-sized order, clamped by exposure caps). + +**Cross-references:** design spec `docs/superpowers/specs/2026-07-15-halal-cb-autotrade-design.md` · §54.18 (optimal-f + fractional) · §83.1 +(equity base) · §83.5/§83.11 (Kelly = `W−(1−W)/R`) · §58.11 (small-sample W/R is noise) · +§54.7 (volatility-parity sizing) · §33/§50.1/§54.22/§68 (MPT declined — Kelly is *not* MPT). diff --git a/docs/superpowers/reports/2026-07-22-bankroll-sizing-comparison.md b/docs/superpowers/reports/2026-07-22-bankroll-sizing-comparison.md new file mode 100644 index 00000000..687e5651 --- /dev/null +++ b/docs/superpowers/reports/2026-07-22-bankroll-sizing-comparison.md @@ -0,0 +1,91 @@ +# Bankroll Sizing Comparison: Kelly Criterion Family vs keel's Fixed-Fractional Risk + +## Purpose and framing + +`keel` is a halal (riba-free), spot-only, long-only, no-leverage crypto trading agent. It currently sizes every trade with fixed-fractional risk sizing (`keel/execution/sizing.py::size`): risk a constant `risk_pct` of equity per trade over the entry-to-stop distance, with a config default of `risk_pct = 0.01` (1%). + +This report studies the Kelly Criterion and its relatives (drawn from the `keeks` family of bankroll-growth formulas) **purely as mathematics of optimal capital allocation** -- a Monte Carlo exercise in bankroll-growth arithmetic run against simulated win/loss trade sequences with stdlib-only Python. Nothing here trades real money, wagers on chance for its own sake, or involves interest (riba); it is a study of how fast a bankroll compounds under different constant-risk-fraction rules, applied to keel's own promotion-floor edge numbers, to ask an engineering question: is keel's fixed 1% risk needlessly conservative, or is there a good reason to stay conservative anyway? + +## Thesis + +keel's PROMOTION FLOOR rule requires win_rate >= 0.55 and R:R (min_rr) >= 1.5 before a strategy is promoted to live trading. For a rule sitting exactly at that floor (p=0.55, b=1.5), full-Kelly risk fraction is: + +``` +f* = (b*p - q) / b = (1.5 * 0.55 - 0.45) / 1.5 = 0.375 / 1.5 = 0.25 (25% of equity per trade) +half-Kelly = 0.1250 (12.50%) +quarter-Kelly = 0.0625 (6.25%) +keel's actual risk_pct = 0.01 (1.00%) ~= 4.0% of full Kelly +``` + +The question: is keel leaving growth on the table by risking only ~4% of the full-Kelly-implied fraction at its own promotion floor, or is sub-Kelly sizing correct once you account for estimation error in `p`/`b`, correlation between trades, and drawdown pain that a pure log-growth-maximizer ignores? + +## Method + +Two deterministic (seeded) Monte Carlo experiments, implemented in `simulate.py` next to this report, using only the Python standard library (`random`, `statistics`). Every path uses `random.Random(seed)` with an explicit integer seed; strategies compared within the same experiment share seeds per path index (common random numbers), so differences between strategies reflect sizing, not differing luck. Money is modeled as `float` (this is an educational sim, not the Decimal-only production `keel` sizing code). A trade wins with probability `p`, paying `+b * f * bankroll` where `f` is the fraction risked and `b` is the reward:risk multiple; a loss costs `f * bankroll`. A path is considered ruined and stopped once bankroll falls to or below $1 (bankroll cannot go negative under fractional betting, but going effectively to zero is treated as ruin). + +## Experiment 1: reproducing the keeks binary comparison + +Setup: 1000 bets, p=0.55, even-money (b=1.0), initial bankroll $1,000, 500 independent seeded paths per strategy. Strategies: Full Kelly, Half Kelly, Quarter Kelly, Fixed-1% (keel), CPPI (floor ratchets at 80% of peak equity, multiplier=3), Naive-flat ($10 constant stake), and Drawdown-adjusted Kelly (scales full Kelly to zero as current drawdown approaches a 20% tolerance ceiling). + +| Strategy | Median terminal | Mean terminal | Stdev terminal | Median max DD | Ruin rate | +|---|---|---|---|---|---| +| Full Kelly | $122,449.19 | $16,950,828 | $122,915,963 | 89.55% | 0.00% | +| Half Kelly (0.5) | $38,571.91 | $165,237.73 | $418,200.53 | 61.02% | 0.00% | +| Quarter Kelly (0.25) | $8,481.72 | $12,625.86 | $12,188.01 | 35.53% | 0.00% | +| Fixed-1% (keel) | $2,534.59 | $2,737.90 | $930.42 | 15.32% | 0.00% | +| CPPI (floor=0.8, m=3) | $640.00 | $1,560.75 | $6,300.56 | 60.00% | 0.00% | +| Naive-flat ($10) | $1,980.00 | $2,003.84 | $324.11 | 11.37% | 0.00% | +| Drawdown-adj Kelly (max_dd=0.20) | $993.44 | $1,225.88 | $637.01 | 20.00% | 0.00% | + +**Reading this table**: Full Kelly has the highest median/mean terminal wealth, but also the widest dispersion (stdev) and the deepest typical drawdowns -- the classic Kelly trait of being growth-optimal in expectation while remaining a psychologically brutal ride. Half- and Quarter-Kelly trade away some terminal wealth for a large cut in drawdown depth and variance -- this is the textbook "why half-Kelly" lesson the `keeks` library is built to demonstrate. keel's Fixed-1% sits far below all Kelly variants on terminal wealth because it never lets its risk keep pace with a compounding bankroll's *edge*, but it also never comes close to the Kelly variants' drawdowns. CPPI at multiplier=3 with an 80%-of-peak floor risks a large fraction of the cushion (60% of bankroll at a fresh high) -- well above this edge's Kelly-optimal level -- and its results show the cost of over-levering an insurance-style rule. Naive-flat ($10) decays into an ever-shrinking fraction of a growing bankroll (or a growing fraction of a shrinking one), producing its own distinct, non-Kelly growth curve. + +## Experiment 2: risk_pct vs the Kelly family at keel's own edge numbers + +Setup: 200-trade bootstrap sequences, 500 seeded paths, initial bankroll $1,000. Each trade wins with probability `p` paying `+b * (risk_pct * equity)`, else loses `risk_pct * equity`. Two edge profiles: (A) the promotion floor itself, p=0.55, b=1.5; (B) a stronger edge, p=0.58, b=2.0. Sizing levels are constant risk fractions: keel-1% (0.01), and the Quarter-/Half-/Full-Kelly fractions implied by each profile's own p and b. For each profile, two worlds are simulated: **p correct** (the realized win rate matches what sizing assumed) and **p over-estimated by 0.05** (sizing was computed assuming the stated p, but the true win rate actually realized is 5 percentage points lower -- an estimation-error stress test). + +### Profile A (floor edge: p=0.55, b=1.5) + +Kelly fractions for this profile: Quarter-Kelly=6.25%, Half-Kelly=12.50%, Full-Kelly=25.00%, keel-1%=1.00%. + +| Sizing level | Risk fraction | World | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | +|---|---|---|---|---|---|---| +| keel-1% | 1.00% | p correct | 2.030x | 6.90% | 19.18% | 0.00% | +| Quarter-Kelly | 6.25% | p correct | 49.142x | 38.81% | 76.00% | 0.00% | +| Half-Kelly | 12.50% | p correct | 720.771x | 65.60% | 95.44% | 0.00% | +| Full-Kelly | 25.00% | p correct | 5076.555x | 93.20% | 99.94% | 0.00% | +| keel-1% | 1.00% | p over-estimated by 0.05 | 1.622x | 8.75% | 24.70% | 0.00% | +| Quarter-Kelly | 6.25% | p over-estimated by 0.05 | 12.273x | 46.42% | 87.20% | 0.00% | +| Half-Kelly | 12.50% | p over-estimated by 0.05 | 46.150x | 75.15% | 99.16% | 0.00% | +| Full-Kelly | 25.00% | p over-estimated by 0.05 | 21.697x | 97.46% | 100.00% | 3.60% | + +### Profile B (stronger edge: p=0.58, b=2.0) + +Kelly fractions for this profile: Quarter-Kelly=9.25%, Half-Kelly=18.50%, Full-Kelly=37.00%, keel-1%=1.00%. + +| Sizing level | Risk fraction | World | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | +|---|---|---|---|---|---|---| +| keel-1% | 1.00% | p correct | 4.150x | 5.87% | 16.55% | 0.00% | +| Quarter-Kelly | 9.25% | p correct | 78446.605x | 45.49% | 82.57% | 0.00% | +| Half-Kelly | 18.50% | p correct | 148341260.795x | 73.33% | 97.48% | 0.00% | +| Full-Kelly | 37.00% | p correct | 40467861601.408x | 96.73% | 99.98% | 0.20% | +| keel-1% | 1.00% | p over-estimated by 0.05 | 3.079x | 6.82% | 15.73% | 0.00% | +| Quarter-Kelly | 9.25% | p over-estimated by 0.05 | 5443.234x | 50.53% | 81.28% | 0.00% | +| Half-Kelly | 18.50% | p over-estimated by 0.05 | 823440.800x | 80.22% | 98.01% | 0.00% | +| Full-Kelly | 37.00% | p over-estimated by 0.05 | 1566835.013x | 98.60% | 100.00% | 0.60% | + +## What this means for keel + +**Is 1% too timid?** Mathematically, yes, relative to the growth-maximizing Kelly fraction: at the promotion floor (p=0.55, b=1.5) full Kelly is 25% of equity per trade, and keel's 1% is roughly 4% of that. In the "p correct" worlds of Experiment 2, every Kelly-family fraction (even Quarter-Kelly) compounds to a dramatically larger median terminal multiple than keel-1% over 200 trades, because 1% barely lets a real edge compound -- the bankroll grows close to linearly rather than geometrically at that scale. Purely as an optimal-growth-rate statement, the thesis holds: keel is far to the conservative side of the Kelly curve. + +**Does the estimation-error run defend sub-Kelly?** Yes, and this is the more important half of the story. In the "p over-estimated by 0.05" worlds, Full-Kelly's edge assumption breaks: at the floor profile (b=1.5, breakeven p=0.40), an assumed p=0.55 with a true p=0.50 is still a real edge -- full Kelly at the *true* p=0.50 would be ~16.7% (down from the 25% it was sized at), not zero -- but Full-Kelly was sized as if the edge were 8-plus points thicker than it actually is, and that overbetting shows up directly in the numbers: median terminal multiple collapses from 5077x ("p correct") to 22x ("p over-estimated"), and a ruin rate that was 0.0% becomes 3.60%. Half- and Quarter-Kelly degrade far more gracefully under the identical misestimation (their ruin rates stay at 0.0%), because they were never betting the full assumed edge in the first place -- the classic argument for sub-Kelly sizing is that it functions as a margin of safety against exactly the kind of parameter error a live trading system cannot avoid (p and b are estimated from a finite, noisy backtest sample, not known constants). keel's actual 1%, while far more conservative than even Quarter-Kelly, sits on the same side of that argument as the fractional-Kelly strategies: it is far more robust to an over-optimistic edge estimate than Full-Kelly is, just at a much larger cost in forgone growth. + +**Net read**: the honest conclusion is that keel's 1% is not "wrong" -- it is an extreme point on the same sub-Kelly safety spectrum that Half- and Quarter-Kelly occupy, just pushed much further toward safety than the math alone would require. If keel's backtested p and b estimates were trustworthy point estimates with no correlation between trades, something in the Quarter-Kelly neighborhood (order of 5-6% at the floor edge) would capture most of the available growth while still being far more robust to estimation error than Full- or Half-Kelly. keel's actual 1% leaves a substantial amount of that growth unclaimed. Whether closing some of that gap is worth it depends on factors this simulation does not model (see Assumptions below) -- most importantly, real trades are not independent, identically-distributed coin flips, and a backtest's p/b point estimates carry real sampling uncertainty that a single-scenario stress test can only gesture at. + +## Assumptions and honest limitations + +- **Independent, i.i.d. trades.** The simulation treats every trade as an independent Bernoulli draw with fixed p and b. Real crypto trades from correlated strategies (e.g. multiple concurrent BTC/ETH positions moving together in a market-wide drawdown) violate this; correlated losses compound faster than this model's math accounts for, which understates the real risk of any of the higher-fraction strategies (Full/Half Kelly, CPPI at m=3). +- **Known b, no fees/slippage.** `b` (R:R) is treated as a known constant per trade; trading fees, slippage, and spread are not modeled. Real R:R realized on a live book is noisier and typically worse than backtested R:R. +- **A single, fixed estimation-error stress test.** The "p over-estimated by 0.05" world tests one specific magnitude of misestimation, not a distribution over possible estimation errors. It illustrates the *direction* of the Full-Kelly fragility argument, not a calibrated probability of it occurring. +- **No position limits, correlation caps, or per-order/per-day caps.** keel's real guards (order caps, day caps, portfolio-level exposure limits) are not modeled here; they are additional risk controls that a real deployment would layer on top of whatever risk_pct is chosen, and they change the practical consequences of raising risk_pct. +- **Float money, not Decimal.** This sim uses `float` for bankroll math for simplicity; keel's real sizing code (`keel/execution/sizing.py`) is Decimal-only by design, because money should never touch float in the production path. That distinction does not change the qualitative conclusions here but is worth flagging. +- **This is not a recommendation to change keel's risk_pct.** The result is a mathematical observation about the growth/safety tradeoff at different Kelly fractions, not a specific proposed new value; any change to keel's actual risk_pct would need its own review against keel's real guard rails, correlation across live positions, and backtest confidence -- none of which this script attempts to quantify.