← The note
Reproducibility

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.

Data

Source bars come from data.binance.vision/data/futures/um/, which is public and needs no credentials. Six series are built per contract: 30-minute OHLCV klines as the primary series; 1-minute klines, reduced on arrival to high, low and their minute, retained only to resolve the order in which each 30-minute high and low printed; funding events at their true settlement timestamps, carrying funding_interval_hours per event; 30-minute mark price klines, which value the funding cash flow; 30-minute premium index klines, the basis input for the exogenous arm; and 5-minute open interest and long-short ratios.

The funding interval is read per event rather than assumed, because Binance moved several contracts from eight-hour to four-hour funding during 2025 and any constant would be wrong for part of the claim period. The published mark price is used rather than a substituted contract open, and the premium index is used directly rather than deriving basis from spot closes, so no second instrument enters the panel.

Prices are never forward-filled through a missing bar, no entry is allowed while a required input is missing and a signal creates an order for the immediately following bar only. If that bar is absent the order is cancelled rather than carried to a later observed open. A position open across a data gap stays open, and indicators must complete their full warm-up again before another signal.

Every non-price input is aligned as-of the bar close, so a value at bar t was knowable at the close of bar t:

# open interest, long-short ratios: last observation at or before the bar close
panel = panel.join_asof(metrics, on="ts", strategy="backward")

# funding: the last settled rate at or before the close, and separately the rate
# whose settlement falls inside the bar, which is what the cost model charges
panel = panel.join_asof(funding.select("ts", "rate"), on="ts", strategy="backward")
panel = panel.with_columns(
    fund_settle_rate=pl.col("rate").filter(
        pl.col("settle_ts").is_between(bar_open, bar_close)
    )
)

Universe

The venue is Binance USDT-margined perpetual futures and the bar frequency is 30 minutes. Eligibility is decided from the ranking quarter alone: USDT-quoted, positive traded volume on every day of October to December, not an index product such as BTCDOMUSDT or DEFIUSDT and not a 1000X redenomination of a contract already included. 141 contracts pass. Ranking is by median daily notional over the quarter, and the universe is the top 30 and the bottom 30.

d = screen.filter(pl.col("rank_days_traded") >= pl.col("rank_days").max())
d = d.sort("median_daily_notional", descending=True)
keep = [s for s in d["symbol"] if s not in excluded]   # index + redenomination rules
H, L = keep[:30], keep[-30:]
ratio = median(notional[H]) / median(notional[L])      # 19.99 against a floor of 10

Realised medians are $138.8M per day in stratum H against $6.9M in stratum L, and the thinnest contract in the frozen universe still traded $5.0M a day. Contracts between the two strata are not traded and exist only to define the ranks. The separation floor of 10 was registered before the screen ran, and a stratification below it would have been abandoned rather than narrowed until it passed.

File existence is not evidence of trading. Eligibility and the end of a contract's history both use positive traded volume, and exchange padding after the last real trade is discarded. Contracts that die inside the claim period stay in the universe with their deaths preserved, which is why MATICUSDT's data ends at its POL migration and EOSUSDT's at its Vaulta rename rather than at a delisting sweep. A universe built from the exchange's current contract list would have deleted both.

Families

Arm C is eight named indicator families, all price-only: ATR, EMA, MACD, PPO, RSI, RSI_LEVEL, SMA and STOCHK. Arm M-price is nine mechanism families, also price-only: volcomp_break, tsmom, session_mom, weekend_rev, volclimax, btc_relstr, gap_fade, range_rev and keltner_trend. Arm M-exo is eleven families that additionally read funding, open interest, basis, order flow or positioning: funding_capit, liq_snap, basis_fade, oi_breakout, flow_absorb, smart_follow, lev_stress, funding_settle, cvd_exhaust, retail_contra and oi_elastic.

Each arm-M family carries a one-paragraph mechanism statement naming the counterparty, the reason that counterparty transacts under constraint, and the observable that proxies it. The statements are serialised, SHA-256 hashed and frozen with the rest of the configuration before any bar is read. They are reconstructions written from the original design record rather than a contemporaneous sealed document, which is the first limitation in the note.

btc_relstr reads a second instrument's price and is therefore price-only but cross-asset. It stays in M-price and is flagged. Arm statistics are equal-weight means across an arm's families, so the eight-against-nine imbalance does not require dropping a family.

A strategy is the tuple (arm, family, variant, threshold, max_hold, stop). Lookback and reward-to-risk are in-sample tunable knobs rather than separate strategies, and are re-selected in every window. Every family in both arms draws from one shared library, copied verbatim from the generator that produced both source corpora and hashed.

TRANSFORMS = ["base", "slope", "normalized_price", "roc", "bias", "volZ", "accel",
              "disFromMedian", "quant_stretch", "rank_resid", "fold_dev"]          # 11
CONFL      = ["RSIge40", "RSIge50", "Pge0.7", "Pge0.8", "BW_filter", "pi", "vr",
              "kurtosis", "kurtosis10", "skew", "skew0.75", "atr_pct", "atr_pct0.8",
              "burstfreq", "TinyBody", "NoNewLowGreen", "RangeSpike", "YesterdayPeak",
              "DeadFlat10", "InsideBar", "SameDirection", "TopOfRange",
              "VolContraction", "EMAHug"]                                          # 24

VARIANTS = TRANSFORMS + CONFL      # 35
THR      = [1.0, 1.5, 2.0]         # entry threshold, in z units
MAXH     = [24, 96, 288]           # 12 hours, 2 days, 6 days
SLS      = [1.0, 2.0, 4.0]         # stop, in entry-ATR units
N_CONFIGS = 35 * 3 * 3 * 3         # 945 attempted configurations

945 attempts per family, per contract, per window, in both arms. That is the equal search budget: equal attempted configurations rather than an equal number of knobs, and it deliberately differs from the source corpora where catalog families ranged from 12 to 8,932 configurations. Every attempt is persisted with its reason, including the ones that never trade, so a family that fails often cannot look less searched than one that trades constantly. A signal event occurs when the current desired state is nonzero and the preceding state was neutral or opposite, so a persistent condition cannot re-fire on every bar.

The realised score table is 28,576,800 rows across 60 contracts, 28 families and 18 windows, which is 1,680 contract-family units each carrying 945 configurations.

Execution

The information set and the trade sequence, in order: features from completed bar t, desired state after bar t, entry at the open of bar t+1, barriers active from t+1 onward, exit at the earliest of stop, target or the open after H complete held bars.

Each strategy holds at most one position, a same-direction signal while occupied is ignored, and an opposite signal exits and reverses at the next observed open, charging two fills. Barriers are ATR-scaled rather than fixed percentages, because across a liquidity-stratified universe a fixed 1% stop is a different risk distance on a major than on a thin alt, which would confound the liquidity axis with a volatility axis.

Both-hit bars are resolved by walking the 1-minute path and taking whichever barrier was crossed first. Resolving instead by which extreme printed first is a different question, and it was measured to bias results by up to 12.5 bp per trade on a driftless martingale, larger than the entire cost model. Where a single minute spans both barriers the order is genuinely unknown at this resolution, and the stop is taken first.

Costs and funding

gross = direction * (exit_price / entry_price - 1.0)
fee   = 5e-4 * fills                                   # 5 bp per fill, both legs
slip  = (0.5 * spread_month + 0.5 * 0.5 * spread_month) * fills
fund  = sum(direction * rate_k * qty * mark_k for k in settlements_held)
net   = gross - fee - slip - fund                      # reconciles to 1e-9 per row

Fee is a flat 5 basis points per fill, the Binance USDT-margined VIP0 taker rate, and it does not vary by contract. Spread is built from daily/aggTrades as the median inter-trade bounce, the absolute relative price change between consecutive trades that flip aggressor side, taken over the 8th and the 22nd of each month with up to three forward fallbacks when a day is absent. Each fill pays half the measured spread plus half again as a slippage allowance, so a round trip pays one full measured spread plus a 50% impact allowance.

bookTicker, the direct quote feed, is unusable here because the archive carries it only from 2023-05-16, which would leave the first third of the claim period uncosted. High-low estimators are prohibited: measured on this desk, Corwin-Schultz overstates BTC spread by a factor of 568 at these bar sizes, which means it reads volatility rather than spread.

The estimator was validated before freeze against an independent bookTicker measurement of BTCUSDT on 15 July 2023 and reproduces it exactly at 0.0330 basis points, over 440,267 trades and 172,496 aggressor flips. That agreement is a blocking gate rather than a note.

A contract-month with traded bars and no measurable spread is a data failure and drops the contract. A contract-month with no traded bars is structural absence and carries no spread, because it carries no position. Across the frozen universe the realised split is 3,607 measured contract-months, 593 structurally absent and zero failures.

Cost componentTreatment
Fee5 bp per fill, flat, both legs
SpreadMeasured per contract-month, half charged per fill
SlippageHalf the measured spread again per fill
FundingObserved signed event at its settlement bar, valued at the published mark price

Positive funding debits longs and credits shorts, and funding is assessed on the position held immediately before the settlement timestamp. A new entry at that timestamp neither pays nor receives; a position carried into and exited at that timestamp does. Position quantity is fixed at strategy capital divided by the raw entry price for the life of the trade. Funding is never a strategy input in either arm, and in arm M-exo the cost path and the feature path are computed from separate columns with an explicit gate asserting that no cost column reaches the feature matrix.

Walk-forward

Windows are calendar-anchored: a 12-calendar-month in-sample block followed by a 3-calendar-month out-of-sample block, advancing three months, eighteen blocks covering 1 January 2022 to 30 June 2026.

def quarters():
    out, d = [], date(2022, 1, 1)
    while d < date(2026, 7, 1):
        nxt = next_quarter(d)
        out.append((date(d.year - 1, d.month, 1), d, nxt))   # IS start, OOS start, OOS end
        d = nxt
    return out                                               # 18 windows

All contracts share window boundaries, so a contract that lists later contributes to fewer windows and one that dies contributes to fewer still. Bar-anchored windows, which both source corpora used, give every contract a different calendar and make week clusters incoherent across the panel.

Inside each in-sample block all 945 attempts are scored, lookback and reward-to-risk are selected by in-sample Sharpe, and the selected knobs are frozen and applied once to the out-of-sample block. A trade whose holding interval crosses the boundary is excluded from the in-sample score entirely rather than truncated. Indicator warm-up may read bars before a block starts, because those bars were already known, but warm-up rows never contribute profit and loss to the block being scored.

Matching

An eligible strategy-window has at least 20 closed in-sample trades across at least 4 distinct calendar weeks and a finite in-sample Sharpe. Matching happens inside one (contract, window) cell, never across a liquidity stratum, greedily by ascending absolute in-sample Sharpe distance and without replacement on both sides, with ties broken by the two strategy ids so the pairing is a pure function of the input table.

order_vals = np.sort(m_sharpe)                  # candidates within the caliper by
lo = np.searchsorted(order_vals, c_sharpe - 0.05, side="left")   # binary search, not a
hi = np.searchsorted(order_vals, c_sharpe + 0.05, side="right")  # |A| x |B| matrix
# pairs taken in ascending |distance|, without replacement, ties broken by (c_sid, m_sid)

The caliper is 0.05 annualised Sharpe units, with a single registered widening to 0.10 available if the overlap gate failed. It was not needed: 6,812,171 pairs matched at 0.05, and the registered overlap gate requires at least 200 matched pairs in every window and at least 60% of the smaller arm matched overall, evaluated per stratum.

The matcher reads in-sample columns only. Running it against a table with the out-of-sample columns deleted produces an identical pairing hash, which is a blocking gate rather than a claim. Balance after matching is reported as the standardised mean difference in in-sample Sharpe, and anything above 0.10 would be a failure of the matching implementation rather than a result.

Strategy ids are made unique across families before any join. sid restarts at zero for every contract-family, so arm C's EMA family and arm M's momentum family both carry ids 0 to 944. Joining on sid alone is ambiguous, and on a two-contract corpus it turned 119,178 pairs into 8,580,816 rows.

Inference

Paired daily net return series are constructed for both arms from the matched set. Within each fixed out-of-sample window the bootstrap resamples Monday-to-Sunday UTC calendar-week clusters, and a quarter boundary splits a calendar week so its days on either side form separate window-week fragments. Every contract, family, strategy and arm inside a sampled fragment travels together, which preserves cross-sectional dependence without changing a window's cluster count. The realised data carries 250 fragments across 1,642 trading days.

rng = np.random.default_rng(20260810)
stat = []
for _ in range(10_000):
    draw = rng.integers(0, n_clusters, n_clusters)   # week fragments, not rows
    stat.append(diff[rows_in(draw)].mean())
point, (lo, hi) = observed_diff, np.percentile(stat, [2.5, 97.5])

10,000 replicates, seed 20260810, percentile interval. Row-wise resampling is prohibited, since overlapping forecast windows do not create independent observations. Two-sided p-values are twice the smaller bootstrap tail probability with the finite-replicate correction (count + 1) / (B + 1), capped at one, before entering Benjamini-Hochberg. No multiplicity adjustment applies to the single registered primary contrast, and the four secondaries form one BH family at 5%.

The decay slope is the one statistic that cannot cluster on weeks. Its observation is one pair-window, one in-sample Sharpe against one out-of-sample return, so it clusters on the window itself and gets 18 clusters rather than 250. The intervals in the note are correspondingly wide, and that is a property of the estimand rather than a choice.

Gates

Twenty blocking gates run before the analysis, and each one carries a planted defect proving it can fail. A gate that passes both its clean and its broken input cannot fail and is reported as broken. All twenty pass and all twenty planted defects are caught.

They cover the frozen configuration hash, the universe and its separation ratio, family liveness in both directions, regime gates measurably filtering the candidate set, future truncation leaving earlier signals unchanged, feature leakage, first-touch resolution on the 1-minute path, funding sign, the cost identity, the 945-attempt count, window ordering and disjointness, the matcher's blindness to out-of-sample columns, overlap, post-matching balance, arm-label symmetry, a planted future outcome, a within-window action rotation, clone controls and the null corpus below.

Two further checks are worth stating. The engine earns zero on a driftless path and matches a first-touch reference within 0.08 basis points per trade. The ledger identity gross - fee - slippage - funding = net holds per row and in aggregate, and after the non-finite funding defect described in the note it now rejects non-finite values explicitly before comparing magnitudes, because a non-finite residual passes a magnitude comparison and certifies a corrupt ledger as clean.

The null corpus

The shared rule vocabulary carries a self-reference artifact: a signal derived from the traded series, combined with barriers scaled by that same series' lagged volatility, does not earn zero on a driftless corpus. Both arms lose roughly 550 basis points from the artifact alone there. It is a property of the vocabulary rather than a defect in the engine, so testing that the level is zero would fail a design that is in fact correct.

The registered test is therefore on the difference, which is the estimand:

# same engine, same vocabulary, same matching, driftless GBM paths
D_null = matched_difference(null_corpus)
# 72.92 bp, 95% CI [-15.11, 166.60], 51,124 matched pairs, covers zero

That interval covers zero, which is what the gate requires. It is also 33% of the observed effect at the point estimate, with an upper bound above the observed effect's lower bound, which is why the note reports the headline and the artifact together rather than the headline alone.

Every chart on the note is drawn from one published JSON file, exported from these artifacts. The full pipeline, the freeze bundle and the per-trade ledger are not distributed from this page. If you want them, email and ask.

Back to the note.

← Do trading strategies need an economic rationale?