From a43fc49c269aacfe522a342efd2021cda40cb5e0 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Thu, 23 Jul 2026 00:27:23 -0400 Subject: [PATCH] =?UTF-8?q?docs(kb):=20explore=20source-84=20leads=20?= =?UTF-8?q?=E2=80=94=20drawdown=20taper=20(kept)=20+=20Merton=20=CE=B3=20(?= =?UTF-8?q?promoted)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measures the two candidate leads flagged in KB source-84 §84.16, "settled by measurement" style. New stdlib-only study explore_leads.py (+18 tests, 56 total in the dir) writing reports/2026-07-23-drawdown-taper-and-merton-exploration.md. - §84.4 dynamic drawdown taper → KEEP as ceiling/diagnostic (not built): barely engages on keel's 1% base; its value is unlocking a HIGHER base — tapered quarter-Kelly at D<20% (below keel's hard breaker) dominates both flat-1% on growth (4.27x vs 2.08x) and untapered quarter-Kelly on safety (hard-breaker trips 99.8%→0%). Strictly conditional on D --- .../analysis/bankroll_sizing/explore_leads.py | 1050 +++++++++++++++++ .../bankroll_sizing/test_explore_leads.py | 152 +++ .../trading-knowledge-base/README.md | 2 +- .../sources/source-84.md | 41 +- ...3-drawdown-taper-and-merton-exploration.md | 215 ++++ 5 files changed, 1452 insertions(+), 8 deletions(-) create mode 100644 docs/superpowers/analysis/bankroll_sizing/explore_leads.py create mode 100644 docs/superpowers/analysis/bankroll_sizing/test_explore_leads.py create mode 100644 docs/superpowers/reports/2026-07-23-drawdown-taper-and-merton-exploration.md diff --git a/docs/superpowers/analysis/bankroll_sizing/explore_leads.py b/docs/superpowers/analysis/bankroll_sizing/explore_leads.py new file mode 100644 index 00000000..36340b69 --- /dev/null +++ b/docs/superpowers/analysis/bankroll_sizing/explore_leads.py @@ -0,0 +1,1050 @@ +#!/usr/bin/env python3 +"""Exploration of two candidate bankroll-sizing leads from KB source-84 (`keeks` family). + +Educational / halal framing +---------------------------- +`keel` is a halal (riba-free) spot-crypto, long-only, no-leverage trading agent. It sizes every +trade with fixed-fractional risk sizing (`keel/execution/sizing.py::size`): risk a constant +`risk_pct` of equity per trade, config default `risk_pct = 0.01` (1%). Promotion floor: +win_rate >= 0.55, R:R (`b`) >= 1.5. As with `simulate.py` next to this script, everything here is +a stdlib-only Monte Carlo study of capital-allocation MATH (the Kelly family and its continuous +cousin, the Merton share) -- not a study of gambling, and not wired into `keel`'s execution path. +Nothing here trades real money or involves interest (riba). + +This script does NOT modify `simulate.py` or its report; it is a separate, self-contained +follow-up that reuses only `sizing_strategies.py`'s pure formulas (`kelly_fraction`, +`fractional_kelly`, `merton_fraction`, `fixed_fraction`) and adds its own small set of helpers. + +Two candidate leads under test (KB source-84 §84.16) +------------------------------------------------------- +1. **Dynamic drawdown taper** (§84.4, blog form): `f_eff = (1 - d/D) * f_base`, where `d` is the + account's current drawdown from its equity peak and `D` is a taper ceiling -- risk tapers + linearly to zero as `d` approaches `D`, continuously, *before* keel's existing hard + drawdown-breaker (rail 11) would halt trading outright. The hypothesis under test: on keel's + tiny 1% base fraction, drawdown rarely gets deep enough for the taper to matter, so the taper's + real value is not "protecting the 1% base" but "letting you safely run a HIGHER base fraction." +2. **Merton share / CRRA sizing** (§84.6): `f = mu / (gamma * sigma^2)`, the continuous-time + analogue of Kelly for an investor with constant relative risk aversion `gamma`. `gamma = 1` + approximately recovers full Kelly; this is explored as a principled, defensible way to express + "how much sub-Kelly" instead of an ad-hoc lambda multiplier. + +Trade model (same convention as `simulate.py`) +------------------------------------------------ +Each trade wins with probability `p`, paying `+b * (f * bankroll)`; otherwise it loses +`f * bankroll`. `f` is recomputed fresh from running state (`bankroll`, `peak`, `initial`) before +every trade. Bankroll cannot go negative; a path is ruined and stops once bankroll falls to or +below $1. Two edge profiles are used throughout: A = keel's promotion floor (p=0.55, b=1.5) and +B = a stronger edge (p=0.58, b=2.0). Two worlds per experiment: "p correct" (the realized win rate +matches the assumed sizing input) and "p over-estimated by 0.05" (sizing assumes the stated p, but +the true realized win rate is 5 points lower) -- an estimation-error stress test. Unless noted, +500 independent seeded paths of 200 trades each, starting from $1,000. + +Determinism +------------ +Every path uses `random.Random(seed)` with an explicit integer seed; strategies compared within +the same world/profile share the same seed per path index (common random numbers), so any +difference between sizing rules reflects sizing, not differing luck. + +Run: `python explore_leads.py` (or from the repo root: +`python docs/superpowers/analysis/bankroll_sizing/explore_leads.py`). Writes the markdown report +to `docs/superpowers/reports/2026-07-23-drawdown-taper-and-merton-exploration.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 + fixed_fraction, + fractional_kelly, + kelly_fraction, + merton_fraction, +) + +REPO_ROOT = HERE.parents[3] # docs/superpowers/analysis/bankroll_sizing -> repo root +REPORT_PATH = ( + REPO_ROOT + / "docs" + / "superpowers" + / "reports" + / "2026-07-23-drawdown-taper-and-merton-exploration.md" +) + +RUIN_THRESHOLD = 1.0 # bankroll <= $1 counts as ruin; path stops (can't go negative) +HARD_BREAKER_DD = 0.20 # keel's hard account-drawdown breaker (rail 11): halt at 20% DD + +FractionFn = Callable[[dict], float] + +# Two edge profiles shared by both experiments: A sits exactly at keel's promotion floor +# (win_rate >= 0.55, R:R >= 1.5); B is a stronger, more comfortably-above-floor edge. +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}, +} + + +# --------------------------------------------------------------------------------------------- +# New helpers for this exploration (not added to sizing_strategies.py, which is reused as-is) +# --------------------------------------------------------------------------------------------- + + +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 taper_fraction(f_base: float, current_dd: float, taper_ceiling: float) -> float: + """Dynamic drawdown taper (KB §84.4, blog form): scale an arbitrary base risk fraction + `f_base` down to zero as the account's current drawdown `current_dd` (from its equity peak) + approaches a taper ceiling `taper_ceiling` ("D"): + + f_eff = clamp(1 - current_dd / taper_ceiling, 0, 1) * f_base + + Unlike `sizing_strategies.drawdown_adjusted_kelly` (which always tapers the *Kelly* fraction + recomputed from `p`/`b`), this wraps an arbitrary base fraction -- including keel's flat 1%, + which has no `p`/`b` dependence at all -- so it can be applied to any constant-risk sizing + rule. At `current_dd = 0` this returns exactly `f_base`; at `current_dd >= taper_ceiling` it + returns exactly 0; in between it decays linearly. + + Raises `ValueError` if `taper_ceiling <= 0`, `current_dd < 0`, or `f_base < 0`. Result is + clamped to `[0, 1]`. + """ + if taper_ceiling <= 0.0: + raise ValueError(f"taper_fraction: taper_ceiling must be > 0, got {taper_ceiling}") + if current_dd < 0.0: + raise ValueError(f"taper_fraction: current_dd must be >= 0, got {current_dd}") + if f_base < 0.0: + raise ValueError(f"taper_fraction: f_base must be >= 0, got {f_base}") + + if current_dd >= taper_ceiling: + return 0.0 + + scale = _clamp(1.0 - current_dd / taper_ceiling) + return _clamp(scale * f_base) + + +def compute_mu_sigma2(p: float, b: float) -> tuple[float, float]: + """Per-unit-risked mean and variance of a binary trade outcome: +b w.p. p, -1 w.p. (1-p). + + mu = p*b - (1-p) + sigma2 = p*b^2 + (1-p)*1 - mu^2 (Var[X] = E[X^2] - E[X]^2) + + These are the `exp_return`/`variance` inputs `merton_fraction` expects, derived from the same + `p`/`b` the Kelly formulas use, so the Kelly and Merton sizers are being fed a consistent + description of the same trade. + + Raises `ValueError` if `p` is not in `[0, 1]` or `b <= 0`. + """ + if not (0.0 <= p <= 1.0): + raise ValueError(f"compute_mu_sigma2: p must be in [0, 1], got {p}") + if b <= 0.0: + raise ValueError(f"compute_mu_sigma2: b must be > 0, got {b}") + + q = 1.0 - p + mu = p * b - q + sigma2 = p * (b**2) + q * 1.0 - mu**2 + return mu, sigma2 + + +def solve_implied_gamma(mu: float, sigma2: float, target_f: float) -> float: + """Solve for the CRRA risk-aversion `gamma` such that `merton_fraction(mu, sigma2, gamma)` + equals `target_f` -- i.e. "what risk-aversion would a Merton-share investor need to have to + end up sizing at exactly this fraction?" + + `merton_fraction` is `mu / (gamma * sigma2)`, which is monotonically decreasing in `gamma` and + exactly invertible (no iterative search needed): + + gamma = mu / (sigma2 * target_f) + + Raises `ValueError` if `mu <= 0` (no edge -- no finite positive `gamma` makes an + edge-less Merton fraction hit a positive target), `sigma2 <= 0`, or `target_f <= 0`. + """ + if mu <= 0.0: + raise ValueError(f"solve_implied_gamma: mu must be > 0 (no edge), got {mu}") + if sigma2 <= 0.0: + raise ValueError(f"solve_implied_gamma: sigma2 must be > 0, got {sigma2}") + if target_f <= 0.0: + raise ValueError(f"solve_implied_gamma: target_f must be > 0, got {target_f}") + + return mu / (sigma2 * target_f) + + +# --------------------------------------------------------------------------------------------- +# Strategy factories: stateless callable(state) -> risk fraction in [0, 1]. `state` carries the +# running per-path values: bankroll, peak (equity high-water mark), initial (starting bankroll). +# --------------------------------------------------------------------------------------------- + + +def strategy_fixed(f: float) -> FractionFn: + fraction = fixed_fraction(f) + return lambda state: fraction + + +def strategy_taper(f_base: float, taper_ceiling: 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 taper_fraction(f_base, current_dd, taper_ceiling) + + return fn + + +# --------------------------------------------------------------------------------------------- +# Core path simulator (adds an optional hard drawdown-breaker halt on top of simulate.py's model) +# --------------------------------------------------------------------------------------------- + + +def simulate_path( + fraction_fn: FractionFn, + n_bets: int, + p: float, + b: float, + seed: int, + initial: float, + hard_breaker_dd: float | None = None, +) -> 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. The + path stops early if bankroll drops to or below `RUIN_THRESHOLD` (ruin is recorded). + + If `hard_breaker_dd` is given, it models keel's hard account-drawdown breaker (rail 11): once + the path's current drawdown from peak reaches or exceeds `hard_breaker_dd`, the path STOPS + PLACING TRADES for the remainder of the sequence (bankroll is simply frozen at that level) -- + it does not resume even if drawdown would otherwise have started to recover, mirroring a hard + halt rather than a taper. `breaker_tripped` records whether this ever happened on the path. + + Returns a dict: terminal bankroll, the path's max drawdown from its own running peak, whether + it was ruined, and whether the hard breaker ever tripped. + """ + rng = random.Random(seed) + bankroll = initial + peak = initial + max_dd = 0.0 + ruined = False + breaker_tripped = False + + for _ in range(n_bets): + if bankroll <= RUIN_THRESHOLD: + ruined = True + break + if breaker_tripped: + break # halted by the hard breaker: no further trades this path + + 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 + if hard_breaker_dd is not None and dd >= hard_breaker_dd: + breaker_tripped = True + + return { + "terminal": bankroll, + "max_dd": max_dd, + "ruined": ruined, + "breaker_tripped": breaker_tripped, + } + + +def run_paths( + strategies: dict[str, FractionFn], + n_paths: int, + n_bets: int, + p: float, + b: float, + base_seed: int, + initial: float, + hard_breaker_dd: float | None = None, +) -> 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": [], "breaker_tripped": []} + 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, hard_breaker_dd) + results[name]["terminal"].append(r["terminal"]) + results[name]["max_dd"].append(r["max_dd"]) + results[name]["ruined"].append(r["ruined"]) + results[name]["breaker_tripped"].append(r["breaker_tripped"]) + return results + + +def summarize(results: dict[str, dict[str, list]], initial: float) -> 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"] + tripped = r["breaker_tripped"] + median_terminal = statistics.median(terminal) + median_max_dd = statistics.median(max_dd) + median_multiple = median_terminal / initial + risk_adjusted = median_multiple / median_max_dd if median_max_dd > 0 else float("inf") + summary[name] = { + "median_terminal": median_terminal, + "median_multiple": median_multiple, + "mean_terminal": statistics.mean(terminal), + "median_max_dd": median_max_dd, + "worst_max_dd": max(max_dd), + "ruin_rate": sum(ruined) / len(ruined), + "breaker_trip_rate": sum(tripped) / len(tripped), + "risk_adjusted": risk_adjusted, + } + return summary + + +# --------------------------------------------------------------------------------------------- +# Experiment 3: dynamic drawdown taper -- does it let a higher base fraction run more safely? +# --------------------------------------------------------------------------------------------- + +EXP3_N_BETS = 200 +EXP3_N_PATHS = 500 +EXP3_INITIAL = 1000.0 +EXP3_P_ERROR = 0.05 +EXP3_TAPER_CEILINGS = [0.15, 0.25, 0.35] +EXP3_KEEL_F = 0.01 +EXP3_BASE_SEED = 30_000 + + +def base_fractions_for(p: float, b: float) -> dict[str, float]: + return { + "keel-1%": EXP3_KEEL_F, + "Quarter-Kelly": fractional_kelly(p, b, 0.25), + "Half-Kelly": fractional_kelly(p, b, 0.5), + } + + +def exp3_combo_name(base_name: str, taper_ceiling: float | None) -> str: + if taper_ceiling is None: + return f"{base_name} / no taper" + return f"{base_name} / taper D={taper_ceiling:.2f}" + + +def experiment_3() -> dict: + """Returns, per profile, per world, the summary stats for every (base fraction x taper + ceiling) combination, all run under keel's hard drawdown breaker at 20%.""" + out: dict = {} + for profile_idx, (profile_name, params) in enumerate(PROFILES.items()): + assumed_p, b = params["p"], params["b"] + bases = base_fractions_for(assumed_p, b) + + strategies: dict[str, FractionFn] = {} + combo_order: list[str] = [] + for base_name, f_base in bases.items(): + name = exp3_combo_name(base_name, None) + strategies[name] = strategy_fixed(f_base) + combo_order.append(name) + for ceiling in EXP3_TAPER_CEILINGS: + name = exp3_combo_name(base_name, ceiling) + strategies[name] = strategy_taper(f_base, ceiling) + combo_order.append(name) + + worlds = { + "p correct": assumed_p, + f"p over-estimated by {EXP3_P_ERROR:.2f}": assumed_p - EXP3_P_ERROR, + } + profile_out: dict = {"bases": bases, "combo_order": combo_order, "worlds": {}} + for world_idx, (world_name, true_p) in enumerate(worlds.items()): + seed = EXP3_BASE_SEED + profile_idx * 1_000_000 + world_idx * 500_000 + results = run_paths( + strategies, + EXP3_N_PATHS, + EXP3_N_BETS, + true_p, + b, + seed, + EXP3_INITIAL, + hard_breaker_dd=HARD_BREAKER_DD, + ) + profile_out["worlds"][world_name] = summarize(results, EXP3_INITIAL) + out[profile_name] = profile_out + return out + + +def taper_dominance_summary(exp3_results: dict) -> dict: + """For every (base fraction, taper ceiling) pair, check across ALL profile/world combos + whether the tapered base "dominates" -- beats flat-1% on growth (median terminal multiple) + AND beats its own untapered version on safety (median max DD and hard-breaker trip rate both + no worse). Returns per (base, D): the count of profile/world combos (out of the total tested) + where growth-dominance holds, where safety-dominance holds, and where both hold together + ("full dominance"), plus the underlying per-combo rows for citing concrete numbers. + """ + bases = ["Quarter-Kelly", "Half-Kelly"] + out: dict = {base: {} for base in bases} + combos: list[tuple[str, str]] = [] + for profile_name, profile_out in exp3_results.items(): + for world_name in profile_out["worlds"]: + combos.append((profile_name, world_name)) + + for base in bases: + for ceiling in EXP3_TAPER_CEILINGS: + rows = [] + growth_hits = 0 + safety_hits = 0 + full_hits = 0 + for profile_name, world_name in combos: + summary = exp3_results[profile_name]["worlds"][world_name] + flat1 = summary["keel-1% / no taper"] + untapered = summary[f"{base} / no taper"] + tapered = summary[exp3_combo_name(base, ceiling)] + growth_dom = tapered["median_multiple"] >= flat1["median_multiple"] + safety_dom = ( + tapered["median_max_dd"] <= untapered["median_max_dd"] + and tapered["breaker_trip_rate"] <= untapered["breaker_trip_rate"] + ) + growth_hits += int(growth_dom) + safety_hits += int(safety_dom) + full_hits += int(growth_dom and safety_dom) + rows.append( + { + "profile": profile_name, + "world": world_name, + "growth_dom": growth_dom, + "safety_dom": safety_dom, + "flat1": flat1, + "untapered": untapered, + "tapered": tapered, + } + ) + out[base][ceiling] = { + "n_combos": len(combos), + "growth_hits": growth_hits, + "safety_hits": safety_hits, + "full_hits": full_hits, + "rows": rows, + } + return out + + +# --------------------------------------------------------------------------------------------- +# Experiment 4: Merton gamma sizing -- implied risk-aversion, fixed-gamma cross-profile behavior, +# and the fractional-Kelly equivalence it implies. +# --------------------------------------------------------------------------------------------- + +EXP4_N_BETS = 200 +EXP4_N_PATHS = 500 +EXP4_INITIAL = 1000.0 +EXP4_P_ERROR = 0.05 +EXP4_KEEL_F = 0.01 +EXP4_TEXTBOOK_GAMMA = 2.0 +EXP4_BASE_SEED = 40_000 + + +def experiment_4() -> dict: + # (a) implied gamma per profile: solve merton_fraction(mu, sigma2, gamma) = keel's actual 1%. + implied: dict[str, dict[str, float]] = {} + for profile_name, params in PROFILES.items(): + p, b = params["p"], params["b"] + mu, sigma2 = compute_mu_sigma2(p, b) + gamma = solve_implied_gamma(mu, sigma2, EXP4_KEEL_F) + implied[profile_name] = {"mu": mu, "sigma2": sigma2, "gamma": gamma} + + gamma_a_implied = implied["A (floor edge: p=0.55, b=1.5)"]["gamma"] + gamma_choices = { + "gamma=A-implied": gamma_a_implied, + "gamma=2 (textbook)": EXP4_TEXTBOOK_GAMMA, + } + + # (b) fixed-gamma sizing across profiles, vs flat-1% and Quarter-Kelly, in both worlds. + cross: dict[str, dict] = {} + for profile_idx, (profile_name, params) in enumerate(PROFILES.items()): + assumed_p, b = params["p"], params["b"] + mu, sigma2 = compute_mu_sigma2(assumed_p, b) + levels: dict[str, float] = { + "keel-1%": EXP4_KEEL_F, + "Quarter-Kelly": fractional_kelly(assumed_p, b, 0.25), + } + for gamma_name, gamma in gamma_choices.items(): + levels[f"Merton ({gamma_name})"] = merton_fraction(mu, sigma2, gamma) + + worlds = { + "p correct": assumed_p, + f"p over-estimated by {EXP4_P_ERROR:.2f}": assumed_p - EXP4_P_ERROR, + } + profile_out: dict = {"levels": levels, "mu": mu, "sigma2": sigma2, "worlds": {}} + for world_idx, (world_name, true_p) in enumerate(worlds.items()): + seed = EXP4_BASE_SEED + profile_idx * 1_000_000 + world_idx * 500_000 + strategies = {name: strategy_fixed(f) for name, f in levels.items()} + results = run_paths( + strategies, EXP4_N_PATHS, EXP4_N_BETS, true_p, b, seed, EXP4_INITIAL + ) + profile_out["worlds"][world_name] = summarize(results, EXP4_INITIAL) + cross[profile_name] = profile_out + + # (c) effective lambda = f_merton / f_kelly at each profile, for each fixed gamma. + lambdas: dict[str, dict[str, dict[str, float]]] = {} + for profile_name, params in PROFILES.items(): + p, b = params["p"], params["b"] + full_kelly = kelly_fraction(p, b) + mu, sigma2 = compute_mu_sigma2(p, b) + lambdas[profile_name] = {} + for gamma_name, gamma in gamma_choices.items(): + f_merton = merton_fraction(mu, sigma2, gamma) + lam = f_merton / full_kelly if full_kelly > 0 else float("nan") + lambdas[profile_name][gamma_name] = { + "f_merton": f_merton, + "full_kelly": full_kelly, + "lambda": lam, + } + + return {"implied": implied, "gamma_choices": gamma_choices, "cross": cross, "lambdas": lambdas} + + +# --------------------------------------------------------------------------------------------- +# 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 fmt_ratio(x: float) -> str: + if x == float("inf"): + return "inf" + return f"{x:.2f}" + + +def exp3_table(profile_out: dict, world_name: str) -> str: + header = ( + "| Base x taper | Median terminal multiple | Median max DD | Worst max DD | " + "Ruin rate | Breaker trip rate | Risk-adj (mult/DD) |\n" + "|---|---|---|---|---|---|---|\n" + ) + summary = profile_out["worlds"][world_name] + rows = [] + for name in profile_out["combo_order"]: + s = summary[name] + rows.append( + f"| {name} | {s['median_multiple']:.3f}x | {fmt_pct(s['median_max_dd'])} | " + f"{fmt_pct(s['worst_max_dd'])} | {fmt_pct(s['ruin_rate'])} | " + f"{fmt_pct(s['breaker_trip_rate'])} | {fmt_ratio(s['risk_adjusted'])} |" + ) + return header + "\n".join(rows) + + +def exp4_table(profile_out: dict, level_order: list[str], world_name: str) -> str: + header = ( + "| Sizing level | Risk fraction | Median terminal multiple | Median max DD | " + "Worst max DD | Ruin rate |\n" + "|---|---|---|---|---|---|\n" + ) + levels = profile_out["levels"] + summary = profile_out["worlds"][world_name] + rows = [] + for name in level_order: + s = summary[name] + rows.append( + f"| {name} | {fmt_pct(levels[name])} | {s['median_multiple']:.3f}x | " + f"{fmt_pct(s['median_max_dd'])} | {fmt_pct(s['worst_max_dd'])} | " + f"{fmt_pct(s['ruin_rate'])} |" + ) + return header + "\n".join(rows) + + +def build_report(exp3_results: dict, exp4_results: dict) -> str: + profile_names = list(PROFILES.keys()) + profile_a_name, profile_b_name = profile_names[0], profile_names[1] + + lines: list[str] = [] + lines.append( + "# Drawdown Taper and Merton-Gamma Exploration: Two Candidate Leads from KB Source-84" + ) + lines.append("") + + # --- Purpose and framing --------------------------------------------------------------- + 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 sizes every trade with fixed-fractional risk sizing " + "(`keel/execution/sizing.py::size`): risk a constant `risk_pct` of equity per trade, " + "config default `risk_pct = 0.01` (1%). Promotion floor: win_rate >= 0.55, R:R >= 1.5." + ) + lines.append("") + lines.append( + "This report is a follow-up, stdlib-only Monte Carlo study of two SPECIFIC candidate " + "leads flagged in KB source-84 §84.16, exploring the Kelly family and its continuous " + "cousin (the Merton share) **as mathematics of capital allocation** -- not as gambling " + "advice, and not wired into `keel`'s execution path. Nothing here trades real money or " + "involves interest (riba). It reuses `sizing_strategies.py`'s pure formulas " + "(`kelly_fraction`, `fractional_kelly`, `merton_fraction`, `fixed_fraction`) and does not " + "modify `simulate.py` or its existing report." + ) + lines.append("") + + # --- The two leads ----------------------------------------------------------------------- + lines.append("## The two leads under test") + lines.append("") + lines.append( + "1. **Dynamic drawdown taper (KB §84.4, blog form):** `f_eff = (1 - d/D) * f_base`, " + "`d` = current account drawdown from peak, `D` = a taper ceiling -- risk tapers linearly " + "to zero as `d -> D`, continuously and *before* keel's existing hard drawdown breaker " + "(rail 11, halts at 20% account DD) would otherwise stop trading outright. Hypothesis " + "under test: on keel's tiny 1% base fraction the taper almost never engages (1% rarely " + "draws an account down far), so the taper's real value is not protecting the 1% base -- " + "it is letting a HIGHER base fraction run more safely." + ) + lines.append( + "2. **Merton share / CRRA sizing (KB §84.6):** `f = mu / (gamma * sigma^2)`, the " + "continuous-time analogue of Kelly for an investor with constant relative risk aversion " + "`gamma` (`gamma = 1` approximately recovers full Kelly; higher `gamma` sizes smaller). " + "Explored as a principled, defensible way to express \"how sub-Kelly\" instead of an " + "ad-hoc fractional-Kelly `lambda`." + ) + lines.append("") + + # --- Method ---------------------------------------------------------------------------- + lines.append("## Method") + lines.append("") + lines.append( + "Deterministic (seeded) Monte Carlo experiments, implemented in `explore_leads.py` next " + "to this report, stdlib-only (`random`, `statistics`). Every path uses " + "`random.Random(seed)`; strategies compared within the same world/profile share seeds per " + "path index (common random numbers). A trade wins with probability `p`, paying " + "`+b * (f * bankroll)`, else loses `f * bankroll`, `f` recomputed fresh from running " + "state before every trade. A path is ruined and stopped once bankroll falls to or below " + "$1. Two edge profiles: **A** = keel's promotion floor (p=0.55, b=1.5); **B** = a " + "stronger edge (p=0.58, b=2.0). Two worlds per experiment: **p correct** (realized win " + "rate matches the sizing assumption) and **p over-estimated by 0.05** (sizing assumes the " + "stated p, the true realized win rate is 5 points lower). Unless noted, 500 seeded paths " + "of 200 trades each, starting from $1,000." + ) + lines.append("") + + # --- Experiment 3 ------------------------------------------------------------------------ + lines.append("## Experiment 3: dynamic drawdown taper") + lines.append("") + lines.append( + "For each profile, three base fractions are tested: keel-1% (0.01, flat), Quarter-Kelly, " + "and Half-Kelly (both computed from that profile's own p/b). Each base is run (i) " + "untapered and (ii) tapered at ceilings D in {0.15, 0.25, 0.35}. EVERY combination also " + "runs under keel's hard drawdown breaker, modeled as a hard halt (no further trades for " + "the rest of the sequence) once a path's current drawdown from peak reaches 20% -- " + "mirroring rail 11. \"Risk-adj\" is a crude ratio: median terminal multiple / median max " + "DD (higher is better: more growth per unit of typical pain)." + ) + lines.append("") + for profile_name in profile_names: + profile_out = exp3_results[profile_name] + bases = profile_out["bases"] + lines.append(f"### Profile {profile_name}") + lines.append("") + lines.append( + f"Base fractions: keel-1%={fmt_pct(bases['keel-1%'])}, " + f"Quarter-Kelly={fmt_pct(bases['Quarter-Kelly'])}, " + f"Half-Kelly={fmt_pct(bases['Half-Kelly'])}." + ) + lines.append("") + for world_name in profile_out["worlds"]: + lines.append(f"**World: {world_name}**") + lines.append("") + lines.append(exp3_table(profile_out, world_name)) + lines.append("") + + # Headline comparison pulled directly from the computed data (no hand-transcription drift): + # systematically checks, across ALL 4 profile x world combos, whether each (base, D) beats + # flat-1% on growth AND beats its own untapered version on safety. + dominance = taper_dominance_summary(exp3_results) + pa_correct = exp3_results[profile_a_name]["worlds"]["p correct"] + flat1_row = pa_correct["keel-1% / no taper"] + keel_taper_row = pa_correct["keel-1% / taper D=0.15"] + + lines.append("### Headline read: does taper-on-Quarter-Kelly dominate?") + lines.append("") + lines.append( + "Checked systematically across all 4 profile x world combos (A/B x \"p correct\"/" + "\"p over-estimated\"): for each (base fraction, taper ceiling D), does the tapered " + "version reach a median terminal multiple >= flat-1%'s (growth-dominates), AND does it " + "reach a median max DD and hard-breaker trip rate both <= its own untapered version's " + "(safety-dominates)?" + ) + lines.append("") + lines.append( + "| Base | Taper D | Growth-dominates flat-1% | Safety-dominates untapered | " + "Full dominance (both) |" + ) + lines.append("|---|---|---|---|---|") + for base in ["Quarter-Kelly", "Half-Kelly"]: + for ceiling in EXP3_TAPER_CEILINGS: + d = dominance[base][ceiling] + n = d["n_combos"] + lines.append( + f"| {base} | {ceiling:.2f} | {d['growth_hits']}/{n} combos | " + f"{d['safety_hits']}/{n} combos | {d['full_hits']}/{n} combos |" + ) + lines.append("") + + qk_d15 = dominance["Quarter-Kelly"][0.15] + qk_d15_a_correct = next( + r for r in qk_d15["rows"] if r["profile"] == profile_a_name and r["world"] == "p correct" + ) + qk_flat_row = qk_d15_a_correct["untapered"] + qk_taper_row = qk_d15_a_correct["tapered"] + lines.append( + f"**Full dominance (more growth than flat-1% AND less drawdown/fewer breaker trips than " + f"untapered) holds in {qk_d15['full_hits']}/{qk_d15['n_combos']} combos for " + f"Quarter-Kelly tapered at D=0.15** -- the one ceiling tested that sits BELOW keel's own " + f"20% hard-breaker threshold. At D=0.25 and D=0.35 (ceilings ABOVE the hard breaker), " + f"full dominance drops to {dominance['Quarter-Kelly'][0.25]['full_hits']}/" + f"{dominance['Quarter-Kelly'][0.25]['n_combos']} and " + f"{dominance['Quarter-Kelly'][0.35]['full_hits']}/" + f"{dominance['Quarter-Kelly'][0.35]['n_combos']} combos respectively -- safety-dominance " + "still holds almost everywhere (the taper reliably shrinks drawdown and breaker trips " + "versus untapered, regardless of D), but growth-dominance over flat-1% mostly fails, " + "because once D exceeds the hard-breaker threshold the taper no longer prevents the " + "breaker from tripping -- and a tripped, frozen bankroll forfeits the same growth " + "untapered Quarter-Kelly forfeits. Concretely, profile A / \"p correct\": flat-1% reaches " + f"{flat1_row['median_multiple']:.3f}x; untapered Quarter-Kelly reaches " + f"{qk_flat_row['median_multiple']:.3f}x but trips the breaker on " + f"{fmt_pct(qk_flat_row['breaker_trip_rate'])} of paths (median max DD " + f"{fmt_pct(qk_flat_row['median_max_dd'])}); Quarter-Kelly tapered at D=0.15 reaches " + f"{qk_taper_row['median_multiple']:.3f}x with median max DD " + f"{fmt_pct(qk_taper_row['median_max_dd'])} and a " + f"{fmt_pct(qk_taper_row['breaker_trip_rate'])} breaker trip rate." + ) + lines.append("") + lines.append( + f"**Does the taper help AT ALL on the 1% base?** Barely, and the hypothesis holds: on " + f"keel-1%, drawdown almost never reaches even the tightest taper ceiling (D=0.15) -- " + f"untapered keel-1% breaker-trips on {fmt_pct(flat1_row['breaker_trip_rate'])} of paths " + f"(profile A, \"p correct\"), and tapering at D=0.15 changes that to " + f"{fmt_pct(keel_taper_row['breaker_trip_rate'])} while giving up some growth " + f"({keel_taper_row['median_multiple']:.3f}x vs {flat1_row['median_multiple']:.3f}x, " + "because the taper starts shaving size any time drawdown is nonzero, not just near the " + "ceiling). **Half-Kelly never achieves growth-dominance regardless of taper ceiling** " + f"({dominance['Half-Kelly'][0.15]['growth_hits']}/" + f"{dominance['Half-Kelly'][0.15]['n_combos']} combos at D=0.15): its base fraction is " + "simply too large -- a single adverse trade can jump drawdown past even a tight taper " + "ceiling in one or two trades, so the taper either zeroes risk out too early to compound " + "meaningfully, or fails to prevent the breaker trip anyway." + ) + lines.append("") + + # --- Experiment 4 ------------------------------------------------------------------------ + lines.append("## Experiment 4: Merton gamma sizing") + lines.append("") + implied = exp4_results["implied"] + gamma_choices = exp4_results["gamma_choices"] + gamma_a = implied[profile_a_name]["gamma"] + gamma_b = implied[profile_b_name]["gamma"] + + lines.append("### (a) Implied risk-aversion gamma at keel's actual 1%") + lines.append("") + lines.append( + "Solving `merton_fraction(mu, sigma2, gamma) = 0.01` for `gamma` at each profile's own " + "mu/sigma2 (mu = p*b - (1-p), sigma2 = p*b^2 + (1-p) - mu^2):" + ) + lines.append("") + lines.append("| Profile | mu | sigma^2 | Implied gamma | x more risk-averse than gamma=1 |") + lines.append("|---|---|---|---|---|") + for profile_name in profile_names: + row = implied[profile_name] + lines.append( + f"| {profile_name} | {row['mu']:.4f} | {row['sigma2']:.4f} | {row['gamma']:.2f} | " + f"{row['gamma']:.1f}x |" + ) + lines.append("") + lines.append( + f"keel's implied risk-aversion is roughly **{gamma_a:.1f}x** the Kelly-equivalent " + f"(gamma=1) investor at profile A, and roughly **{gamma_b:.1f}x** at profile B. Full " + "Kelly is approximately gamma=1; keel's flat 1% is, in this framing, the choice of an " + "extremely risk-averse Merton investor -- far past the textbook gamma~2 estimate of " + "typical human risk aversion." + ) + lines.append("") + + lines.append("### (b) Fixed-gamma sizing across profiles") + lines.append("") + lines.append( + "One `gamma` is fixed and applied to BOTH profiles' own mu/sigma2, compared against " + "flat-1% and Quarter-Kelly: `gamma=A-implied` " + f"({gamma_choices['gamma=A-implied']:.2f}, i.e. the gamma solved in (a) at profile A) and " + f"`gamma=2` (textbook human-risk-aversion estimate)." + ) + lines.append("") + level_order = [ + "keel-1%", + "Quarter-Kelly", + "Merton (gamma=A-implied)", + "Merton (gamma=2 (textbook))", + ] + for profile_name in profile_names: + profile_out = exp4_results["cross"][profile_name] + lines.append(f"#### Profile {profile_name}") + lines.append("") + levels = profile_out["levels"] + lines.append( + f"Sizing fractions: keel-1%={fmt_pct(levels['keel-1%'])}, " + f"Quarter-Kelly={fmt_pct(levels['Quarter-Kelly'])}, " + f"Merton(gamma=A-implied)={fmt_pct(levels['Merton (gamma=A-implied)'])}, " + f"Merton(gamma=2)={fmt_pct(levels['Merton (gamma=2 (textbook))'])}." + ) + lines.append("") + for world_name in profile_out["worlds"]: + lines.append(f"**World: {world_name}**") + lines.append("") + lines.append(exp4_table(profile_out, level_order, world_name)) + lines.append("") + + fa = exp4_results["cross"][profile_a_name]["levels"]["Merton (gamma=A-implied)"] + fb = exp4_results["cross"][profile_b_name]["levels"]["Merton (gamma=A-implied)"] + gamma_a_implied_val = gamma_choices["gamma=A-implied"] + lines.append( + f"**Cross-profile behavior at a single fixed gamma ({gamma_a_implied_val:.2f}, " + f"profile A's implied gamma):** the SAME gamma sizes profile A at exactly " + f"{fmt_pct(fa)} (by construction) but sizes the stronger, lower-variance profile B at " + f"{fmt_pct(fb)} -- automatically MORE, with no re-tuning. Flat-1% cannot do this: it " + "risks the identical 1% on both the floor edge and the stronger edge, by definition. This " + "is the mechanical demonstration of the KB claim that Merton sizing is edge-aware where " + "flat-fractional sizing is not." + ) + lines.append("") + + lines.append("### (c) Fractional-Kelly equivalence (effective lambda)") + lines.append("") + lines.append( + "Merton-at-a-fixed-gamma is mathematically a form of fractional Kelly: dividing the " + "Merton fraction by that profile's own full-Kelly fraction gives an effective lambda " + "(`lambda = f_merton / f_kelly`) -- \"what fraction of full Kelly is this gamma " + "equivalent to, at this specific edge?\"" + ) + lines.append("") + lines.append("| Profile | gamma | f_merton | Full Kelly | Effective lambda |") + lines.append("|---|---|---|---|---|") + for profile_name in profile_names: + for gamma_name, gamma in gamma_choices.items(): + row = exp4_results["lambdas"][profile_name][gamma_name] + lines.append( + f"| {profile_name} | {gamma_name} ({gamma:.2f}) | {fmt_pct(row['f_merton'])} | " + f"{fmt_pct(row['full_kelly'])} | {row['lambda']:.4f} ({row['lambda'] * 100:.2f}%) |" + ) + lines.append("") + lam_a = exp4_results["lambdas"][profile_a_name]["gamma=A-implied"]["lambda"] + lam_b = exp4_results["lambdas"][profile_b_name]["gamma=A-implied"]["lambda"] + lines.append( + f"At `gamma=A-implied`, the effective lambda is {lam_a * 100:.2f}% of full Kelly at " + f"profile A (matching keel-1%'s own ~4% of full Kelly noted in the prior bankroll-sizing " + f"report) and {lam_b * 100:.2f}% at profile B -- close but not identical, because " + "`merton_fraction` (a mean/variance formula) and `kelly_fraction` (the discrete binary " + "formula) are two different approximations of the same growth-optimal bet size, not " + "algebraically identical. Both worlds tables above show `Merton (gamma=A-implied)` " + "keeping ruin at 0.0% under \"p over-estimated by 0.05\" at both profiles -- degrading " + "gracefully, the same qualitative behavior fractional Kelly showed in the original " + "`simulate.py` study." + ) + lines.append("") + + # --- Verdicts ---------------------------------------------------------------------------- + lines.append("## Verdict per lead") + lines.append("") + + qk_taper_ruin = qk_taper_row["ruin_rate"] + qk_flat_ruin = qk_flat_row["ruin_rate"] + taper_fully_dominates = qk_d15["full_hits"] == qk_d15["n_combos"] + verdict_1 = ( + "PROMOTE to build candidate (with a specific, testable condition)" + if taper_fully_dominates + else "KEEP as ceiling-or-diagnostic only" + ) + lines.append("### Lead 1: dynamic drawdown taper -- VERDICT: " + verdict_1) + lines.append("") + lines.append( + f"Taper-on-Quarter-Kelly at D=0.15 vs flat-1% (growth): " + f"{qk_taper_row['median_multiple']:.3f}x vs {flat1_row['median_multiple']:.3f}x -- more " + f"growth in all {qk_d15['growth_hits']}/{qk_d15['n_combos']} combos tested. " + f"Taper-on-Quarter-Kelly at D=0.15 vs untapered Quarter-Kelly (safety): median max DD " + f"{fmt_pct(qk_taper_row['median_max_dd'])} vs {fmt_pct(qk_flat_row['median_max_dd'])}, " + f"breaker trip rate {fmt_pct(qk_taper_row['breaker_trip_rate'])} vs " + f"{fmt_pct(qk_flat_row['breaker_trip_rate'])}, ruin rate {fmt_pct(qk_taper_ruin)} vs " + f"{fmt_pct(qk_flat_ruin)} -- safer in all " + f"{qk_d15['safety_hits']}/{qk_d15['n_combos']} combos. This full dominance is " + "CONDITIONAL: it holds cleanly only when the taper ceiling D sits below keel's own 20% " + "hard-breaker threshold (D=0.15 here). At D=0.25 or D=0.35 the taper still reliably " + "improves safety over untapered Quarter-Kelly, but usually stops beating flat-1% on " + "growth, because a ceiling above the hard breaker no longer prevents the breaker from " + "tripping. On the keel-1% base itself the taper barely engages (drawdown almost never " + "reaches even D=0.15) -- the hypothesis holds: the taper's real value is in letting a " + "HIGHER base fraction (Quarter-Kelly, not Half-Kelly -- see below) run with materially " + "fewer breaker trips and shallower drawdowns, not in protecting the already-tiny 1% " + "base. Half-Kelly's base fraction is too large for any taper ceiling tested to rescue: " + "it never achieves growth-dominance, taper or no taper, because a single adverse trade " + "can jump drawdown past the taper zone before it has a chance to brake gradually." + ) + lines.append("") + + verdict_2 = "PROMOTE to build candidate" + lines.append("### Lead 2: Merton gamma sizing -- VERDICT: " + verdict_2) + lines.append("") + lines.append( + f"keel's implied risk-aversion is gamma~{gamma_a:.0f} at profile A and gamma~{gamma_b:.0f} " + "at profile B -- both far above the textbook gamma~2 human-risk-aversion estimate and far " + "above the gamma=1 Kelly-equivalent, i.e. keel is a mathematically extreme (not merely " + "\"conservative\") point on this spectrum. A single fixed gamma automatically scales risk " + "up on the stronger/lower-variance edge (B) and down on the floor edge (A) with zero " + "re-tuning, which flat-1% cannot do by construction; and Merton-at-a-fixed-gamma degrades " + "gracefully under the p-over-estimated stress test (ruin stays 0.0% at both profiles), " + "matching fractional Kelly's known robustness. The formula is a legitimate, more " + "principled way to express the SAME sub-Kelly choice keel already makes -- worth adopting " + "as vocabulary/diagnostic (\"keel runs at effectively gamma~24-34\") even without changing " + "risk_pct itself." + ) + lines.append("") + + # --- Assumptions ------------------------------------------------------------------------- + lines.append("## Assumptions and honest limitations") + lines.append("") + lines.append( + "- **Independent, i.i.d. trades.** Every trade is an independent Bernoulli draw with " + "fixed p and b. Real crypto trades from correlated strategies (multiple concurrent " + "positions moving together in a market-wide drawdown) violate this; correlated losses " + "compound faster than this model accounts for, which understates real risk for any " + "higher-fraction sizing (Half-Kelly, high-gamma-inverse Merton at large fractions)." + ) + lines.append( + "- **Known b, no fees/slippage.** `b` is treated as a known constant; trading fees, " + "slippage, and spread are not modeled." + ) + lines.append( + "- **A single, fixed estimation-error magnitude.** The \"p over-estimated by 0.05\" " + "world tests one specific misestimation size, not a distribution over possible errors. " + "It illustrates a direction, not a calibrated probability." + ) + lines.append( + "- **Merton is a mean/variance approximation, not an exact rederivation of Kelly.** " + "`merton_fraction` and `kelly_fraction` are two different formulas for \"how much to " + "risk\"; gamma=1 approximately, not exactly, recovers full Kelly, and the effective-" + "lambda numbers in (c) above reflect that approximation gap, not an algebraic identity." + ) + lines.append( + "- **The hard-breaker model is simplified.** It is modeled as a permanent halt for the " + "rest of a fixed 200-trade sequence once tripped, with no recovery/reset logic and no " + "modeling of the real rail 11 implementation's exact bookkeeping (weekly vs total DD, " + "reset conditions). It exists here only to compare relative trip rates across sizing " + "rules, not to reproduce rail 11 exactly." + ) + lines.append( + "- **This is not a recommendation to change keel's risk_pct.** Both leads are explored " + "as mathematics and vocabulary for reasoning about sizing; any actual change to " + "risk_pct, taper ceilings, or breaker thresholds would need its own review against " + "keel's live guard rails, correlation across real positions, and backtest confidence -- " + "none of which this script attempts to quantify." + ) + lines.append("") + + # --- Numbers to fold into the KB --------------------------------------------------------- + lines.append("## Exact numbers for KB source-84 §84.4 and §84.6") + lines.append("") + lines.append( + f"- **§84.6 (Merton) -- keel's implied gamma:** ~{gamma_a:.1f} at profile A " + f"(p=0.55, b=1.5, the promotion floor), ~{gamma_b:.1f} at profile B (p=0.58, b=2.0) -- " + f"roughly {gamma_a:.0f}x to {gamma_b:.0f}x more risk-averse than the gamma=1 " + "Kelly-equivalent investor, and 12-17x more risk-averse than the textbook gamma=2 human " + "estimate." + ) + lines.append( + f"- **§84.6 -- effective lambda at gamma=A-implied:** {lam_a * 100:.2f}% of full Kelly " + f"at profile A, {lam_b * 100:.2f}% at profile B (both close to keel-1%'s own ~4% of full " + "Kelly figure from the original bankroll-sizing report)." + ) + lines.append( + f"- **§84.4 (taper) -- does taper-on-Quarter-Kelly dominate flat-1% AND untapered " + f"Quarter-Kelly?** Yes, but ONLY when taper ceiling D < keel's 20% hard-breaker " + f"threshold: at D=0.15, full dominance holds in {qk_d15['full_hits']}/{qk_d15['n_combos']} " + f"profile x world combos tested. At D=0.25 it drops to " + f"{dominance['Quarter-Kelly'][0.25]['full_hits']}/{dominance['Quarter-Kelly'][0.25]['n_combos']}" + f", and at D=0.35 to {dominance['Quarter-Kelly'][0.35]['full_hits']}/" + f"{dominance['Quarter-Kelly'][0.35]['n_combos']} -- safety-dominance (lower DD, fewer " + "breaker trips than untapered) persists at every D tested, but growth-dominance over " + "flat-1% requires the ceiling to sit below the hard breaker. Concretely at D=0.15, " + f"profile A / \"p correct\": {qk_taper_row['median_multiple']:.3f}x median terminal " + f"multiple (vs flat-1%'s {flat1_row['median_multiple']:.3f}x), median max DD " + f"{fmt_pct(qk_taper_row['median_max_dd'])} and breaker trip rate " + f"{fmt_pct(qk_taper_row['breaker_trip_rate'])} (vs untapered Quarter-Kelly's " + f"{fmt_pct(qk_flat_row['median_max_dd'])} DD and " + f"{fmt_pct(qk_flat_row['breaker_trip_rate'])} breaker trip rate)." + ) + lines.append( + f"- **§84.4 -- breaker-trip-rate deltas (profile A, \"p correct\"):** keel-1% untapered " + f"{fmt_pct(flat1_row['breaker_trip_rate'])} -> keel-1% taper D=0.15 " + f"{fmt_pct(keel_taper_row['breaker_trip_rate'])} (taper barely engages on the 1% base); " + f"Quarter-Kelly untapered {fmt_pct(qk_flat_row['breaker_trip_rate'])} -> Quarter-Kelly " + f"taper D=0.25 {fmt_pct(qk_taper_row['breaker_trip_rate'])} (taper materially cuts " + "breaker trips on a higher base). Full per-D, per-base breaker-trip figures for both " + "profiles and both worlds are in the Experiment 3 tables above." + ) + lines.append("") + + return "\n".join(lines) + + +def main() -> None: + print("Running Experiment 3 (dynamic drawdown taper)...") + exp3_results = experiment_3() + for profile_name, profile_out in exp3_results.items(): + print(f" Profile {profile_name}") + for world_name, summary in profile_out["worlds"].items(): + print(f" World: {world_name}") + for name in profile_out["combo_order"]: + s = summary[name] + print( + f" {name}: median_mult={s['median_multiple']:.4f}x " + f"median_dd={s['median_max_dd']:.4f} worst_dd={s['worst_max_dd']:.4f} " + f"ruin={s['ruin_rate']:.4f} breaker_trip={s['breaker_trip_rate']:.4f} " + f"risk_adj={s['risk_adjusted']:.4f}" + ) + + print("Running Experiment 4 (Merton gamma sizing)...") + exp4_results = experiment_4() + for profile_name, row in exp4_results["implied"].items(): + print(f" Implied gamma at {profile_name}: {row['gamma']:.4f}") + for profile_name, profile_out in exp4_results["cross"].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_mult={s['median_multiple']:.4f}x " + f"median_dd={s['median_max_dd']:.4f} worst_dd={s['worst_max_dd']:.4f} " + f"ruin={s['ruin_rate']:.4f}" + ) + + report = build_report(exp3_results, exp4_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/test_explore_leads.py b/docs/superpowers/analysis/bankroll_sizing/test_explore_leads.py new file mode 100644 index 00000000..bd5e495a --- /dev/null +++ b/docs/superpowers/analysis/bankroll_sizing/test_explore_leads.py @@ -0,0 +1,152 @@ +"""Unit tests for explore_leads.py (drawdown-taper and Merton-gamma exploration helpers).""" + +from __future__ import annotations + +import pytest +from explore_leads import ( + compute_mu_sigma2, + merton_fraction, + simulate_path, + solve_implied_gamma, + strategy_fixed, + taper_fraction, +) + +# --- taper_fraction ------------------------------------------------------------------------ + + +def test_taper_fraction_no_drawdown_returns_f_base(): + # d=0 -> f_eff = (1 - 0/D) * f_base = f_base exactly. + result = taper_fraction(f_base=0.0625, current_dd=0.0, taper_ceiling=0.25) + assert result == pytest.approx(0.0625) + + +def test_taper_fraction_at_ceiling_returns_zero(): + # d >= D -> f_eff = 0 exactly. + assert taper_fraction(f_base=0.0625, current_dd=0.25, taper_ceiling=0.25) == 0.0 + assert taper_fraction(f_base=0.0625, current_dd=0.30, taper_ceiling=0.25) == 0.0 + + +def test_taper_fraction_linear_decay_midpoint(): + # d = D/2 -> f_eff = 0.5 * f_base. + assert taper_fraction(f_base=0.10, current_dd=0.10, taper_ceiling=0.20) == pytest.approx(0.05) + + +def test_taper_fraction_clamped_to_one(): + # A pathological f_base > 1 must still clamp the output to [0, 1]. + assert taper_fraction(f_base=1.5, current_dd=0.0, taper_ceiling=0.25) == 1.0 + + +def test_taper_fraction_invalid_ceiling_raises(): + with pytest.raises(ValueError): + taper_fraction(f_base=0.05, current_dd=0.0, taper_ceiling=0.0) + with pytest.raises(ValueError): + taper_fraction(f_base=0.05, current_dd=0.0, taper_ceiling=-0.1) + + +def test_taper_fraction_negative_dd_raises(): + with pytest.raises(ValueError): + taper_fraction(f_base=0.05, current_dd=-0.01, taper_ceiling=0.25) + + +def test_taper_fraction_negative_f_base_raises(): + with pytest.raises(ValueError): + taper_fraction(f_base=-0.05, current_dd=0.0, taper_ceiling=0.25) + + +# --- compute_mu_sigma2 --------------------------------------------------------------------- + + +def test_compute_mu_sigma2_known_case(): + # p=0.55, b=1.5 -> mu = 0.55*1.5 - 0.45 = 0.825 - 0.45 = 0.375 + # sigma2 = p*b^2 + (1-p)*1 - mu^2 = 0.55*2.25 + 0.45 - 0.140625 = 1.2375 + 0.45 - 0.140625 + mu, sigma2 = compute_mu_sigma2(0.55, 1.5) + assert mu == pytest.approx(0.375) + assert sigma2 == pytest.approx(1.546875) + + +def test_compute_mu_sigma2_invalid_p_raises(): + with pytest.raises(ValueError): + compute_mu_sigma2(1.5, 1.0) + with pytest.raises(ValueError): + compute_mu_sigma2(-0.1, 1.0) + + +def test_compute_mu_sigma2_invalid_b_raises(): + with pytest.raises(ValueError): + compute_mu_sigma2(0.55, 0.0) + + +# --- solve_implied_gamma ------------------------------------------------------------------- + + +def test_solve_implied_gamma_round_trips(): + mu, sigma2 = compute_mu_sigma2(0.55, 1.5) + target_f = 0.01 + gamma = solve_implied_gamma(mu, sigma2, target_f) + assert merton_fraction(mu, sigma2, gamma) == pytest.approx(target_f) + + +def test_solve_implied_gamma_round_trips_second_profile(): + mu, sigma2 = compute_mu_sigma2(0.58, 2.0) + target_f = 0.01 + gamma = solve_implied_gamma(mu, sigma2, target_f) + assert merton_fraction(mu, sigma2, gamma) == pytest.approx(target_f) + + +def test_solve_implied_gamma_no_edge_raises(): + with pytest.raises(ValueError): + solve_implied_gamma(mu=0.0, sigma2=1.0, target_f=0.01) + with pytest.raises(ValueError): + solve_implied_gamma(mu=-0.1, sigma2=1.0, target_f=0.01) + + +def test_solve_implied_gamma_invalid_sigma2_raises(): + with pytest.raises(ValueError): + solve_implied_gamma(mu=0.1, sigma2=0.0, target_f=0.01) + + +def test_solve_implied_gamma_invalid_target_raises(): + with pytest.raises(ValueError): + solve_implied_gamma(mu=0.1, sigma2=1.0, target_f=0.0) + + +# --- hard-breaker halt logic (simulate_path) ----------------------------------------------- + + +def test_hard_breaker_halts_trading_for_rest_of_path(): + # A large, constant risk fraction with an unfavorable seed should trip the 20% breaker + # early; once tripped, terminal bankroll must stop changing for the remaining trades (i.e. + # simulating fewer bets with the same seed produces the same terminal bankroll once past + # the trip point). + fraction_fn = strategy_fixed(0.5) # aggressive: half of bankroll risked every trade + full = simulate_path( + fraction_fn, n_bets=200, p=0.55, b=1.5, seed=1, initial=1000.0, hard_breaker_dd=0.20 + ) + assert full["breaker_tripped"] is True + + # Re-running with far fewer trades (but long enough to have already tripped) must match the + # full run's terminal bankroll exactly, proving no further trades were placed after the trip. + short = simulate_path( + fraction_fn, n_bets=5, p=0.55, b=1.5, seed=1, initial=1000.0, hard_breaker_dd=0.20 + ) + assert short["breaker_tripped"] is True + assert short["terminal"] == pytest.approx(full["terminal"]) + + +def test_hard_breaker_not_tripped_without_dd_reaching_ceiling(): + # A tiny, safe fraction over a short run should not trip a 20% breaker. + fraction_fn = strategy_fixed(0.01) + result = simulate_path( + fraction_fn, n_bets=20, p=0.55, b=1.5, seed=1, initial=1000.0, hard_breaker_dd=0.20 + ) + assert result["breaker_tripped"] is False + + +def test_no_hard_breaker_when_none(): + # hard_breaker_dd=None (default) must never trip, even for an aggressive fraction. + fraction_fn = strategy_fixed(0.9) + result = simulate_path( + fraction_fn, n_bets=50, p=0.55, b=1.5, seed=1, initial=1000.0, hard_breaker_dd=None + ) + assert result["breaker_tripped"] is False diff --git a/docs/superpowers/references/trading-knowledge-base/README.md b/docs/superpowers/references/trading-knowledge-base/README.md index 3bb76886..21fdbda2 100644 --- a/docs/superpowers/references/trading-knowledge-base/README.md +++ b/docs/superpowers/references/trading-knowledge-base/README.md @@ -136,7 +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) | +| 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. ⚑ **BOTH new leads EXPLORED 2026-07-23** (report `2026-07-23-drawdown-taper-and-merton-exploration.md`, +18 tests): **(a) drawdown taper → KEPT as ceiling/diagnostic** — barely engages on the 1% base; only pays off by unlocking a higher base (tapered ¼-Kelly at D<20% breaker cuts hard-breaker trips **99.8%→0%** and beats flat-1% on growth), so parked until/unless the base is raised off 1% (§58.11/§84.14 say don't); **(b) Merton → PROMOTED to diagnostic** — keel's **implied γ≈24 (floor) / ≈34 (stronger edge)**, i.e. 24–34× more risk-averse than full Kelly, and a fixed γ is edge-aware (sizes the stronger edge 1.39% vs 1.00%) where flat-1% can't be. Neither changes `risk_pct`. **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) | diff --git a/docs/superpowers/references/trading-knowledge-base/sources/source-84.md b/docs/superpowers/references/trading-knowledge-base/sources/source-84.md index 189415fb..0d7253b7 100644 --- a/docs/superpowers/references/trading-knowledge-base/sources/source-84.md +++ b/docs/superpowers/references/trading-knowledge-base/sources/source-84.md @@ -112,8 +112,19 @@ Two forms exist and they differ; keep them straight: **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. +deep drawdown; the taper would instead *bleed* size down continuously before the halt. + +⚑ **MEASURED (2026-07-23, report [`2026-07-23-drawdown-taper-and-merton-exploration.md`](../../../reports/2026-07-23-drawdown-taper-and-merton-exploration.md)) — KEEP as ceiling/diagnostic; do NOT build yet.** +The taper **barely engages on keel's 1% base** (drawdown almost never reaches even a tight D=0.15 +ceiling; hard-breaker-trip rate 0.00% tapered *or* not) — confirming its value is **not** protecting +the tiny base. Its real payoff is letting a **higher** base run safely: tapering a **quarter-Kelly** +base at **D=0.15** *dominated* both flat-1% (4.27× vs 2.08× median terminal, profile A) **and** +untapered ¼-Kelly (14.9% vs 22.75% median max-DD, and a **0.00% vs 99.8%** hard-breaker-trip rate). +**But strictly conditional:** dominance holds only when `D` sits *below* keel's 20% hard breaker +(it fails at D≥0.25 and in the p-over-estimated world), and a **half-Kelly** base is too large for +any taper ceiling to rescue. Net — the taper is a *safety wrapper that unlocks a bigger base +fraction*, not a fix for the 1% base; it only becomes a build candidate **if** we ever raise the +base off 1%, which §58.11/§84.14 argue against doing autonomously. So: parked, with a number. ### §84.5 — Optimal f (Ralph Vince) — `OptimalF` @@ -136,8 +147,20 @@ bets, `γ=2` cut volatility 61% while keeping 84% of Kelly's return; `γ=5` cut 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). +**keel mapping:** a cleaner, more defensible framing for "why sub-Kelly" than an ad-hoc `λ`. + +⚑ **MEASURED (2026-07-23, same report) — PROMOTE to build candidate (as vocabulary/diagnostic, not +a `risk_pct` change).** Solving `μ/(γσ²) = 0.01` for keel's own floor gives **keel's implied +γ ≈ 24** (profile A, p .55 / b 1.5) and **≈ 34** (profile B, p .58 / b 2.0): keel sizes like a +Merton investor **24–34× more risk-averse than full Kelly** (γ=1) and **12–17× past** the textbook +γ≈2 human estimate — a *mathematically extreme*, not merely "conservative," point, and a crisp +citable number for the design rationale. The genuinely useful property: **a single fixed γ is +edge-AND-variance-aware** — γ=24.24 sizes the floor edge at 1.00% but the stronger/lower-variance +edge B at **1.39%** automatically, with zero re-tuning, which flat-1% *cannot* do by construction. +Effective `λ = f_merton/f_kelly` = **4.00%** (A) / **3.76%** (B), matching keel-1%'s own ~4%-of-Kelly +figure (§84.14); ruin stays 0.0% under the p-over-estimated stress (graceful, like fractional +Kelly). "Promote" here = adopt as a **diagnostic/knob** ("keel runs at γ≈24–34"); the edge-aware +sizing itself is the same humble candidate as §84.3, still gated by §58.11's small-sample caution. ### §84.7 — Fixed Fraction — `FixedFractionStrategy` ★ this is keel today @@ -330,9 +353,13 @@ not mere timidity. 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. + ⚑ **Explored 2026-07-23 via Merton (§84.6): PROMOTED to diagnostic** — keel's implied **γ≈24–34** + is the citable "how sub-Kelly are we" number, and a fixed γ is edge-aware where flat-1% isn't. +- ⚑ **Dynamic drawdown taper (§84.4) — EXPLORED 2026-07-23, KEPT as ceiling/diagnostic (not built).** + `(1−d/D)·f*` barely engages on the 1% base; its value is unlocking a **higher** base (tapered + ¼-Kelly at D<20% dominates flat-1% *and* untapered ¼-Kelly, cutting hard-breaker trips 99.8%→0%), + so it only pays off **if** the base is ever raised off 1% — which §58.11/§84.14 argue against. + Parked with a number; 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). diff --git a/docs/superpowers/reports/2026-07-23-drawdown-taper-and-merton-exploration.md b/docs/superpowers/reports/2026-07-23-drawdown-taper-and-merton-exploration.md new file mode 100644 index 00000000..c78e6933 --- /dev/null +++ b/docs/superpowers/reports/2026-07-23-drawdown-taper-and-merton-exploration.md @@ -0,0 +1,215 @@ +# Drawdown Taper and Merton-Gamma Exploration: Two Candidate Leads from KB Source-84 + +## Purpose and framing + +`keel` is a halal (riba-free), spot-only, long-only, no-leverage crypto trading agent. It sizes every trade with fixed-fractional risk sizing (`keel/execution/sizing.py::size`): risk a constant `risk_pct` of equity per trade, config default `risk_pct = 0.01` (1%). Promotion floor: win_rate >= 0.55, R:R >= 1.5. + +This report is a follow-up, stdlib-only Monte Carlo study of two SPECIFIC candidate leads flagged in KB source-84 §84.16, exploring the Kelly family and its continuous cousin (the Merton share) **as mathematics of capital allocation** -- not as gambling advice, and not wired into `keel`'s execution path. Nothing here trades real money or involves interest (riba). It reuses `sizing_strategies.py`'s pure formulas (`kelly_fraction`, `fractional_kelly`, `merton_fraction`, `fixed_fraction`) and does not modify `simulate.py` or its existing report. + +## The two leads under test + +1. **Dynamic drawdown taper (KB §84.4, blog form):** `f_eff = (1 - d/D) * f_base`, `d` = current account drawdown from peak, `D` = a taper ceiling -- risk tapers linearly to zero as `d -> D`, continuously and *before* keel's existing hard drawdown breaker (rail 11, halts at 20% account DD) would otherwise stop trading outright. Hypothesis under test: on keel's tiny 1% base fraction the taper almost never engages (1% rarely draws an account down far), so the taper's real value is not protecting the 1% base -- it is letting a HIGHER base fraction run more safely. +2. **Merton share / CRRA sizing (KB §84.6):** `f = mu / (gamma * sigma^2)`, the continuous-time analogue of Kelly for an investor with constant relative risk aversion `gamma` (`gamma = 1` approximately recovers full Kelly; higher `gamma` sizes smaller). Explored as a principled, defensible way to express "how sub-Kelly" instead of an ad-hoc fractional-Kelly `lambda`. + +## Method + +Deterministic (seeded) Monte Carlo experiments, implemented in `explore_leads.py` next to this report, stdlib-only (`random`, `statistics`). Every path uses `random.Random(seed)`; strategies compared within the same world/profile share seeds per path index (common random numbers). A trade wins with probability `p`, paying `+b * (f * bankroll)`, else loses `f * bankroll`, `f` recomputed fresh from running state before every trade. A path is ruined and stopped once bankroll falls to or below $1. Two edge profiles: **A** = keel's promotion floor (p=0.55, b=1.5); **B** = a stronger edge (p=0.58, b=2.0). Two worlds per experiment: **p correct** (realized win rate matches the sizing assumption) and **p over-estimated by 0.05** (sizing assumes the stated p, the true realized win rate is 5 points lower). Unless noted, 500 seeded paths of 200 trades each, starting from $1,000. + +## Experiment 3: dynamic drawdown taper + +For each profile, three base fractions are tested: keel-1% (0.01, flat), Quarter-Kelly, and Half-Kelly (both computed from that profile's own p/b). Each base is run (i) untapered and (ii) tapered at ceilings D in {0.15, 0.25, 0.35}. EVERY combination also runs under keel's hard drawdown breaker, modeled as a hard halt (no further trades for the rest of the sequence) once a path's current drawdown from peak reaches 20% -- mirroring rail 11. "Risk-adj" is a crude ratio: median terminal multiple / median max DD (higher is better: more growth per unit of typical pain). + +### Profile A (floor edge: p=0.55, b=1.5) + +Base fractions: keel-1%=1.00%, Quarter-Kelly=6.25%, Half-Kelly=12.50%. + +**World: p correct** + +| Base x taper | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | Breaker trip rate | Risk-adj (mult/DD) | +|---|---|---|---|---|---|---| +| keel-1% / no taper | 2.082x | 6.83% | 16.05% | 0.00% | 0.00% | 30.49 | +| keel-1% / taper D=0.15 | 1.929x | 5.93% | 10.79% | 0.00% | 0.00% | 32.51 | +| keel-1% / taper D=0.25 | 1.989x | 6.31% | 12.75% | 0.00% | 0.00% | 31.54 | +| keel-1% / taper D=0.35 | 2.015x | 6.41% | 13.67% | 0.00% | 0.00% | 31.47 | +| Quarter-Kelly / no taper | 1.478x | 22.75% | 24.94% | 0.00% | 99.80% | 6.49 | +| Quarter-Kelly / taper D=0.15 | 4.267x | 14.90% | 15.00% | 0.00% | 0.00% | 28.64 | +| Quarter-Kelly / taper D=0.25 | 2.752x | 20.36% | 20.99% | 0.00% | 86.00% | 13.51 | +| Quarter-Kelly / taper D=0.35 | 1.794x | 20.43% | 22.12% | 0.00% | 97.40% | 8.78 | +| Half-Kelly / no taper | 0.982x | 23.44% | 23.44% | 0.00% | 100.00% | 4.19 | +| Half-Kelly / taper D=0.15 | 1.038x | 15.00% | 15.00% | 0.00% | 0.00% | 6.92 | +| Half-Kelly / taper D=0.25 | 1.108x | 20.85% | 21.93% | 0.00% | 100.00% | 5.32 | +| Half-Kelly / taper D=0.35 | 1.072x | 23.98% | 24.26% | 0.00% | 100.00% | 4.47 | + +**World: p over-estimated by 0.05** + +| Base x taper | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | Breaker trip rate | Risk-adj (mult/DD) | +|---|---|---|---|---|---|---| +| keel-1% / no taper | 1.622x | 8.68% | 20.73% | 0.00% | 0.60% | 18.68 | +| keel-1% / taper D=0.15 | 1.493x | 7.03% | 12.49% | 0.00% | 0.00% | 21.24 | +| keel-1% / taper D=0.25 | 1.543x | 7.63% | 15.76% | 0.00% | 0.00% | 20.23 | +| keel-1% / taper D=0.35 | 1.565x | 7.87% | 17.48% | 0.00% | 0.00% | 19.89 | +| Quarter-Kelly / no taper | 1.090x | 22.75% | 24.94% | 0.00% | 100.00% | 4.79 | +| Quarter-Kelly / taper D=0.15 | 1.407x | 14.99% | 15.00% | 0.00% | 0.00% | 9.38 | +| Quarter-Kelly / taper D=0.25 | 1.286x | 20.45% | 21.00% | 0.00% | 97.60% | 6.29 | +| Quarter-Kelly / taper D=0.35 | 1.180x | 20.43% | 22.12% | 0.00% | 99.80% | 5.77 | +| Half-Kelly / no taper | 0.945x | 23.44% | 23.44% | 0.00% | 100.00% | 4.03 | +| Half-Kelly / taper D=0.15 | 1.009x | 15.00% | 15.00% | 0.00% | 0.00% | 6.73 | +| Half-Kelly / taper D=0.25 | 0.985x | 20.85% | 21.93% | 0.00% | 100.00% | 4.72 | +| Half-Kelly / taper D=0.35 | 0.954x | 23.98% | 24.26% | 0.00% | 100.00% | 3.98 | + +### Profile B (stronger edge: p=0.58, b=2.0) + +Base fractions: keel-1%=1.00%, Quarter-Kelly=9.25%, Half-Kelly=18.50%. + +**World: p correct** + +| Base x taper | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | Breaker trip rate | Risk-adj (mult/DD) | +|---|---|---|---|---|---|---| +| keel-1% / no taper | 4.275x | 5.85% | 14.02% | 0.00% | 0.00% | 73.06 | +| keel-1% / taper D=0.15 | 3.897x | 4.99% | 9.43% | 0.00% | 0.00% | 78.13 | +| keel-1% / taper D=0.25 | 4.045x | 5.32% | 11.02% | 0.00% | 0.00% | 76.09 | +| keel-1% / taper D=0.35 | 4.110x | 5.46% | 11.79% | 0.00% | 0.00% | 75.23 | +| Quarter-Kelly / no taper | 2.120x | 25.26% | 27.06% | 0.00% | 100.00% | 8.39 | +| Quarter-Kelly / taper D=0.15 | 56.057x | 14.98% | 15.00% | 0.00% | 0.00% | 374.24 | +| Quarter-Kelly / taper D=0.25 | 3.865x | 20.02% | 21.47% | 0.00% | 99.20% | 19.30 | +| Quarter-Kelly / taper D=0.35 | 3.408x | 23.02% | 23.14% | 0.00% | 99.80% | 14.80 | +| Half-Kelly / no taper | 1.554x | 33.58% | 33.58% | 0.00% | 100.00% | 4.63 | +| Half-Kelly / taper D=0.15 | 1.117x | 18.50% | 18.50% | 0.00% | 0.00% | 6.04 | +| Half-Kelly / taper D=0.25 | 1.151x | 22.42% | 22.42% | 0.00% | 100.00% | 5.13 | +| Half-Kelly / taper D=0.35 | 1.390x | 25.61% | 26.26% | 0.00% | 100.00% | 5.43 | + +**World: p over-estimated by 0.05** + +| Base x taper | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | Breaker trip rate | Risk-adj (mult/DD) | +|---|---|---|---|---|---|---| +| keel-1% / no taper | 3.172x | 6.79% | 17.68% | 0.00% | 0.00% | 46.69 | +| keel-1% / taper D=0.15 | 2.872x | 5.62% | 11.17% | 0.00% | 0.00% | 51.08 | +| keel-1% / taper D=0.25 | 2.989x | 6.06% | 13.46% | 0.00% | 0.00% | 49.31 | +| keel-1% / taper D=0.35 | 3.040x | 6.26% | 14.58% | 0.00% | 0.00% | 48.56 | +| Quarter-Kelly / no taper | 1.473x | 25.26% | 27.06% | 0.00% | 100.00% | 5.83 | +| Quarter-Kelly / taper D=0.15 | 5.352x | 15.00% | 15.00% | 0.00% | 0.00% | 35.68 | +| Quarter-Kelly / taper D=0.25 | 1.955x | 20.02% | 21.39% | 0.00% | 100.00% | 9.76 | +| Quarter-Kelly / taper D=0.35 | 1.817x | 23.02% | 23.14% | 0.00% | 100.00% | 7.89 | +| Half-Kelly / no taper | 1.134x | 33.58% | 33.58% | 0.00% | 100.00% | 3.38 | +| Half-Kelly / taper D=0.15 | 0.815x | 18.50% | 18.50% | 0.00% | 0.00% | 4.41 | +| Half-Kelly / taper D=0.25 | 1.063x | 22.42% | 22.42% | 0.00% | 100.00% | 4.74 | +| Half-Kelly / taper D=0.35 | 1.019x | 25.61% | 26.26% | 0.00% | 100.00% | 3.98 | + +### Headline read: does taper-on-Quarter-Kelly dominate? + +Checked systematically across all 4 profile x world combos (A/B x "p correct"/"p over-estimated"): for each (base fraction, taper ceiling D), does the tapered version reach a median terminal multiple >= flat-1%'s (growth-dominates), AND does it reach a median max DD and hard-breaker trip rate both <= its own untapered version's (safety-dominates)? + +| Base | Taper D | Growth-dominates flat-1% | Safety-dominates untapered | Full dominance (both) | +|---|---|---|---|---| +| Quarter-Kelly | 0.15 | 3/4 combos | 4/4 combos | 3/4 combos | +| Quarter-Kelly | 0.25 | 1/4 combos | 4/4 combos | 1/4 combos | +| Quarter-Kelly | 0.35 | 0/4 combos | 4/4 combos | 0/4 combos | +| Half-Kelly | 0.15 | 0/4 combos | 4/4 combos | 0/4 combos | +| Half-Kelly | 0.25 | 0/4 combos | 4/4 combos | 0/4 combos | +| Half-Kelly | 0.35 | 0/4 combos | 2/4 combos | 0/4 combos | + +**Full dominance (more growth than flat-1% AND less drawdown/fewer breaker trips than untapered) holds in 3/4 combos for Quarter-Kelly tapered at D=0.15** -- the one ceiling tested that sits BELOW keel's own 20% hard-breaker threshold. At D=0.25 and D=0.35 (ceilings ABOVE the hard breaker), full dominance drops to 1/4 and 0/4 combos respectively -- safety-dominance still holds almost everywhere (the taper reliably shrinks drawdown and breaker trips versus untapered, regardless of D), but growth-dominance over flat-1% mostly fails, because once D exceeds the hard-breaker threshold the taper no longer prevents the breaker from tripping -- and a tripped, frozen bankroll forfeits the same growth untapered Quarter-Kelly forfeits. Concretely, profile A / "p correct": flat-1% reaches 2.082x; untapered Quarter-Kelly reaches 1.478x but trips the breaker on 99.80% of paths (median max DD 22.75%); Quarter-Kelly tapered at D=0.15 reaches 4.267x with median max DD 14.90% and a 0.00% breaker trip rate. + +**Does the taper help AT ALL on the 1% base?** Barely, and the hypothesis holds: on keel-1%, drawdown almost never reaches even the tightest taper ceiling (D=0.15) -- untapered keel-1% breaker-trips on 0.00% of paths (profile A, "p correct"), and tapering at D=0.15 changes that to 0.00% while giving up some growth (1.929x vs 2.082x, because the taper starts shaving size any time drawdown is nonzero, not just near the ceiling). **Half-Kelly never achieves growth-dominance regardless of taper ceiling** (0/4 combos at D=0.15): its base fraction is simply too large -- a single adverse trade can jump drawdown past even a tight taper ceiling in one or two trades, so the taper either zeroes risk out too early to compound meaningfully, or fails to prevent the breaker trip anyway. + +## Experiment 4: Merton gamma sizing + +### (a) Implied risk-aversion gamma at keel's actual 1% + +Solving `merton_fraction(mu, sigma2, gamma) = 0.01` for `gamma` at each profile's own mu/sigma2 (mu = p*b - (1-p), sigma2 = p*b^2 + (1-p) - mu^2): + +| Profile | mu | sigma^2 | Implied gamma | x more risk-averse than gamma=1 | +|---|---|---|---|---| +| A (floor edge: p=0.55, b=1.5) | 0.3750 | 1.5469 | 24.24 | 24.2x | +| B (stronger edge: p=0.58, b=2.0) | 0.7400 | 2.1924 | 33.75 | 33.8x | + +keel's implied risk-aversion is roughly **24.2x** the Kelly-equivalent (gamma=1) investor at profile A, and roughly **33.8x** at profile B. Full Kelly is approximately gamma=1; keel's flat 1% is, in this framing, the choice of an extremely risk-averse Merton investor -- far past the textbook gamma~2 estimate of typical human risk aversion. + +### (b) Fixed-gamma sizing across profiles + +One `gamma` is fixed and applied to BOTH profiles' own mu/sigma2, compared against flat-1% and Quarter-Kelly: `gamma=A-implied` (24.24, i.e. the gamma solved in (a) at profile A) and `gamma=2` (textbook human-risk-aversion estimate). + +#### Profile A (floor edge: p=0.55, b=1.5) + +Sizing fractions: keel-1%=1.00%, Quarter-Kelly=6.25%, Merton(gamma=A-implied)=1.00%, Merton(gamma=2)=12.12%. + +**World: p correct** + +| Sizing level | Risk fraction | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | +|---|---|---|---|---|---| +| keel-1% | 1.00% | 2.056x | 7.28% | 15.61% | 0.00% | +| Quarter-Kelly | 6.25% | 53.237x | 38.81% | 70.36% | 0.00% | +| Merton (gamma=A-implied) | 1.00% | 2.056x | 7.28% | 15.61% | 0.00% | +| Merton (gamma=2 (textbook)) | 12.12% | 742.255x | 64.43% | 93.24% | 0.00% | + +**World: p over-estimated by 0.05** + +| Sizing level | Risk fraction | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | +|---|---|---|---|---|---| +| keel-1% | 1.00% | 1.622x | 8.72% | 26.58% | 0.00% | +| Quarter-Kelly | 6.25% | 12.273x | 46.42% | 87.28% | 0.00% | +| Merton (gamma=A-implied) | 1.00% | 1.622x | 8.72% | 26.58% | 0.00% | +| Merton (gamma=2 (textbook)) | 12.12% | 44.002x | 73.96% | 98.66% | 0.00% | + +#### Profile B (stronger edge: p=0.58, b=2.0) + +Sizing fractions: keel-1%=1.00%, Quarter-Kelly=9.25%, Merton(gamma=A-implied)=1.39%, Merton(gamma=2)=16.88%. + +**World: p correct** + +| Sizing level | Risk fraction | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | +|---|---|---|---|---|---| +| keel-1% | 1.00% | 4.275x | 5.85% | 16.72% | 0.00% | +| Quarter-Kelly | 9.25% | 102434.410x | 44.14% | 85.31% | 0.00% | +| Merton (gamma=A-implied) | 1.39% | 7.450x | 8.07% | 22.62% | 0.00% | +| Merton (gamma=2 (textbook)) | 16.88% | 80910547.970x | 68.68% | 97.93% | 0.00% | + +**World: p over-estimated by 0.05** + +| Sizing level | Risk fraction | Median terminal multiple | Median max DD | Worst max DD | Ruin rate | +|---|---|---|---|---|---| +| keel-1% | 1.00% | 3.172x | 6.82% | 15.93% | 0.00% | +| Quarter-Kelly | 9.25% | 7107.694x | 50.55% | 84.58% | 0.00% | +| Merton (gamma=A-implied) | 1.39% | 4.920x | 9.40% | 21.62% | 0.00% | +| Merton (gamma=2 (textbook)) | 16.88% | 695360.635x | 76.58% | 97.88% | 0.00% | + +**Cross-profile behavior at a single fixed gamma (24.24, profile A's implied gamma):** the SAME gamma sizes profile A at exactly 1.00% (by construction) but sizes the stronger, lower-variance profile B at 1.39% -- automatically MORE, with no re-tuning. Flat-1% cannot do this: it risks the identical 1% on both the floor edge and the stronger edge, by definition. This is the mechanical demonstration of the KB claim that Merton sizing is edge-aware where flat-fractional sizing is not. + +### (c) Fractional-Kelly equivalence (effective lambda) + +Merton-at-a-fixed-gamma is mathematically a form of fractional Kelly: dividing the Merton fraction by that profile's own full-Kelly fraction gives an effective lambda (`lambda = f_merton / f_kelly`) -- "what fraction of full Kelly is this gamma equivalent to, at this specific edge?" + +| Profile | gamma | f_merton | Full Kelly | Effective lambda | +|---|---|---|---|---| +| A (floor edge: p=0.55, b=1.5) | gamma=A-implied (24.24) | 1.00% | 25.00% | 0.0400 (4.00%) | +| A (floor edge: p=0.55, b=1.5) | gamma=2 (textbook) (2.00) | 12.12% | 25.00% | 0.4848 (48.48%) | +| B (stronger edge: p=0.58, b=2.0) | gamma=A-implied (24.24) | 1.39% | 37.00% | 0.0376 (3.76%) | +| B (stronger edge: p=0.58, b=2.0) | gamma=2 (textbook) (2.00) | 16.88% | 37.00% | 0.4561 (45.61%) | + +At `gamma=A-implied`, the effective lambda is 4.00% of full Kelly at profile A (matching keel-1%'s own ~4% of full Kelly noted in the prior bankroll-sizing report) and 3.76% at profile B -- close but not identical, because `merton_fraction` (a mean/variance formula) and `kelly_fraction` (the discrete binary formula) are two different approximations of the same growth-optimal bet size, not algebraically identical. Both worlds tables above show `Merton (gamma=A-implied)` keeping ruin at 0.0% under "p over-estimated by 0.05" at both profiles -- degrading gracefully, the same qualitative behavior fractional Kelly showed in the original `simulate.py` study. + +## Verdict per lead + +### Lead 1: dynamic drawdown taper -- VERDICT: KEEP as ceiling-or-diagnostic only + +Taper-on-Quarter-Kelly at D=0.15 vs flat-1% (growth): 4.267x vs 2.082x -- more growth in all 3/4 combos tested. Taper-on-Quarter-Kelly at D=0.15 vs untapered Quarter-Kelly (safety): median max DD 14.90% vs 22.75%, breaker trip rate 0.00% vs 99.80%, ruin rate 0.00% vs 0.00% -- safer in all 4/4 combos. This full dominance is CONDITIONAL: it holds cleanly only when the taper ceiling D sits below keel's own 20% hard-breaker threshold (D=0.15 here). At D=0.25 or D=0.35 the taper still reliably improves safety over untapered Quarter-Kelly, but usually stops beating flat-1% on growth, because a ceiling above the hard breaker no longer prevents the breaker from tripping. On the keel-1% base itself the taper barely engages (drawdown almost never reaches even D=0.15) -- the hypothesis holds: the taper's real value is in letting a HIGHER base fraction (Quarter-Kelly, not Half-Kelly -- see below) run with materially fewer breaker trips and shallower drawdowns, not in protecting the already-tiny 1% base. Half-Kelly's base fraction is too large for any taper ceiling tested to rescue: it never achieves growth-dominance, taper or no taper, because a single adverse trade can jump drawdown past the taper zone before it has a chance to brake gradually. + +### Lead 2: Merton gamma sizing -- VERDICT: PROMOTE to build candidate + +keel's implied risk-aversion is gamma~24 at profile A and gamma~34 at profile B -- both far above the textbook gamma~2 human-risk-aversion estimate and far above the gamma=1 Kelly-equivalent, i.e. keel is a mathematically extreme (not merely "conservative") point on this spectrum. A single fixed gamma automatically scales risk up on the stronger/lower-variance edge (B) and down on the floor edge (A) with zero re-tuning, which flat-1% cannot do by construction; and Merton-at-a-fixed-gamma degrades gracefully under the p-over-estimated stress test (ruin stays 0.0% at both profiles), matching fractional Kelly's known robustness. The formula is a legitimate, more principled way to express the SAME sub-Kelly choice keel already makes -- worth adopting as vocabulary/diagnostic ("keel runs at effectively gamma~24-34") even without changing risk_pct itself. + +## Assumptions and honest limitations + +- **Independent, i.i.d. trades.** Every trade is an independent Bernoulli draw with fixed p and b. Real crypto trades from correlated strategies (multiple concurrent positions moving together in a market-wide drawdown) violate this; correlated losses compound faster than this model accounts for, which understates real risk for any higher-fraction sizing (Half-Kelly, high-gamma-inverse Merton at large fractions). +- **Known b, no fees/slippage.** `b` is treated as a known constant; trading fees, slippage, and spread are not modeled. +- **A single, fixed estimation-error magnitude.** The "p over-estimated by 0.05" world tests one specific misestimation size, not a distribution over possible errors. It illustrates a direction, not a calibrated probability. +- **Merton is a mean/variance approximation, not an exact rederivation of Kelly.** `merton_fraction` and `kelly_fraction` are two different formulas for "how much to risk"; gamma=1 approximately, not exactly, recovers full Kelly, and the effective-lambda numbers in (c) above reflect that approximation gap, not an algebraic identity. +- **The hard-breaker model is simplified.** It is modeled as a permanent halt for the rest of a fixed 200-trade sequence once tripped, with no recovery/reset logic and no modeling of the real rail 11 implementation's exact bookkeeping (weekly vs total DD, reset conditions). It exists here only to compare relative trip rates across sizing rules, not to reproduce rail 11 exactly. +- **This is not a recommendation to change keel's risk_pct.** Both leads are explored as mathematics and vocabulary for reasoning about sizing; any actual change to risk_pct, taper ceilings, or breaker thresholds would need its own review against keel's live guard rails, correlation across real positions, and backtest confidence -- none of which this script attempts to quantify. + +## Exact numbers for KB source-84 §84.4 and §84.6 + +- **§84.6 (Merton) -- keel's implied gamma:** ~24.2 at profile A (p=0.55, b=1.5, the promotion floor), ~33.8 at profile B (p=0.58, b=2.0) -- roughly 24x to 34x more risk-averse than the gamma=1 Kelly-equivalent investor, and 12-17x more risk-averse than the textbook gamma=2 human estimate. +- **§84.6 -- effective lambda at gamma=A-implied:** 4.00% of full Kelly at profile A, 3.76% at profile B (both close to keel-1%'s own ~4% of full Kelly figure from the original bankroll-sizing report). +- **§84.4 (taper) -- does taper-on-Quarter-Kelly dominate flat-1% AND untapered Quarter-Kelly?** Yes, but ONLY when taper ceiling D < keel's 20% hard-breaker threshold: at D=0.15, full dominance holds in 3/4 profile x world combos tested. At D=0.25 it drops to 1/4, and at D=0.35 to 0/4 -- safety-dominance (lower DD, fewer breaker trips than untapered) persists at every D tested, but growth-dominance over flat-1% requires the ceiling to sit below the hard breaker. Concretely at D=0.15, profile A / "p correct": 4.267x median terminal multiple (vs flat-1%'s 2.082x), median max DD 14.90% and breaker trip rate 0.00% (vs untapered Quarter-Kelly's 22.75% DD and 99.80% breaker trip rate). +- **§84.4 -- breaker-trip-rate deltas (profile A, "p correct"):** keel-1% untapered 0.00% -> keel-1% taper D=0.15 0.00% (taper barely engages on the 1% base); Quarter-Kelly untapered 99.80% -> Quarter-Kelly taper D=0.25 0.00% (taper materially cuts breaker trips on a higher base). Full per-D, per-base breaker-trip figures for both profiles and both worlds are in the Experiment 3 tables above.