Every number in the note, and where it came from
Enough method to check the arithmetic by hand, or to rebuild an equivalent study from public Binance data.
Venue, universe and dates
The venue is Binance USDT-margined perpetual futures and the universe selection date is 31 December 2022. Eligible contracts had to be quoted and margined in USDT, carry at least 24 months of genuine traded-volume history by that date, trade during October to December 2022 and not duplicate another contract's exposure. Eligible contracts were then ranked by median daily notional traded over October to December 2022, using only information available by the selection date, and the top six were frozen: BTCUSDT, ETHUSDT, DOGEUSDT, XRPUSDT, BNBUSDT and SOLUSDT.
File existence is not evidence of trading. Eligibility and end-of-history both use positive traded volume, and exchange padding after a contract's last real trade is discarded.
The primary out-of-sample period runs 1 January 2023 through 30 June 2026: fourteen complete, non-overlapping three-month windows. July 2026 is excluded as an incomplete quarter.
Data and calendar
All six contracts use hourly UTC bars from their Binance futures launch through 30 June 2026, with official one-minute klines retained to reconstruct the order in which each hour's high and low occurred, and funding events and hourly mark-price opens retained at their real timestamps. The observed launches are BTC on 8 September 2019 17:00 UTC, ETH on 27 November 2019 07:00, XRP on 6 January 2020 08:00, BNB on 10 February 2020 08:00, DOGE on 10 July 2020 09:00 and SOL on 14 September 2020 07:00.
The panel keeps common calendar boundaries rather than an intersection mask, so one market's gap never deletes an hour from the other five, and prices are never forward-filled through a missing bar. A signal creates an order for the immediately following UTC hour only, and if that hour is absent the order is cancelled rather than carried forward. A position open across a gap stays open, and indicators must complete their full warm-up again before another signal.
An hourly row may be replaced by a one-minute aggregate or a current REST response only when the source row is demonstrably corrupt or sparse, and every replacement stores its source response, reason, timestamp and hash in a repair manifest. Fourteen high/low rows and 240 absent hourly rows were repaired under that rule before the data freeze, leaving a panel of 333,424 hourly rows and no gaps.
The six strategy families
Every rule computes a desired state after completed bar t, and a signal event fires only when the desired state is nonzero and the preceding state was neutral or opposite, so a persistent condition cannot emit a signal every hour.
# a desired state becomes a signal only on a fresh transition
state = np.where(fast_ema - slow_ema > threshold * atr, 1,
np.where(fast_ema - slow_ema < -threshold * atr, -1, 0))
prior = np.roll(state, 1)
signal = (state != 0) & ((prior == 0) | (prior == -state))| Family | Entry rule | Numeric domains |
|---|---|---|
| EMA trend | Fast EMA above slow EMA by a threshold in ATR units | fast 12/24/48, slow 48/96/168/336, threshold 0/0.25/0.50 ATR |
| Donchian breakout | Close beyond the prior rolling channel, current bar excluded | lookback 24/48/96/168/336/720 h, buffer 0/5/10 bp |
| TS momentum | Sign of the L-hour log return past a volatility-scaled threshold | lookback 24/72/168/336/720, threshold 0/0.25/0.50/1.00 |
| RSI reversal | Wilder RSI outside a bound pair, faded | length 6/14/28/56, bounds 20/80, 30/70, 40/60 |
| Bollinger fade | Close outside a band, faded | lookback 24/48/96/168, width 1.0/1.5/2.0/2.5 sd |
| Vol compression | Channel break while short vol sits under a fraction of long vol | short 12/24, long 72/168/336, ratio 0.40/0.60/0.80, break 12/24/48 |
The compression rule's defining property was measured before any out-of-sample exposure: its eligible signal events had to actually occur during the 2021-2022 calibration period in every market, because a filter that never filters is a label rather than a strategy.
Exits, execution and the intrabar path
The six families differ only in entry logic and share one exit architecture, which stops an exit search from smuggling in new families. Entry is at the open of hour t+1 after a signal on completed bar t, one position per sleeve, same-direction signals ignored while occupied, and an opposite signal reverses at the next open and pays two fills. Otherwise the position leaves at the earliest of stop, target or maximum hold.
Wilder ATR(14) known at entry fixes barrier distances for the life of the trade, and the exit domains are common to every family: stops at 1.0, 1.5, 2.0 or 3.0 entry ATR, with reward-to-risk at 1.0, 1.5, 2.0 or 3.0 and maximum holds of 12, 24, 48, 96 or 168 hours.
Hourly OHLC cannot say which barrier executed first when both sit inside one candle, so the engine reconstructs each hour's path from one-minute bars as open, first recorded extreme, second recorded extreme, close. If both extremes fall in the same minute and the order is unresolved the stop executes first. A gap through a stop fills at the first available open, while a gap through a target fills no better than the target.
# events sharing one hourly timestamp resolve in a fixed order
for hour in window:
apply_funding(hour) # carried into this timestamp
apply_gap_barriers(hour)
apply_reversal(hour) # closes old, opens new, two taker fills
apply_max_hold(hour)
apply_entry(hour)
apply_intrabar_barriers(hour, minute_path[hour])Search budget and eligibility
Each family receives exactly 64 attempted configurations in every strategy-market-window. A deterministic discrete Latin-hypercube generator with seed 20260809 samples the signal and exit domains, rejects duplicate or invalid combinations and writes one canonical 64-row list per family. The lists are generated once, reused in every market and window, serialized canonically and hashed before any out-of-sample exposure, and the search manifest declares n_searched = 2304, which is 64 attempts in each of the 36 family-market cells.
An attempt is eligible to win the parameter block only if it closes at least 24 trades across at least six distinct calendar weeks. Failed, non-finite and ineligible attempts stay in the score table and still count toward 64. Parameter ties break on lower turnover, then lower canonical index; picker ties break on lower aggregate axis-block turnover, then the frozen market or strategy order.
Walk-forward schedule
Every outer window is twelve months of in-sample history followed by three months out of sample, advancing by three months. The first nine months fit each cell's numeric parameters, the final three months replay the selected configuration untouched, and only those held-forward scores decide the market and the strategy axis.
for window in windows: # 14 non-overlapping OOS quarters
fit = window.is_slice[:9_months] # choose numeric parameters per cell
axis = window.is_slice[9_months:] # replay the chosen config, untouched
picks = {
"market": best_mean_over(axis, group="market"), # 6 strategies on it
"strategy": best_mean_over(axis, group="family"), # on 6 markets
}
apply_once(picks, window.oos_slice) # frozen configs, frozen decisionsA trade whose holding interval crosses a block boundary is excluded from the earlier block's score, and no label, exit or cash flow straddles the parameter block into the axis-selection block or in-sample into out-of-sample. Indicator warm-up may read bars before a block starts, because those bars were already known, but warm-up rows never contribute P&L to the block being scored. The selected configuration is not refit on the full twelve months after the axis decision.
Costs, funding and accounting
Every entry, reversal leg, stop, target and time exit is a taker fill paying 5 basis points of fee and 2 basis points of slippage on absolute filled notional, with position quantity fixed at sleeve capital divided by the raw entry mid for the life of the trade. Funding uses observed Binance events at their real timestamps, positive funding debiting longs and crediting shorts, assessed on the position held immediately before the settlement timestamp.
# a buy fills adversely, and the gap is booked as slippage rather than hidden in gross fill = raw_mid * (1 + 0.0002) if side > 0 else raw_mid * (1 - 0.0002) fee = 0.0005 * abs(quantity * fill) slippage = abs(quantity * (fill - raw_mid)) funding_cost = direction * funding_rate * quantity * mark_open # positive when paid net_pnl = gross_pnl - fee - slippage - funding_cost # holds to 1e-9
A missing expected funding observation is a data failure, not a zero and not an invitation to impute. Every ledger row keeps gross P&L, fee, slippage, funding and net P&L as separate columns, and the identity above reconciles across the full corpus to within 5.55e-17 return units.
| Component, all 36 OOS cells | bp of fixed sleeve capital |
|---|---|
| Gross P&L | +27,454.6 |
| Taker fees | -168,288.4 |
| Slippage | -67,315.4 |
| Funding | -2,147.0 |
| Net P&L | -210,296.2 |
Portfolio arms
| Arm | Axis decision from the held-forward block | Out-of-sample portfolio |
|---|---|---|
| No axis selection | none | all 36 cells at 1/36 |
| Market picker | best equal-weight mean across six strategies | six strategies on that market at 1/6 |
| Strategy picker | best equal-weight mean across six markets | that strategy on six markets at 1/6 |
All sleeve returns use fixed capital, one unit of maximum absolute position and no compounding. Flat, missing and ineligible sleeves stay in cash and active sleeves are never renormalized, so each arm carries the same 100% maximum gross budget while realized exposure and turnover are outcomes rather than settings. Sharpe may be reported descriptively from the non-overlapping daily book, but it never selects a configuration, chooses an arm or decides a conclusion.
Inference
The engine builds paired daily net returns for all three arms, and within each fixed out-of-sample window the bootstrap resamples Monday-to-Sunday UTC calendar-week clusters, with a quarter boundary that splits a week leaving two separate fragments. Every market, strategy and arm inside a sampled fragment travels together, which preserves cross-sectional dependence without changing a window's cluster count.
draws = paired_week_cluster_bootstrap(
daily_by_arm, windows=14, n_boot=10_000, seed=20260809,
)
point = cumulative(market_picker) - cumulative(strategy_picker) # +193.9 bp
low, high = np.percentile(draws, [2.5, 97.5]) # -7868.3, +8194.8
ruling = "no_reliable_winner" if low < 0 < high else "winner"Each secondary two-sided p-value is twice the smaller bootstrap tail probability with the finite replicate correction (count + 1) / (B + 1), capped at one, before the two picker-versus-no-selection p-values enter Benjamini-Hochberg at 5%, where both returned 0.7513, and no multiplicity adjustment applies to the single registered primary contrast, and the fourteen per-window differences are descriptive diagnostics rather than bootstrap units.
Decomposition
The decomposition uses the 36 unweighted cell-level out-of-sample net returns in every complete window, after per-cell parameters were frozen, and computes orthogonal fixed-effect sums of squares on the balanced market by strategy by window cube.
ss = orthogonal_fixed_effect_ss(cube) # market x strategy x window
shares = {k: v / ss["total"] for k, v in ss.items()}
# market 0.46%, strategy 1.94%, interaction 6.98%, time 1.78%, residual 88.84%
boot = [shares_of(resample_weeks(cube)) for _ in range(n_boot)]
critical = np.percentile(max_abs_centered_error(boot), 95) # 4.92 ppThese are descriptive sums-of-squares shares, not population variance components; calling them random-effect variance estimates would assert an exchangeability that six deliberately selected markets do not have. A component is called dominant only when its point share is largest and its simultaneous intervals against both other cross-sectional components exclude zero. Interaction qualifies: market minus interaction runs -11.45 to -1.61 pp and strategy minus interaction -9.96 to -0.12 pp, while market minus strategy spans -6.40 to +3.44 pp.
Clones, transfer and sensitivities
Clone groups are determined inside each window from the axis-block signed daily position vectors of the already-selected configurations, concatenated across all six markets. Exact duplicates always merge, near clones merge by complete linkage only when every cross-pair correlation is at least 0.95, negative correlation never merges inverse strategies, and a pair needs at least 60 finite daily observations with nonzero variance before a correlation is even defined. Six singleton groups formed in every window, so the clone-collapsed arm equals the primary arm.
Transfer is tested by replaying each window's 36 frozen configurations on all six target markets across the axis-selection block and the matching out-of-sample block, with identical costs, funding, sizing and execution. The transfer stage runs once per window rather than pooled, because a pooled invocation would let later in-sample periods participate in a selection graded on earlier out-of-sample periods.
The registered sensitivity list, fixed before results, is an expanding launch-history parameter block, a constant positive 1 bp funding event, a clone-collapsed strategy axis, transfer scored by expectancy instead of total P&L, six leave-one-market-out picker panels, and the July 2026 partial window as descriptive only. No sensitivity was added after the primary result became visible, and none can replace the headline.
| Estimate | Market minus strategy | 95% interval |
|---|---|---|
| Registered primary | +193.9 bp | -7,868.3 to 8,194.8 |
| Expanding parameter history | -1,596.7 bp | -8,488.4 to 5,322.1 |
| Constant 1 bp funding | +1,359.1 bp | -6,012.0 to 8,808.5 |
| Market omitted | Market minus strategy | Market picker | Strategy picker |
|---|---|---|---|
| BTC | +928.4 bp | -4,979.0 | -5,907.4 |
| ETH | +345.3 bp | -3,320.1 | -3,665.4 |
| DOGE | +4,064.3 bp | -3,646.1 | -7,710.4 |
| XRP | +940.3 bp | -4,278.4 | -5,218.7 |
| BNB | -4,494.8 bp | -6,474.6 | -1,979.7 |
| SOL | +465.2 bp | -4,795.8 | -5,260.9 |
Pre-registration gates
Nineteen blocking gates had to pass after the last relevant code change and before any out-of-sample bar was read, each with a planted defect proving the gate can fail. They cover hash agreement across data, strategies, search, costs and analysis; real-data signal, direction and activity checks for all six families on all six markets; a compression filter that measurably filters; future truncation leaving every earlier signal, candidate and completed trade unchanged; no funding, cost, high/low timing or outcome field reaching a signal; Python-versus-Rust execution parity on real slices; one-minute path order changing a planted both-hit result; funding signs and counts reconciling with source data; the net-P&L identity holding per trade and in aggregate; all 64 attempts persisting including failures; ordered, purged and disjoint block boundaries; a transposed planted panel swapping the two pickers exactly; arm sleeve counts and weights with no cash renormalization; planted clone controls producing the expected clusters; a planted future outcome materially improving the optimizer and being detected; a within-window action rotation preserving turnover exactly while destroying the planted relationship; failed gates propagating a nonzero exit code; and harness readiness reporting the registered checks as runnable rather than silently partial.
The last of them is the null-corpus gate: on a test-only zero-drift random-walk corpus, across 30 independent seeds, no family's in-sample-selected out-of-sample mean had a 95% bootstrap interval entirely above zero.
Verification results
- Corpus: 31,770,479 candidate trade rows, 16,826 selected out-of-sample trades.
- Cost identity: reconciles to 5.55e-17 return units.
- Engine parity: 25,259 trades across 72 real-data cases, maximum absolute P&L disagreement 1.94e-16.
- Same-bar leak screen: -0.0001 correlation, against +0.0051 for the forward return.
- Boundary check: zero trades crossing or falling outside their registered out-of-sample window.
- Backtest guard: zero automated failures across the walk-forward, ledger, cost and week-clustered uncertainty checks.
Two automated warnings were resolved by checking the data directly rather than by argument. The generic check read the shared in-sample and out-of-sample timestamp as a zero-day purge, when the engine purges at trade level, which the direct boundary count settles. The Sharpe-oriented inputs stage of the same check reported low power and a noise-scale spread across families, which does not touch a P&L-decided comparison but does rule out presenting any single family here as a discovered edge.
pre-OOS freeze b7c102a3a6cbbbb0752e174cc610ed7c56a0f9fe8f94fd42ef184fa16ccd12cb engine manifest f2e70f0c2334751029be76673e7f8a429413944ef8026af149c1c9730f717a78 results 4cdf4ac1b42420f480528f3db6323b2e12b7d21ac160d99712e08983dc35451d data manifest 34a26b3380223fe19afb73cf3d63a60d9e991fba82917e42adc8e7d66adae1e3
Any change after an out-of-sample result is visible requires a new run root, new hashes and a written amendment. Two amendments exist, both post-freeze and both presentational: one removed a duplicate column rename in the window export, and one restored a shortened figure label, wrote window boundaries as timezone-naive datetimes and made the check runner return failure when any requested check fails. Neither touched data, configurations, trades, returns, portfolios, inference or the ruling.
The charts in the note are drawn from the published result tables, which are the same tables the numbers in the text come from.
Requesting the full pipeline
This page is enough to check every number in the note by hand, or to rebuild an equivalent study from public Binance data. The production scripts, the data build, the Rust engine, the analysis and the figure build are not attached. Email daniel@daru.finance and I'll send them over.

