edgeorluck 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. edgeorluck/__init__.py +16 -0
  2. edgeorluck/cli.py +92 -0
  3. edgeorluck/compare.py +146 -0
  4. edgeorluck/data/claims.json +184 -0
  5. edgeorluck/data/validation.json +641 -0
  6. edgeorluck/evidence.py +325 -0
  7. edgeorluck/fills.py +195 -0
  8. edgeorluck/judge.py +1540 -0
  9. edgeorluck/llms.txt +59 -0
  10. edgeorluck/mcp_server.py +113 -0
  11. edgeorluck/readers.py +1062 -0
  12. edgeorluck/report.py +142 -0
  13. edgeorluck/result.py +86 -0
  14. edgeorluck/samples.py +242 -0
  15. edgeorluck/schema.py +142 -0
  16. edgeorluck/single.py +142 -0
  17. edgeorluck/web/__init__.py +1 -0
  18. edgeorluck/web/app.py +378 -0
  19. edgeorluck/web/ledger.py +211 -0
  20. edgeorluck/web/static/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
  21. edgeorluck/web/static/assets/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
  22. edgeorluck/web/static/assets/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  23. edgeorluck/web/static/assets/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  24. edgeorluck/web/static/assets/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  25. edgeorluck/web/static/assets/fonts/ibm-plex-sans-latin-700-normal.woff2 +0 -0
  26. edgeorluck/web/static/assets/fonts/kalam-LICENSE.txt +93 -0
  27. edgeorluck/web/static/assets/fonts/kalam-latin-400-normal.woff2 +0 -0
  28. edgeorluck/web/static/assets/fonts/kalam-latin-700-normal.woff2 +0 -0
  29. edgeorluck/web/static/assets/site.css +182 -0
  30. edgeorluck/web/static/assets/site.js +117 -0
  31. edgeorluck/web/static/how.html +294 -0
  32. edgeorluck/web/static/index.html +1576 -0
  33. edgeorluck/words.py +122 -0
  34. edgeorluck-2.0.0.dist-info/METADATA +226 -0
  35. edgeorluck-2.0.0.dist-info/RECORD +39 -0
  36. edgeorluck-2.0.0.dist-info/WHEEL +5 -0
  37. edgeorluck-2.0.0.dist-info/entry_points.txt +3 -0
  38. edgeorluck-2.0.0.dist-info/licenses/LICENSE +21 -0
  39. edgeorluck-2.0.0.dist-info/top_level.txt +1 -0
edgeorluck/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """edge or luck: is a trading record a real edge, or luck?
2
+
3
+ import edgeorluck
4
+ result = edgeorluck.judge("trades.csv", variants_tried=40)
5
+ print(result["verdict"]["headline"])
6
+ print(result.to_markdown())
7
+
8
+ Pure: reads its input, writes nothing, and the same input always gives the
9
+ same result. Not financial advice: a statistical check of past trades.
10
+ """
11
+ from .judge import CHANGELOG, ENGINE_VERSION, SCHEMA_VERSION
12
+ from .result import Result, judge
13
+ from .schema import schema
14
+
15
+ __version__ = ENGINE_VERSION
16
+ __all__ = ["judge", "schema", "Result", "ENGINE_VERSION", "SCHEMA_VERSION", "CHANGELOG", "__version__"]
edgeorluck/cli.py ADDED
@@ -0,0 +1,92 @@
1
+ """The command line.
2
+
3
+ edgeorluck trades.csv [--tried 50] [--benchmark 34.5] [--json | --md]
4
+ edgeorluck live.csv --backtest backtest.csv
5
+ edgeorluck - # read the trades from stdin
6
+ edgeorluck --schema # the result's JSON Schema
7
+ edgeorluck --serve [--port 5078] # the web app (needs edgeorluck[web])
8
+ edgeorluck --single-file out.py # the judge in one file, for chatbots
9
+
10
+ Several files are judged one by one. Exit code 0 when judged, 2 when the
11
+ input needs something (status need_input), 1 on an error.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from typing import List, Optional
19
+
20
+ from .judge import ENGINE_VERSION
21
+ from .result import judge
22
+ from .schema import schema
23
+
24
+
25
+ def _parser() -> argparse.ArgumentParser:
26
+ p = argparse.ArgumentParser(prog="edgeorluck",
27
+ description="Is a trading record a real edge, or luck? Give it a trade export "
28
+ "(TradingView list of trades, exchange fills, any CSV with a P&L "
29
+ "column, XLSX, freqtrade JSON) or '-' for stdin.")
30
+ p.add_argument("files", nargs="*", help="trade exports to judge, one by one")
31
+ p.add_argument("--tried", "--variants-tried", dest="tried",
32
+ help="how many versions or settings were tried in all (raises the bar)")
33
+ p.add_argument("--benchmark", help="what buying and holding returned over the same dates, in percent")
34
+ p.add_argument("--fees", help="are fees included? yes or no")
35
+ p.add_argument("--source", help="backtest, paper or live")
36
+ p.add_argument("--unit", help="'%%' or a currency like USDT, when the file doesn't say")
37
+ p.add_argument("--kind", help="auto, trades, fills, tradingview or pnl")
38
+ p.add_argument("--backtest", help="the backtest's trades, to check live trades against")
39
+ out = p.add_mutually_exclusive_group()
40
+ out.add_argument("--json", action="store_true", help="print the JSON result")
41
+ out.add_argument("--md", action="store_true", help="print Markdown (the default)")
42
+ p.add_argument("--schema", action="store_true", help="print the result's JSON Schema and exit")
43
+ p.add_argument("--serve", action="store_true", help="run the web app with waitress")
44
+ p.add_argument("--host", default="127.0.0.1", help="web app host (default 127.0.0.1)")
45
+ p.add_argument("--port", type=int, default=None, help="web app port (default $PORT or 5078)")
46
+ p.add_argument("--single-file", nargs="?", const="edgeorluck_single.py", metavar="PATH",
47
+ help="write the one-file build and exit")
48
+ p.add_argument("--version", action="version", version=f"edgeorluck {ENGINE_VERSION}")
49
+ return p
50
+
51
+
52
+ def main(argv: Optional[List[str]] = None) -> int:
53
+ args = _parser().parse_args(argv)
54
+ if args.schema:
55
+ print(json.dumps(schema(), indent=2))
56
+ return 0
57
+ if args.single_file:
58
+ from .single import write
59
+ print(write(args.single_file))
60
+ return 0
61
+ if args.serve:
62
+ try:
63
+ from .web.app import serve
64
+ except ImportError:
65
+ print("The web app needs: pip install 'edgeorluck[web]'", file=sys.stderr)
66
+ return 1
67
+ serve(host=args.host, port=args.port)
68
+ return 0
69
+ if not args.files:
70
+ _parser().print_help()
71
+ return 2
72
+ worst = 0
73
+ for i, path in enumerate(args.files):
74
+ data = sys.stdin.buffer.read() if path == "-" else path
75
+ result = judge(data, variants_tried=args.tried, benchmark_return_pct=args.benchmark, kind=args.kind,
76
+ unit=args.unit, fees_included=args.fees, source=args.source, backtest=args.backtest)
77
+ if i:
78
+ print()
79
+ text = result.to_json() if args.json else result.to_markdown()
80
+ try:
81
+ print(text)
82
+ except UnicodeEncodeError:
83
+ sys.stdout.buffer.write((text + "\n").encode("utf-8"))
84
+ if result.status == "need_input":
85
+ worst = max(worst, 2)
86
+ elif result.status == "error":
87
+ worst = max(worst, 1) if worst != 2 else worst
88
+ return worst
89
+
90
+
91
+ if __name__ == "__main__":
92
+ raise SystemExit(main())
edgeorluck/compare.py ADDED
@@ -0,0 +1,146 @@
1
+ """Backtest vs live: are the live trades behaving like the backtest promised?
2
+
3
+ The backtest is a finished sample; live trades keep coming and the trader
4
+ checks often. So the live average gets an anytime-valid range (one side at
5
+ 1.25%), the backtest average a fixed-sample range (one side at 1.25%), and
6
+ the difference range is their union: valid at 95% however often the live
7
+ side is checked. "Worse than the backtest" means the whole difference range
8
+ is below zero, which is the same as the live e-value against the backtest's
9
+ pessimistic average reaching 80.
10
+
11
+ Next to it, plain odds (one look): if live trades were drawn from the
12
+ backtest's own trades, how often would a live record this weak happen?
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import math
18
+ from typing import Any, Dict, Optional
19
+
20
+ import numpy as np
21
+
22
+ from .evidence import half_width, log_e_one_sided, mixing_rho2, serial_factor
23
+ from .readers import ReadResult, Trades
24
+ from .words import count, fmt, one_in, rnd
25
+
26
+ MIN_LIVE = 10
27
+ BAR = 80.0 # one side at 1.25% for the live side
28
+ Z_BACKTEST = 2.2414 # one side at 1.25% for the backtest side
29
+ DRAW_CELLS = 2_000_000
30
+
31
+
32
+ def _live_sd(x: np.ndarray) -> float:
33
+ if len(x) < 2:
34
+ return float("nan")
35
+ sd = float(np.std(x, ddof=1))
36
+ return max(sd, 1e-12) * math.sqrt(serial_factor(x))
37
+
38
+
39
+ def _pick(read: ReadResult, name: Optional[str] = None) -> Optional[Trades]:
40
+ if not read.strategies:
41
+ return None
42
+ if name:
43
+ for st in read.strategies:
44
+ if st.name == name:
45
+ return st
46
+ return read.strategies[0]
47
+
48
+
49
+ def _series(st: Trades, unit: str) -> Optional[np.ndarray]:
50
+ if st.unit == unit:
51
+ return st.pnl
52
+ if unit == "%" and st.pct is not None and np.isfinite(st.pct).all():
53
+ return st.pct
54
+ return None
55
+
56
+
57
+ def compare_runs(live_read: ReadResult, backtest_read: ReadResult) -> Dict[str, Any]:
58
+ why = ("A backtest is a promise. Live trades show whether the promise holds, and the sooner you know, the less "
59
+ "it costs.")
60
+ live = _pick(live_read)
61
+ bt = _pick(backtest_read, live.name if live else None)
62
+ if live is None or bt is None:
63
+ missing = "live trades" if live is None else "backtest trades"
64
+ return {"code": "need_input", "label": "Can't compare", "tone": "muted",
65
+ "headline": f"Can't compare: no {missing} found.",
66
+ "plain": (backtest_read.need or live_read.need or {}).get("problem", ""), "why": why,
67
+ "next_step": {"text": "Send both files: the backtest's list of trades and the live trades.",
68
+ "why": "The comparison needs one result per trade on both sides."}}
69
+ unit = live.unit if live.unit == bt.unit else "%"
70
+ x, y = _series(live, unit), _series(bt, unit)
71
+ if x is None or y is None:
72
+ return {"code": "units_differ", "label": "Can't compare", "tone": "muted",
73
+ "headline": f"Can't compare: the live trades are in {live.unit} and the backtest in {bt.unit}.",
74
+ "plain": "Export both in the same unit, or with a percent column, and check again.", "why": why,
75
+ "next_step": {"text": "Export both lists with a percent-per-trade column, then compare again.",
76
+ "why": "Money per trade depends on position size; percent compares like with like."}}
77
+ n, nb = len(x), len(y)
78
+ mean_l, mean_b = float(np.mean(x)), float(np.mean(y)) if nb else float("nan")
79
+ base = {"live": {"name": live.name, "trades": n, "mean": rnd(mean_l), "unit": unit},
80
+ "backtest": {"name": bt.name, "trades": nb, "mean": rnd(mean_b), "unit": unit},
81
+ "unit_compared": unit, "why": why}
82
+ if nb < 30:
83
+ return dict(base, code="too_early", label="Too early", tone="muted",
84
+ headline=f"The backtest has only {count(nb, 'trade')}; it can't set a standard yet.",
85
+ plain="A backtest needs at least 30 trades to say what to expect.",
86
+ next_step={"text": "Run the backtest over a longer period, then compare again.",
87
+ "why": "A short backtest promises very little."})
88
+ sd_l, sd_b = _live_sd(x), _live_sd(y)
89
+ h_live = half_width(n, sd_l, BAR) if n >= 2 else math.inf
90
+ h_bt = Z_BACKTEST * sd_b / math.sqrt(nb)
91
+ diff = mean_l - mean_b
92
+ lo, hi = diff - h_live - h_bt, diff + h_live + h_bt
93
+ floor = mean_b - h_bt
94
+ log_e = log_e_one_sided(n, float(np.sum(x - floor)), sd_l, mixing_rho2(BAR), -1) if n >= 2 else 0.0
95
+ p = _odds(x, y)
96
+ odds = one_in(p, _draws(n))
97
+ base.update(difference={"value": rnd(diff), "range_95": [rnd(lo), rnd(hi)]}, odds=rnd(p), odds_text=odds,
98
+ e_value_worse=rnd(math.exp(min(log_e, 700))), bar=BAR)
99
+ if n < MIN_LIVE:
100
+ return dict(base, code="too_early", label="Too early", tone="muted",
101
+ headline=f"Too early to compare: {count(n, 'live trade')} so far.",
102
+ plain=f"Live averages {fmt(mean_l, unit)} per trade and the backtest {fmt(mean_b, unit)}. With "
103
+ f"{count(n, 'trade')}, luck decides almost everything.",
104
+ next_step={"text": f"Collect at least {MIN_LIVE - n} more live trades, then compare again.",
105
+ "why": "A handful of trades can't tell a broken strategy from a bad week."})
106
+ if hi < 0:
107
+ return dict(base, code="worse_than_backtest", label="Worse than the backtest", tone="bad",
108
+ headline=f"Live is worse than the backtest promised: {fmt(mean_l, unit)} per trade live, "
109
+ f"{fmt(mean_b, unit)} in the backtest.",
110
+ plain=f"Over {count(n, 'live trade')} the gap is {fmt(diff, unit)} per trade (likely "
111
+ f"{fmt(lo, unit)} to {fmt(hi, unit)}). If live trades were like the backtest's, a live "
112
+ f"record this weak would happen {odds} times. The backtest was likely too kind: fees, "
113
+ "fills, or rules tuned to the past.",
114
+ next_step={"text": "Compare the live fills with the backtest's: fees, slippage and the exact "
115
+ "entry prices. Ask your AI to look for lookahead in the backtest.",
116
+ "why": "The gap between backtest and live almost always comes from costs, fills or "
117
+ "a backtest that saw the future."})
118
+ gap_text = (f" If live trades were like the backtest's, a live record this weak would happen {odds} times."
119
+ if diff < 0 else "")
120
+ return dict(base, code="consistent", label="In line with the backtest", tone="good" if diff >= 0 else "warn",
121
+ headline=f"Live is in line with the backtest so far: {fmt(mean_l, unit)} per trade live, "
122
+ f"{fmt(mean_b, unit)} in the backtest.",
123
+ plain=f"Over {count(n, 'live trade')} the gap is {fmt(diff, unit)} per trade (likely {fmt(lo, unit)} "
124
+ f"to {fmt(hi, unit)}), and that includes zero.{gap_text} This does not prove the edge; it only "
125
+ "says live has not broken the promise yet.",
126
+ next_step={"text": "Keep comparing every 20 to 30 new live trades.",
127
+ "why": "The comparison stays valid however often you check, so checking often is free."})
128
+
129
+
130
+ def _draws(n: int) -> int:
131
+ return int(max(2000, min(20000, DRAW_CELLS // max(n, 1))))
132
+
133
+
134
+ def _odds(x: np.ndarray, y: np.ndarray) -> float:
135
+ """P(a live record of len(x) drawn from the backtest's trades sums this low)."""
136
+ n = len(x)
137
+ draws = _draws(n)
138
+ digest = hashlib.sha256(np.round(np.concatenate((x, y)), 8).tobytes() + b"compare").digest()
139
+ rng = np.random.default_rng(int.from_bytes(digest[:8], "little"))
140
+ obs = float(x.sum())
141
+ low = 0
142
+ for start in range(0, draws, 1000):
143
+ rows = min(1000, draws - start)
144
+ sums = y[rng.integers(0, len(y), size=(rows, n))].sum(axis=1)
145
+ low += int((sums <= obs + 1e-12).sum())
146
+ return (low + 1) / (draws + 1)
@@ -0,0 +1,184 @@
1
+ {
2
+ "updated": "2026-10-14",
3
+ "statuses": {
4
+ "proved": {"label": "Proved", "means": "Checked by the Lean proof assistant, with no gaps. Click a name to read the proof."},
5
+ "measured": {"label": "Measured", "means": "Approximate on real trades, so we measure it instead of proving it: see VALIDATION.md."},
6
+ "choice": {"label": "Our choice", "means": "A rule we picked to stay careful. Not maths, and we say why."},
7
+ "unchecked": {"label": "Can't check", "means": "Something I cannot see or know. I tell you what to do about it."}
8
+ },
9
+ "steps": [
10
+ {
11
+ "id": "evidence",
12
+ "mood": "idle",
13
+ "title": "I turn your trades into one number: the evidence",
14
+ "flip": "First I ask: if this were pure luck, how surprised should I be?",
15
+ "plain": "I weigh your trades against every possible size of edge at once, small to large, and average the results. That gives one number, the evidence. With no edge it averages exactly 1, so the bigger it gets, the less luck can explain it.",
16
+ "formula": "e = average over edge sizes λ ≥ 0 of exp(λS − λ²n/2)\n = (2/√v) · exp(ρ²S²/2v) · Φ(ρS/√v), where v = 1 + ρ²n",
17
+ "formula_note": "S is your total divided by the spread of your trades, n the number of trades, and the edge sizes λ are weighted by a half-normal curve of width ρ.",
18
+ "claims": [
19
+ {"status": "proved", "text": "The formula in the code is exactly this average.",
20
+ "lean": ["Mixture.eMix_closed_form", "Mixture.eMix_eq_exp_logEStd"], "python": "evidence.log_e_standardized"},
21
+ {"status": "proved", "text": "With no edge the evidence averages exactly 1; with a losing edge, at most 1.",
22
+ "assumes": "Normal trades with a known spread.",
23
+ "lean": ["Mixture.eMix_expectation_eq_one", "Mixture.eMix_expectation_le_one"]},
24
+ {"status": "proved", "text": "More profit always means more evidence, never less.",
25
+ "lean": ["Mixture.eMix_strictMono", "Mixture.logEStd_strictMono"]},
26
+ {"status": "measured", "text": "Real trades are not perfectly normal, and their spread has to be estimated. So we test it: on 42,000 made-up strategies with no edge, it called one a real edge 0.15% of the time, and at most 1.9% in the hardest case.",
27
+ "link": "VALIDATION.md"}
28
+ ]
29
+ },
30
+ {
31
+ "id": "bar",
32
+ "mood": "happy",
33
+ "title": "I say “real” only past a bar of 40",
34
+ "flip": "Luck would need a 1-in-40 fluke to fool me. Same for “losing”.",
35
+ "plain": "Because the evidence averages 1 when there is no edge, it rarely climbs to 40 by luck. Calling “real edge” at 40 on one side and “losing” at 40 on the other leaves at most a 5% chance of a false call in total. That is the 95% in the range you see.",
36
+ "formula": "P(e ≥ 40) ≤ 1/40",
37
+ "claims": [
38
+ {"status": "proved", "text": "The range and the verdict are the same statement: “real edge” exactly when the whole 95% range is above zero.",
39
+ "lean": ["Mixture.verdict_iff_range_clears_zero", "Mixture.range_eq_not_rejected"], "python": "evidence.range_of"},
40
+ {"status": "proved", "text": "The code's search for the range's ends always finds the right point.",
41
+ "lean": ["Mixture.boundary_exists_unique", "Mixture.boundary_bracket", "Mixture.boundaryCode_spec"], "python": "evidence.boundary"},
42
+ {"status": "proved", "text": "Evidence that averages at most 1 reaches 40 at most 1 time in 40 (Markov's inequality).",
43
+ "lean": ["Selection.evalue_markov"]},
44
+ {"status": "proved", "text": "When your total is positive, the “losing” side can't pass at the same time.",
45
+ "lean": ["Mixture.eMix_neg_le_one"]}
46
+ ]
47
+ },
48
+ {
49
+ "id": "versions",
50
+ "mood": "shrug",
51
+ "title": "Every version you tried raises the bar",
52
+ "flip": "Tried 40 settings and kept the best? Then I need 40 times more evidence.",
53
+ "plain": "Try enough versions of a strategy and one will look great by luck alone. So every version you tried and every strategy you show me count as one family, and the bar grows with the family. Versions you tried but didn't show count as zero evidence.",
54
+ "formula": "one strategy, N versions: real edge ⟺ e ≥ 40 · N",
55
+ "claims": [
56
+ {"status": "proved", "text": "Across any number of strategies and versions, false “real edge” calls stay under 5% of all such calls on average, however the strategies relate to each other (the e-BH procedure).",
57
+ "lean": ["Selection.ebh_fdr", "Selection.ebhSet_fdr"], "python": "evidence.ebh"},
58
+ {"status": "proved", "text": "The code's procedure is exactly that one, whatever order ties come in, and a version you didn't show is never selected.",
59
+ "lean": ["Selection.ebhSet_selfConsistent", "Selection.card_ebhSet", "Selection.pyTop_eq_ebhSet", "Selection.notMem_ebhSet_of_nonpos"],
60
+ "python": "evidence.ebh"},
61
+ {"status": "proved", "text": "For one strategy and N versions the rule is simply: real edge exactly when the evidence reaches 40 × N.",
62
+ "lean": ["Selection.single_strategy_rule_q05"], "python": "evidence.describe_family"},
63
+ {"status": "proved", "text": "Keeping the best of N versions and dividing its evidence by N is still fair evidence.",
64
+ "lean": ["Selection.best_div_isEValue", "Selection.best_of_N_markov"]},
65
+ {"status": "measured", "text": "Keep the best of 1,000 random versions without saying so and I'm fooled 51% of the time. Say you tried 1,000 and it falls to 0.2%. That's why I ask.",
66
+ "link": "VALIDATION.md"}
67
+ ]
68
+ },
69
+ {
70
+ "id": "anytime",
71
+ "mood": "happy",
72
+ "title": "Checking again and again doesn't fool me",
73
+ "flip": "Look after every trade if you like. I was built for that.",
74
+ "plain": "An ordinary 95% range checked after every trade is wrong about half the time, because sooner or later luck pokes out of it. Mine holds at every check at once: under luck the evidence behaves like a fair game that can't drift up, and such a game rarely ever reaches 40.",
75
+ "formula": "P(the evidence ever reaches 40, at any number of trades) ≤ 1/40 (Ville's inequality)",
76
+ "claims": [
77
+ {"status": "proved", "text": "Ville's inequality: a fair or losing game that starts at 1 ever reaches 40 at most 1 time in 40.",
78
+ "lean": ["Anytime.ville_super", "Anytime.ville"]},
79
+ {"status": "proved", "text": "Under luck, the evidence is such a game.",
80
+ "assumes": "Independent normal trades.",
81
+ "lean": ["Anytime.martingale_mix", "Anytime.supermartingale_mix"]},
82
+ {"status": "proved", "text": "So the 95% range holds at every number of trades at once, and a strategy with no edge, or a losing one, is ever called real at most 1 time in 40.",
83
+ "assumes": "Independent normal trades with a known spread.",
84
+ "lean": ["Anytime.anytime_range_95", "Anytime.above_zero_false_alarm"]},
85
+ {"status": "measured", "text": "With the spread estimated from your trades and a check every 10 trades: wrongly “real” 0.5% of the time for ordinary trades, 1.9% for streaky ones.",
86
+ "link": "VALIDATION.md"},
87
+ {"status": "choice", "text": "No range before 30 trades. A spread measured on a handful of trades is too unsteady to promise anything (see my mistakes below)."}
88
+ ]
89
+ },
90
+ {
91
+ "id": "shapes",
92
+ "mood": "shrug",
93
+ "title": "Lopsided and streaky trades get a higher bar",
94
+ "flip": "Many small wins and a rare huge loss fool simple tests. So I get stricter, never looser.",
95
+ "plain": "When losses are rarer but bigger than wins, a few dozen trades can look great before the big loss arrives. When trades move together, they carry less information than their count suggests. In both cases I raise the bar.",
96
+ "claims": [
97
+ {"status": "proved", "text": "The lopsidedness adjustment can only raise the bar and widen the range, never the opposite.",
98
+ "lean": ["Mixture.skewShift_nonneg", "Mixture.skew_never_raises_e", "Mixture.range_with_skew_contains"], "python": "evidence.skew_shift"},
99
+ {"status": "proved", "text": "The streakiness factor never narrows the range: it stays between 1 and 10.",
100
+ "lean": ["Counting.serialFactorOf_mem", "Counting.ar1Floor_mem"], "python": "evidence.serial_factor"},
101
+ {"status": "proved", "text": "Its main ingredient is right: when trades follow each other like an AR(1) process, the spread of the average grows by (1 + r)/(1 − r) as trades pile up.",
102
+ "lean": ["Counting.variance_mean_ar1", "Counting.tendsto_ar1Ratio"]},
103
+ {"status": "measured", "text": "What the guards buy, measured: martingale-like records were called real 37% of the time at 30 trades before them, now 0%; lopsided ones 3.4%, now under 1%.",
104
+ "link": "VALIDATION.md"},
105
+ {"status": "choice", "text": "Nothing is called real before five losing trades: an unseen loss could be any size."}
106
+ ]
107
+ },
108
+ {
109
+ "id": "pictures",
110
+ "mood": "idle",
111
+ "title": "Everything around the verdict adds up",
112
+ "flip": "The coin flips, the streak odds, the money sentence and the paired trades are checked too.",
113
+ "plain": "Around the verdict sit smaller calculations: the coin-flip picture, the odds of a losing streak, the money sentence, the live-vs-backtest range, the drawdown, the time in the market and the pairing of exchange fills into trades. Where they are exact, they are proved; where they are estimates, they say so.",
114
+ "claims": [
115
+ {"status": "proved", "text": "“Your average trade is up but you lost money: your losing trades were bigger” is said only when it is a theorem, when every trade's percent and money point the same way. Otherwise I point at fees and costs.",
116
+ "lean": ["Selection.losers_bigger"], "python": "judge._money_vs_percent"},
117
+ {"status": "proved", "text": "The live-vs-backtest range adds a 97.5% range on each side, so it holds at 95% even when you check your live trades often.",
118
+ "lean": ["Selection.diff_range_union_bound", "Selection.worse_false_alarm"], "python": "compare.compare_runs"},
119
+ {"status": "proved", "text": "The range on your win rate is Wilson's: exactly the rates a fair test wouldn't reject.",
120
+ "lean": ["Selection.wilson_score_inversion", "Selection.wilson_roots"], "python": "evidence.wilson_bounds_count"},
121
+ {"status": "proved", "text": "My own forecasts are graded with the Brier score, which rewards honest probabilities, and compared with always guessing the usual rate, not just with a coin flip.",
122
+ "lean": ["Selection.brier_expected", "Selection.brier_proper", "Selection.brier_base_rate_beats_coin"], "python": "web.ledger.Ledger"},
123
+ {"status": "proved", "text": "The losing-streak odds are exact: the code's calculation equals the probability over every possible sequence of wins and losses.",
124
+ "lean": ["Counting.pStreakAtMost_eq", "Counting.maxRun_le_iff"], "python": "judge._p_streak_at_most"},
125
+ {"status": "proved", "text": "The coin-flip picture is a fair test: if your trades were pure luck (symmetric around zero), a result this good would show up no more often than the picture says.",
126
+ "lean": ["Counting.orbit_count_le", "Counting.randP_valid", "Counting.coinFlip_valid"], "python": "judge._coin_flips"},
127
+ {"status": "measured", "text": "The page draws 1,000 random flips, not all of them, so its count carries a little noise. And it is only a picture: the verdict comes from the evidence."},
128
+ {"status": "proved", "text": "Pairing an exchange's buys and sells first in, first out keeps every coin and every dollar accounted for.",
129
+ "lean": ["Counting.fifo_coins", "Counting.fifo_pnl_pieces", "Counting.fifo_flat"], "python": "fills.round_trips"},
130
+ {"status": "proved", "text": "The worst drop and the trades since your peak are computed exactly as defined.",
131
+ "lean": ["Counting.worstDrop_spec", "Counting.lastPeak_is_last_high"], "python": "judge._drawdown"},
132
+ {"status": "proved", "text": "Time in the market is the true length of the union of your trades' time spans.",
133
+ "lean": ["Counting.sweep_eq_volume"], "python": "judge._exposure"},
134
+ {"status": "measured", "text": "The drawdown odds, “faded” and the live-vs-backtest odds come from resampling your own trades: estimates, and labelled as such."}
135
+ ]
136
+ }
137
+ ],
138
+ "limits": [
139
+ {"status": "measured", "text": "Real trades are not normal, their spread is estimated, and they can depend on each other. The proofs above assume the textbook case; for real trades I measure instead. That is where 0.15%, and 1.9% at worst, come from.",
140
+ "link": "VALIDATION.md"},
141
+ {"status": "unchecked", "text": "I can't see your code. A backtest that peeks at future prices or skips fees hands me trades that look better than they were. When the shape looks too good, I flag it and give you a prompt to have the code checked."},
142
+ {"status": "unchecked", "text": "The past is not the future. A real edge can stop working, which is why I check whether it has faded, and why you should come back."},
143
+ {"status": "choice", "text": "When the evidence decides fastest is a rule of thumb: tuned for about 100 trades, though the exact best is near 112. It costs 0.03%.",
144
+ "lean": ["Mixture.logEStd_scale_free"]}
145
+ ],
146
+ "mistakes": [
147
+ {"date": "2026-10-14", "title": "I showed a range from just two trades.",
148
+ "impact": "At 2 trades it missed the true average 9% of the time, against the 5% I promise; for someone checking after every trade from 2 on, about 13%.",
149
+ "found": "The Lean proof assumes a known spread, so a test checked what happens with an estimated one.",
150
+ "fix": "No range before 30 trades, where verdicts start. From there it holds (about 2.2%)."},
151
+ {"date": "2026-10-14", "title": "My coin-flip sentence could contradict my verdict.",
152
+ "impact": "A passing card could read “3 copies ended higher … a pass needs 1 or fewer … It passes.” The picture and the verdict are different tests.",
153
+ "found": "A test checking the coin-flip proof's conditions against 400 random records.",
154
+ "fix": "The bar is now said as odds, never as a count of copies. The verdict itself was never affected."},
155
+ {"date": "2026-10-14", "title": "I blamed trade sizes when fees were the cause.",
156
+ "impact": "“Your losing trades were bigger” was said even when every trade was up in percent and down in money, which happens when fees are in the money but not in the percent.",
157
+ "found": "Proving the sentence showed exactly when it is true.",
158
+ "fix": "It is said only when it is a theorem. Otherwise I say the percent and the money point opposite ways, and why."},
159
+ {"date": "2026-10-14", "title": "One blank cell broke my money sentence.",
160
+ "impact": "A single empty percent cell turned the average into “n/a”, and could invent a mismatch between percent and money, or hide a real one.",
161
+ "found": "A test checking the money sentence against its theorem.",
162
+ "fix": "Only trades with both numbers count now."},
163
+ {"date": "2026-10-14", "title": "My report card set the bar too low.",
164
+ "impact": "I called my own forecasts “better than guessing” when they beat a coin flip. But always guessing the usual rate beats a coin flip too.",
165
+ "found": "Proving what the Brier score rewards.",
166
+ "fix": "I now compare my forecasts with always guessing the usual rate."},
167
+ {"date": "2026-10-14", "title": "A rounding tolerance misread large amounts.",
168
+ "impact": "A strategy exactly back at its high could read “0.000000 USDT below your peak, 2 trades ago”.",
169
+ "found": "A test comparing the drawdown with its definition.",
170
+ "fix": "The tolerance is now relative to the size of the numbers."},
171
+ {"date": "2026-10-14", "title": "A shortcut far below zero.",
172
+ "impact": "Where the evidence is tiny, a one-term shortcut overstated it by up to 1.5%. It never changed a verdict.",
173
+ "found": "A test comparing the code with the proved formula.",
174
+ "fix": "The exact formula is used there now."},
175
+ {"date": "2026-10-14", "title": "I said my tuning was the best at 100 trades.",
176
+ "impact": "It is best near 112 trades; at 100 the difference is 0.03%.",
177
+ "found": "Trying to prove the claim showed it is only approximately true.",
178
+ "fix": "Worded as the rule of thumb it is."},
179
+ {"date": "2026-10-14", "title": "Three kinds of records fooled me in testing.",
180
+ "impact": "Lopsided records were wrongly called real 3.4% of the time, martingale-like ones 37% at 30 trades, streaky ones checked often 3.4%.",
181
+ "found": "The validation run on records whose truth is known.",
182
+ "fix": "A higher bar for lopsided trades, five losses before “real”, and a streakiness floor. Now under 1%, 0% and 1.9%."}
183
+ ]
184
+ }