← 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 the public Binance archive.

Venue, universe and dates

Binance USDT-margined perpetual futures only, with universe selection dated 30 June 2022. A contract is eligible when, using only information available that day: it is a USDT-quoted, USDT-margined perpetual; its underlying is a single coin, which excludes the three index-underlying contracts DEFIUSDT, BTCDOMUSDT and ALLUSDT; it traded genuine positive volume on at least 80% of calendar days in the twelve months ending on the selection date, with its first observation inside 7 days of that window's start; it traded on at least 55 days in April, May and June 2022; and it was still alive at the selection date, its last positive-volume day falling within 7 days of it.

That last rule exists because the quarter count alone does not catch a contract that died inside the ranking window. Four contracts cleared the day count while not trading at the selection date and are excluded: ICPUSDT and SCUSDT were suspended from 16 and 17 June 2022, while DODOUSDT and AKROUSDT last traded on 27 May 2022 and still scored 57 days in the quarter.

Eligible contracts are ranked by median daily quote-asset volume over 1 April to 30 June 2022, which for linear perpetuals is already notional in USDT. The screen yields 105 contracts, whereupon the frozen ladder takes the top 10, 25, 50 and 100 and stays strictly nested, leaving 5 contracts of margin above the largest rung.

Reference points at the selection date: rank 1 is BTCUSDT at $13,246M median daily notional, rank 10 XRPUSDT at $367M, rank 25 AAVEUSDT at $160M, rank 50 ENJUSDT at $46M and rank 100 NKNUSDT at $16M.

eligible = (
    single_coin
    & (volume_days_12m / calendar_days_12m >= 0.80)
    & (first_obs <= window_start + timedelta(days=7))
    & (quarter_trading_days >= 55)
    & (last_positive_volume_day >= SELECTION_DATE - timedelta(days=7))
)
ranked = median_daily_quote_volume[eligible].sort_values(ascending=False)
rungs = {n: ranked.index[:n].tolist() for n in (10, 25, 50, 100)}

File existence is not evidence of trading, so eligibility, the ranking and the end of a contract's history all use positive traded volume, with exchange padding after the last real trade discarded. Contracts that die after the selection date stay in the universe and go to cash at their last traded bar.

The archive index rather than the live contract endpoint is the master symbol list, because the live endpoint omits 333 contracts that have since been delisted. Two ticker renames are stitched after verifying price continuity at the handoff, MATIC to POL in September 2024 and EOS to A in May 2025, both 1:1 migrations. A redenomination, where notional changes by a constant factor as in the 1000X contracts, is not a rename and is never stitched.

In-sample data for the first walk-forward window covers July 2022 through June 2023, then the out-of-sample period runs 1 July 2023 to 31 July 2026, giving 37 complete non-overlapping calendar months. Every bar used anywhere postdates the selection date, and the selection date sits that late deliberately: a universe chosen on June 2022 liquidity and then used to estimate ranking statistics on earlier history would condition those estimates on survival that was not knowable at the time.

Data and calendar

Two timeframes, 15-minute and 1-hour, both on the UTC clock grid, treated as separate cells and never pooled. Bars come from the Binance public archive for all 105 contracts: 15-minute and 1-hour klines as the primary series, 1-minute klines retained only to reconstruct the order in which each bar's high and low occurred, funding-rate events at their true settlement timestamps and mark-price klines for funding valuation.

Panels never forward-fill a price or a return through a missing bar. A signal creates an order for the immediately following bar only, so 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 applies the gap barrier rules at its first later observed bar, whereupon indicators complete their full warm-up again before another signal can fire.

Built panels span 1 June 2022 to 31 July 2026 and carry 15,015,087 fifteen-minute bars and 3,757,437 hourly bars across 105 contracts, with 481,644 funding events. Data gates cover duplicate timestamps, monotonicity, OHLC identities, positive prices, volume-based trading history, onboarding dates, gap counts, one-minute-to-bar aggregation agreement and funding coverage against source, all passing with zero failures at a median bar gap of 0.000%.

Spreads

Spreads are measured rather than estimated from bars. The Corwin-Schultz high-low estimator was tested and rejected for this venue during an earlier build: it returned 18.76 bps for BTCUSDT where bookTicker quotes give 0.0330 bps, an overstatement of 568 times, while it also moved with bar frequency, giving 8.9 bps on 15-minute bars against 18.8 bps on 1-hour bars. An estimator that depends on the sampling frequency of the bars it is computed from is measuring volatility.

Measured instead is the bid-ask bounce, the median absolute price change between consecutive aggregate trades. Trades alternate between bid and ask, so that median recovers the spread directly. Validated on BTCUSDT for 15 July 2023, it returned 0.0330 bps against a bookTicker ground truth of 0.0330 bps across 8.3M quote updates.

# two sampled days per symbol-month, streamed and discarded
changes = agg_trades["price"].diff().abs()
changes = changes[changes > 0]
spread_bps = 1e4 * changes.median() / agg_trades["price"].median()

Two days per symbol-month are sampled, the 7th and the 21st, with fallback to nearby dates when one is absent, which comes to about 10,500 symbol-days and roughly 26 GB streamed. A sampled day yields no estimate when the contract printed fewer than 500 trades or fewer than 100 non-zero price changes, which happens in 589 of 5,146 symbol-months, concentrated in the illiquid tail.

A symbol-month without an estimate takes the 90th percentile of that symbol's own measured spreads, while a symbol with no estimate in any month takes the 90th percentile of its universe rung for that month. Both fallbacks are deliberately wide, so absent data can only make trading look more expensive, and 88.6% of symbol-months are priced directly.

The archive publishes bookTicker from May 2023, overlapping most of the out-of-sample period, and it was registered as a stratified validation of the measured series under gate 16. That gate was never run for this study, so the ground-truth comparison is inherited from the earlier build rather than repeated here.

The strategy library

Eight structural families with ten frozen numeric configurations each, 80 strategies, generated under seed 20260808 and inherited verbatim: bollinger_reversal, breakout, mean_reversion, momentum, moving_average_crossover, rsi_reversal, trend_following and volatility_breakout.

Numeric parameters are frozen properties of a strategy here rather than tunable knobs, because this study performs no parameter search, leaving the 80 strategies as units of replication.

The inherited library's implementation filter had been calibrated on a 10-instrument panel, so coverage was re-measured on this universe before freezing, reading activity, holding period, instrument coverage and turnover while never reading profit. It required no replacement: 8,398 of 8,400 strategy-asset pairs produce at least one candidate at 1-hour bars, and 8,400 of 8,400 do at 15-minute bars.

Signals, exits and intrabar execution

Every rule computes a condition in {-1, 0, +1} from completed bars only, and a candidate fires only when the strategy's view actually inverts. A neutral bar is neither a signal nor a reset, so the view is held until the opposite side fires.

# a candidate exists only on a genuine directional inversion
view = condition.replace(0, np.nan).ffill()
candidate = view.ne(view.shift(1)) & view.notna() & view.shift(1).notna()

A position closes at its take-profit, at its stop-loss or when the strategy produces an opposite signal on that asset, with no maximum hold. Wilder ATR(14), known at entry, fixes barrier distances for the life of the trade, at a 2.0 ATR stop and a 3.0 ATR target for every strategy.

That pair came from an activity-only calibration over the first in-sample window on the focal rung, reading holding period, exit-reason mix and occupancy while never reading profit. Occupancy decided it: across the barrier grid, mean concurrent positions per strategy on the top-50 ran from 1.96 at a 1.0 ATR stop to 11.33 at 3.0 ATR.

Selected instead is the pair giving 6.39 mean concurrent positions against a focal capacity of 5, binding on 34.7% of bars, while keeping barrier exits the majority of outcomes at 58.5% against 41.5% flips, a median hold of 6 bars and zero truncation.

Bar OHLC alone cannot say which barrier executed first when both lie inside one bar, so the engine reconstructs the intrabar path from 1-minute bars as open → first recorded extreme → second recorded extreme → close. Where the high and low fall in the same minute and the order is unresolved, the stop executes first, which happens on 0.13% of bars at the focal cell. A gap through a stop fills at the first available open, whereas a gap through a target fills no better than the target.

# events sharing one bar timestamp resolve in a fixed order
for ts in timestamps:
    apply_funding(ts)
    apply_gap_barriers(ts)
    apply_opposite_signal_exits(ts)
    release_slots(ts)          # exits always precede entries
    allocate_entries(ts)       # a slot freed at this open is available now
    apply_intrabar_barriers(ts)

The invariant candidate stream

This is the spine of the study and the reason the execution modes are comparable at all. With fixed per-slot capital, an unlevered book and no modelled impact, a trade's entry price, exit price, exit reason, per-unit costs, per-unit funding and per-unit result do not depend on what else the portfolio holds. Every candidate's outcome is therefore computed once, after which every execution mode, universe size, capacity and ranking rule is a scheduling policy over the same frozen object.

A candidate is generated for every signal transition on every strategy-asset pair, independent of occupancy and of any portfolio, carrying asset, strategy, family, timeframe, signal and entry timestamps, direction, entry price, entry ATR, the declared strength value, the precomputed exit timestamp, exit price and exit reason, plus per-unit gross, fee, slippage, funding and net result in both accounting units.

That those outcomes are bit-identical across every arm, capacity and rung is a blocking gate rather than an assumption. The stream holds 3,199,677 candidates at 1-hour bars and 13,253,337 at 15-minute, with 16,452,374 outcome rows in total.

One limitation follows directly: because costs are flat in notional, the model does not charge a concentrated book more than a diffuse one for the same trade, which is why the exposure-neutral diagnostics and the size-dependent slippage sensitivity are registered.

Costs, funding and accounting

5 bps taker, 2 bps maker, 2 bps slippage, plus half the measured spread on any crossing fill. A crossing fill pays taker plus slippage plus half spread, whereas a take-profit rests as a limit order and pays the maker fee with no slippage and no spread. A stop crosses and entry fills always cross, with costs applied to absolute filled notional.

def fill_cost(notional, crosses, spread_bps):
    fee = TAKER_BPS if crosses else MAKER_BPS
    slip = SLIPPAGE_BPS if crosses else 0.0
    half_spread = 0.5 * spread_bps if crosses else 0.0
    return abs(notional) * (fee + slip + half_spread) / 1e4

Funding uses observed Binance funding events at their true settlement timestamps rather than an assumed schedule, and a missing expected funding observation is a data failure rather than a zero, whereupon positive funding debits longs and credits shorts. Funding is assessed on the position held immediately before the settlement timestamp, so a new entry at that timestamp neither pays nor receives while a position carried into and exited at it does.

Every ledger row preserves gross, fee, slippage, funding and net separately, whereupon the identity gross - fee - slippage - funding = net holds within 1e-9 at row level and in aggregate, reconciling to 0.0 across all 16,452,374 candidate outcomes.

Two accounting units are maintained throughout and neither substitutes for the other: normalised R, the ATR-normalised per-trade unit used for selection quality, then return on notional, used for portfolio profit and loss at the position weight. Idle capital earns nothing and returns are arithmetic on fixed capital, with nothing compounding.

The arms and the ranking rules

All arms use fixed capital, equal capital per open position, an unlevered book and no compounding. Unused capital stays idle and active positions are never rescaled to replace a flat or rejected one.

ArmSlotsWeightSelection when slots are scarce
Independent baselineU, the universe size1/Unever scarce, every candidate is taken
RandomK1/Kuniform draw among competitors
Signal strengthK1/Khighest declared strength
Historical expectancyK1/Khighest in-sample expectancy for the asset
Information ratioK1/Khighest in-sample information ratio for the asset

Capacities are K in {1, 3, 5, 10} and universe sizes U in {10, 25, 50, 100}. Portfolios are always single-strategy: one strategy competes across assets for its own slots, and signals from different families never share a queue. An arm's reported return is the equal-weight mean across the 80 single-strategy portfolios, a 1/80 book that stays unlevered.

The random arm is the mean over 100 independent replicates under a generator seeded per cell, window and replicate, since a single draw is noise and is never reported as the random arm.

Signal strength is declared per family and fixed before the run, always in a scale-free unit so a high-volatility asset cannot outrank a low-volatility one for reasons unrelated to conviction, since raw percentage distances would encode a volatility tilt and are forbidden.

FamilyStrength
bollinger_reversalexcess of the absolute log-price z-score over the band width, in standard deviations
breakoutdistance of the close beyond the prior channel extreme and its buffer, in entry ATR
mean_reversionexcess of the absolute z-score over the entry threshold, in standard deviations
momentumexcess of the volatility-scaled lookback return over its threshold
moving_average_crossoverabsolute fast-minus-slow EMA separation, in entry ATR
rsi_reversaldistance of RSI beyond its bound, in RSI points
trend_followingexcess of the volatility-scaled distance from the EMA over its threshold
volatility_breakoutvolatility-scaled magnitude of the directional lookback return

Historical expectancy is the mean in-sample net result per executed candidate under the independent baseline, in normalised R, requiring at least 20 executed in-sample candidates for that strategy-asset pair. The information ratio divides that mean by its own in-sample standard deviation and requires the same minimum plus a non-zero standard deviation. Assets below either threshold rank last, ordered by frozen universe index, with no shrinkage, no pooling and no model.

stats = (
    is_trades.groupby(["strategy", "symbol"])["net_r"]
    .agg(["mean", "std", "count"])
    .query("count >= 20")
)
expectancy_rank = stats["mean"].sort_values(ascending=False)
info_ratio_rank = (stats["mean"] / stats["std"]).sort_values(ascending=False)

How often the threshold binds was measured before the freeze from candidate counts alone, with no profit read. At the focal rung a median of 48 of 50 assets are rankable per strategy-window, and 79.8% of strategy-windows carry at least 10 rankable assets, with the shortfall concentrated in the slow families, where rsi_reversal clears 20 in-sample trades in 33% of cells against 91% for moving_average_crossover.

Slot mechanics

Rejection is immediate-or-cancel, so a candidate is eligible for a slot only at its own entry bar, and if no slot is free at that instant it is permanently rejected and never enters later. A persistent queue was considered and rejected because a candidate that waits enters at a stale signal and a different price, making it a different trade and breaking the claim that only trade selection differs between arms.

Within one timestamp, exits and slot releases are processed before entry allocation, after which competing candidates are sorted by the arm's ranking rule and slots fill in rank order until capacity is reached, with ties broken on the lower frozen universe index. Occupancy may never exceed K and deployed capital may never exceed 100%, both enforced as blocking gates.

When K is greater than or equal to U the constraint can never bind and the portfolio arm must produce a ledger identical to the independent baseline, a positive control and a blocking gate.

Simultaneous arrivals are the minority channel, with more than one candidate arriving at the same timestamp on 9.8% of arrival events and more than five on 2.4% of bars. Rejection therefore happens mostly because earlier trades still hold the slots, the realistic mechanism and the one the design intends to test. It also means ranking acts on a narrow slice, given that rejection by an already-full book is rank-blind.

Walk-forward schedule

Every window is 12 calendar months in-sample followed by 1 calendar month out-of-sample, advancing one month at a time, so July 2023 is the first out-of-sample month and July 2026 the last, giving 37 windows.

In-sample data serves exactly one purpose, estimating the per-asset ranking statistics, with no in-sample parameter search and no in-sample model selection anywhere. Rankings are frozen at the window boundary and applied for one out-of-sample month, and a position opened under one month's ranking is never re-decided when the ranking rolls.

The longest indicator warm-up may use bars from before a block starts, because those bars were already known, although warm-up rows never contribute profit or loss to the block being scored.

A 12-month in-sample window makes the ranking statistics noisier than a longer one would, and both ranked arms are built from exactly those statistics, so the design tilts toward finding that ranking does not beat random. That was registered as a known property rather than discovered afterwards, hence the longer-window sensitivity.

Inference

The engine constructs paired daily net returns on fixed capital for every arm at every cell, on UTC calendar days: 1,127 days across 162 week clusters. Resampling is over Monday-to-Sunday UTC calendar-week clusters, where a month boundary splits a week into separate window-week fragments and every asset, strategy and arm inside a sampled fragment travels together.

# paired, week-clustered, common random numbers across arms
rng = np.random.default_rng(20260810)
for b in range(B):
    weeks = rng.choice(clusters, size=len(clusters), replace=True)
    draw = np.concatenate([day_index[w] for w in weeks])
    stat[b] = 1e4 * (ret_a[draw].sum() - ret_b[draw].sum())
point, lo, hi = stat.mean(), *np.percentile(stat, [2.5, 97.5])

Paired week-clustered bootstrap replicates under seed 20260810 build the primary interval, where in every replicate the statistic is the cumulative arithmetic return difference over the resampled out-of-sample history. 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 entering Benjamini-Hochberg.

No multiplicity adjustment applies to the single registered primary contrast. D_cap and D_rank form one Benjamini-Hochberg family of two, whereas the registered surface of 288 contrasts forms a separate family corrected within itself, of which 193 are significant.

A realised trade books its whole net result on the day it exits. Median holds are 6 hours at 1-hour bars and 1.2 hours at 15-minute, so what matters for week clustering is how often a trade straddles a week boundary: 6.1% at 1-hour and 1.5% at 15-minute. Those few introduce mild dependence between adjacent clusters, which slightly understates uncertainty, an effect the entry-day attribution variant bounds at 1.9 bp on D_cap.

Descriptive diagnostics rather than bootstrap units, the 37 monthly differences are not 37 independent strategies. Sharpe may be reported descriptively and never selects a configuration, ranks an arm or decides a conclusion.

The zero-drift null corpus

Gate 20 builds thirty independent zero-drift corpora, each of 50 synthetic assets carrying the realised volatility and spread of one real contract, and runs each through the entire focal-cell pipeline: signals, candidates, first-passage exits under the real cost model, in-sample ranking statistics, then the arms. These series contain no return predictability of any kind, so anything a ranked arm earns on them was manufactured by the machinery.

# volatility and spread preserved on purpose; drift removed
for seed in range(1, 31):
    rng = np.random.default_rng(seed)
    for symbol in rung_50:
        sigma = realised_vol[symbol]        # per-bar, from the real panel
        log_ret = rng.normal(loc=0.0, scale=sigma, size=n_bars)
        synthetic[symbol] = np.exp(np.cumsum(log_ret)) * first_price[symbol]
    run_full_pipeline(synthetic, spreads=measured_spreads)

Preserving the volatility profile is the point. Barriers are ATR-scaled while costs are a fixed fraction of notional, so a low-volatility asset pays proportionally more cost per unit of risk. Past expectancy and signal strength both correlate with volatility, and volatility persists, so a ranking rule can select cheaper-per-unit-risk trades without forecasting anything at all.

A null corpus that equalised volatility would have hidden precisely the mechanism worth testing.

Registered as the pass condition was that no ranked arm's advantage over random has a 95% interval entirely above zero across the seeds. Signal strength clears that bar on 8 of 30 corpora containing no signal, so the gate fails. Expectancy does not trip the registered condition while still sitting at the 87th percentile of its own null, an empirical p of 0.133 computed as the share of null draws at or above the real estimate.

Diagnostics that separate deployment from selection

Four diagnostics exist specifically to stop a cash-drag artifact being scored as skill, and are mandatory wherever a headline number appears: net R per candidate offered, net return per unit of average deployed capital, opportunity utilisation with the rejection rate, then average and maximum occupancy with average idle capital.

# denominator is the frozen candidate stream, identical for every arm
net_r_per_offered = taken["net_r"].sum() / offered_count
binding_rate = rejected_book_full / arrivals   # asset-block rejections counted separately

The binding rate is the share of candidate arrivals rejected because the book already held K positions, with rejections caused by the one-position-per-asset rule counted separately and not treated as binding, and the independent baseline is included as the control on an expectation of near zero.

A capacity-1 book holding one position at a time out of a 100-name universe will show a flattering Sharpe that is almost entirely idle cash, so reporting fixed-capital return alongside the deployed and per-offered-candidate measures keeps that visible.

For the focal cell a full per-position ledger, 2,303,532 rows across all five arms, is written for inspection. Producing these diagnostics required a plain-Python reimplementation of the scheduler, which agreed with the numba kernel on every taken set across all five arms and 80 strategies with zero disagreements.

Gates and check results

Eight of the specification's 21 blocking gates are implemented. The six pre-out-of-sample gates run on in-sample data only and all pass.

GateResult
K ≥ U identity against the independent baselinemax absolute difference 3.47e-17
foresight ranking dominates random and anti-ranking+12.53 versus -1.29 versus -15.13
random seed stabilityspread 0.042, some 330 times smaller than the foresight gap
deployed capital never exceeds fixed capitalmax mean deployed 95.2%
one position per asset per strategyenforced, 7,244 taken of 40,824 offered
planted defect is detectedidentity diverges by 0.514 rather than zero

Beyond those, the numba first-passage walk reproduces the frozen Python reference exactly across 59,214 candidates with all six exit reasons exercised, and an adversarial test suite added 18,672 more comparisons with zero mismatches. Gate 14 scheduler parity passes with zero disagreements, whereas gate 20 fails, and that failure is what withdrew the ranking claim.

Two checks after the first freeze, one of the engine and one of the statistics, found four defects that changed the numbers: a slot released for any trade closing inside its own entry bar, so 7.4% of trades occupied no capacity; candidates accepted on any positive ATR, letting denormal risk denominators produce results as large as 1e95; a book that emptied at every month boundary; and a tie-break on parquet row order rather than the frozen universe index. Correcting all of it moved the headline from -10,459 bp to -8,090 bp with every sign and conclusion unchanged.

Thirteen gates remain unimplemented, none of which supports a claim the note makes. A pre-freeze pilot run exists and is quarantined rather than reported, since the specification's original gate ordering required running the scheduler before the freeze, which necessarily exposed out-of-sample numbers. The ordering was corrected so that every scheduler-dependent gate now runs on the in-sample period only.

freeze             f575878c86937ab63067aeb98c9b8fb7723e5ec4a0a64c4112cc6f1ce77d8d6b
superseded         5ecb4866f65386ddb18358d991b2eb541a3111362a4afb5646caf9cca62eb3b9
superseded         bd72f21f1180bb32e2fd17fed02b2a616ffe9f2dfe6f2e91a60385e67de04d39
specification      1f7687a381a256e3f4d6ec0502bc8069705c4d70d0f6b01527107a8f6c0744b0
universe           429fcc435cd8ab32ca98480e6d011ee590ccfd79541b2431af8489128c81a3e5
data manifest      add2fc3c1fc460d0eb74ba08d14ae829593f779fcd6ab02dfe890e27e73ab0dd

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 the public Binance archive, although the production scripts, the data build, the numba scheduler, the analysis and the figure build are not attached. Email daniel@daru.finance and I'll send them over.

← Back to the note