← 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 same public and licensed data.

Data and universe

Two panels, both at 15-minute and one-hour bars from January 2021 to July 2026. Crypto is Binance USDT-margined perpetual OHLCV with the measured quoted spread per symbol-month and every funding event at its settlement timestamp. Equities are split-adjusted consolidated bars restricted to NBBO-eligible trades in the regular session, with odd lots and off-exchange trade reports excluded, keyed by a permanent security identifier rather than the ticker and carrying a measured time-weighted NBBO spread per bar.

The universe was fixed on 31 December 2022 from October to December 2022 liquidity alone, taking the ten most liquid names in each market. MATIC stops trading in September 2024 and is left absent afterwards rather than replaced, since a replacement chosen later would use information about which contracts survived.

MarketInstruments
CryptoBTCUSDT, ETHUSDT, DOGEUSDT, XRPUSDT, BNBUSDT, SOLUSDT, MATICUSDT, LTCUSDT, CHZUSDT, ETCUSDT
EquitiesTSLA, AAPL, NVDA, AMZN, MSFT, AMD, META, GOOGL, NFLX, XOM

Rule library

Fourteen families, thirty parameterisations each, for 420 rules. For every family the generator enumerates the grid below, drops invalid and duplicate settings, then takes 30 space-filling configurations with seed 20260804. The library is serialised canonically and hashed, and every runner refuses to start if the hash differs.

FamilySignal at the close of bar tGrid
Trend followingSide of a moving trend, if its strength clears a thresholdlookback 12, 24, 48, 96; strength 0, 0.5, 1 vol units; hold 1, 2, 4, 8, 16
Mean reversionAgainst a standardised deviation from a rolling meanlookback 12, 24, 48, 96; entry z 0.5, 1, 1.5, 2; hold 1, 2, 4, 8
BreakoutClose beyond the prior rolling close extremelookback 6 to 96; buffer 0, 5, 10 bp; hold 1, 2, 4, 8
MomentumSign of a lagged cumulative return above a strengthlookback 3 to 96; strength 0, 0.25, 0.5, 1 vol units; hold 1, 2, 4, 8
Volatility expansionShort-horizon direction when short vol exceeds long volshort 6, 12; long 24, 48, 96; ratio 1.25, 1.5, 2; direction 1, 3; hold 1, 2, 4
Volatility contractionShort-horizon direction during compressionshort 6, 12; long 24, 48, 96; ratio 0.4, 0.6, 0.8; direction 1, 3; hold 1, 2, 4
Moving-average crossoverCompleted-bar fast/slow EMA crossfast 3 to 24; slow 12 to 96, fast below slow; hold 1 to 16
Channel breakoutClose beyond a mean plus or minus an ATR channellookback 12 to 96; width 0.5, 1, 1.5, 2 ATR; hold 1, 2, 4, 8
RSI reversalBuy oversold, sell overboughtRSI 6, 14, 28; bounds 20/80, 30/70, 40/60; hold 1, 2, 4, 8
Bollinger reversalAgainst a close outside the bandlookback 12, 20, 48, 96; width 1 to 2.5 sd; hold 1, 2, 4, 8
Donchian breakoutClose beyond the prior high/low channellookback 12 to 96; hold 1 to 16; optional one-bar confirmation
Volume confirmationMomentum only when normalised volume clears a ratiomomentum 3 to 24; volume lookback 12 to 96; ratio 1, 1.5, 2; hold 1, 2, 4, 8
Opening-range breakoutBreak of the first bars of the market dayrange 1, 2, 4 bars; buffer 0, 5, 10 bp; latest entry 0.5, 0.75 of the day; hold 1, 2, 4
Time basedFixed session bucket and directionsession fraction 0, 0.25, 0.5, 0.75; long or short; weekday mask; hold 1, 2, 4, 8

A signal is an entry transition: it fires when a rule first enters a long or short condition, while repeated bars in the same condition are not new signals. Inside each 24-month training window a rule is dropped only if it has fewer than 120 completed trades, trades fewer than 5 instruments or produces exactly the same signal set as a lower-numbered rule, and no profitability statistic is ever used to drop one.

Seven volatility-expansion settings never trade, because their required ratio of short to long volatility is out of reach for those window lengths, and the duplicate check compares signals without the holding period, so rules that differ only in how long they hold collapse to the lowest-numbered one. On average 392 to 396 of the 420 rules are eligible in a given window.

Execution

The decision is made after bar t completes. The position enters at the open of bar t+1 and exits at the close of the h-th held bar, where h is the rule's holding period in observed bars. There are no stops or targets, so the order of a bar's high and low never decides an exit.

Each rule-instrument pair holds at most one candidate position, and a signal that arrives while one is open is ignored; a model that skips a trade never creates a replacement.

entry = signal_idx + 1
exit  = signal_idx + hold                       # close of the hold-th held bar
gross_bps = direction * log(close[exit] / open[entry]) * 1e4
net_bps   = gross_bps - spread_bps - fee_bps - slippage_bps + funding_bps

Features

Sixty-two price and volume features computed at bar t within each instrument's own history: lagged returns and momentum at 1 to 24 bars, high-low range, candle body and wick fractions, close-to-open gap, volatility, moving-average distance, position in range and distance from highs and lows at 6 to 96 bars, plus normalised volume. Seven more are added: trend strength (absolute 24-bar moving-average distance over 24-bar volatility), session position and day of week as sine and cosine pairs, bars left in the market day and the log close of bar t.

The rule contributes its family as a one-hot vector, its parameters scaled to their grid position with missing-parameter flags, the signal's direction and strength, the intended holding period and the bars since its previous signal. Recent performance is the mean, sum, wins and losses of the rule's last 20 completed trades in that instrument, alongside the mean, wins and losses of its last 100 completed trades across instruments, counting only trades that exited at or before the decision bar.

# strategy-level trailing 100: only trades that have already exited count
for p in trades_sorted_by_entry:
    while exits[cursor].exit_ts <= p.signal_ts:
        ring.push(exits[cursor].net_bps); cursor += 1
    p.recent_100_mean = ring.mean()

Execution data (the next bar's open, every cost column and funding) is stored on each row and removed from the model matrix. Session position and bars remaining use the day's bar count, which on a day with missing bars reflects the gap; those days carry between 0.002% and 0.33% of trades depending on the cell, and dropping them moves the best book from +0.748% to +0.761%.

A trade that entered inside the training window but exits after it is excluded from both training and the out-of-sample set, so it also drops out of the 20-trade history, which leaves that history slightly stale but never ahead of the decision.

Models and search

Three families, each fitted separately for the net-return target and the profit target, each with 30 fixed configurations. The regression target is clipped at its 0.5% and 99.5% training quantiles and standardised. Failed, constant and non-converging configurations count toward the 30 and cannot be chosen.

FamilyImplementationGrid range over the 30 configurations
LinearRidge for pure L2, elastic net otherwise; logistic elastic net for profitalpha 0.0001 to 0.03; L1 ratio 0 to 1
Boosted treesLightGBM, 150 trees, no row or column subsamplingdepth 2 to 6; leaves 4 to 31; learning rate 0.0054 to 0.091; min child 104 to 4,645; L2 0.0014 to 68
Neural netGELU MLP, 6 epochs, batch 8,192depth 1 to 3; width 16 to 128; learning rate 0.0001 to 0.0028; weight decay 1.5e-7 to 0.0094

Inside each window the training rows split chronologically 80/20 into inner training and validation, with 16 bars of clock time removed before the split. The net-return models are selected on validation Spearman rank IC and the profit models on validation ROC AUC, ties going to the lower configuration number. The winner is refitted on the full 24 months with its preprocessing fitted on those rows only.

Walk-forward

Forty-three monthly out-of-sample windows, January 2023 to July 2026, in each of four market-timeframe cells, for 172 windows. Each window trains on exactly the preceding 24 calendar months, using only trades whose entry and exit both fall inside them, then predicts every eligible trade entered in the next calendar month once. One model per market, timeframe, family and target pools all rules and instruments.

for cell in ["equity_1h", "equity_15m", "crypto_1h", "crypto_15m"]:
    for month in months("2023-01", "2026-07"):
        is_rows  = trades(entry >= month - 24M, exit < month, eligible_rules(month))
        oos_rows = trades(month <= entry < month + 1M, eligible_rules(month))
        for family in ("linear", "gbt", "mlp"):
            for target in ("net_return", "profit"):
                best = select(family, target, is_rows, n_configs=30)   # 80/20 inner split
                model = refit(best, is_rows)
                oos_rows[f"pred_{family}_{target}"] = model.predict(oos_rows)

That gives 1,032 selected models from 30,960 attempted configurations, 30,394 of which fitted successfully, and 239,452,104 out-of-sample predictions over 39,908,684 candidate trades.

Trading policies

Every rule-instrument pair gets the same base weight, w0 = 1 / (S × 10) with S the number of eligible rules in the window, so taking every signal can never exceed 100% gross exposure.

baseline          w = w0
simple filter     w = w0 if at least 2 of { vol_24, trend_strength_24, vol_norm_24 }
                               exceed the instrument's median over the 24 training months
ML filter         w = w0 if prediction > 0 bp (net return) or > 0.5 (profit)
ML sizing         q = rank of prediction in the window's sorted validation predictions
                  w = w0 * (0.25 + 1.5 * q)
ML ranking        at each signal timestamp keep ceil(20%) of the new signals, at least one,
                  by prediction; w = w0 * min(5, n_candidates / n_kept)

Simple-filter medians come from unique market bars in the training window, not from trade rows, so a bar with many signals is not counted many times. A filter that takes no trade in a month leaves that month in cash.

Costs

MarketSpreadFee per fillSlippage per fillFunding
CryptoHalf the measured spread at entry plus half at exit5 bp2 bpEvery actual event from the entry bar to the exit bar, signed
EquitiesHalf the measured per-bar spread at entry plus half at exit0.5 bp0None

Positive funding debits longs and credits shorts, and funding events are assigned to the bar that contains them, so a trade entering at the open of a settlement bar pays that settlement. Funding stays small next to fees: taking every crypto one-hour signal nets -0.28% of capital in funding over the run against 174.59% paid in fees. Costs are identical for every policy, which is why the per-trade cost bar in the note barely moves while the gross bar does.

Inference and claim bars

Economics come from the concatenated out-of-sample ledger and a non-overlapping daily return series, with P&L booked at each trade's exit. The note reports summed daily returns, since compounding pins every crypto book near -100%; the registered net-return bar uses the compounded series.

Each contrast is a policy minus its comparator on the daily series. Uncertainty is a paired bootstrap resampling whole weeks, 10,000 replicates with seed 20260804, and Benjamini-Hochberg runs across all 144 contrasts: 4 cells, 18 model policies and 2 comparators, failures included.

weeks  = daily_delta.groupby(week)                        # week-clustered resampling
boot   = [weeks.sample(n_weeks, replace=True).sum() for _ in range(10_000)]
p      = two_sided(boot)
q      = benjamini_hochberg(p_all_144)

# rotation null: same timestamps, same instrument, same count, same exposure budget
for r in range(1_000):
    shifted = circular_shift(scores, offset=nonzero, within=(cell, month, instrument))
    null[r] = net_of_policy(select(shifted))
passes_rotation = policy_net > quantile(null, 0.95)

A contrast is credited when all seven bars hold: net return above the comparator, net return per unit of average gross exposure above the comparator, q below 0.05, more than half of the 43 months won, monthly reward-to-risk above 1, a positive lead after deleting the best month and a result above the 95th percentile of the rotation null. When a policy wins every month the reward-to-risk ratio is undefined and the bar fails; twelve contrasts are in that position, six in equity 15-minute bars and six in crypto one-hour bars.

The general claim needs a credited result in both markets, both timeframes and at least two of the three model families. The absolute weekly figures quoted in the note for single books come from a separate week-clustered bootstrap of each book's own weekly P&L.

Checks

Before any out-of-sample number existed, the panel-level suite confirmed that truncating the future leaves every earlier feature, signal and trade unchanged (maximum difference 0.0), that a planted target leak lifts validation IC from 0.017 to 0.9999, that no cost or funding column reaches the model matrix, that funding is event-timed with long positions debited when the rate is positive, that known equity splits are adjusted and that next-bar targets align to within 1.7e-6 bp. A Python reference matched the compiled signal engine on 57,648 trades from one configuration in every family on 50,000 real BTCUSDT bars.

After the run, every one of the 39,908,684 out-of-sample candidate rows was checked against the source panels: entry one bar after the signal, exit after exactly the holding period, entry price equal to the panel open and exit price to the panel close, gross return, spread, fee, slippage and funding recomputed from raw inputs, the net identity, entry inside the scoring month and the price feature equal to the log close of the signal bar. Every filter, sizing, ranking and simple-filter weight was rebuilt from the stored predictions and training medians, and the pooled totals of all 92 policy books rebuild to within 5.3e-15.

The equity spreads are the per-bar values frozen with the study. Re-pricing every equity trade with a later re-pull of the same spreads moves no policy by more than 0.0024 of capital and changes no sign. The bootstrap p-values, BH q-values and rotation-null exceedances also reproduce exactly from the daily series.

Hashes

ArtifactSHA-256
Rule library501f04df5537f881709027e8755eade85d7ca667acb0051d1fcd4b9dc18bb14a
Model grids6c6d89b6cfd435ce875876820443405f5d383956a5673ea38f5d2d410b490e47
Policiesdf469ad083dde73c7be52ed5d33ad4d434faa684a74e7777c200b6d5b988bf1d
Featureseedebd594bfa8aa9c56fb282e7d065eef06c99b63a0d07f974668a526862cc02
Combined configuration6fc808d41d71d9930b17eb81a0b6b0d2189ba9b7e084896e70c61a3020c94f0f
Analysis configuration1e3af923775cf8fca30f2c28da0ecab7803320454cfafd15e9101a33d53b5735
Analysis implementation86f433e42fe14560425f6da690754e5b603509b4b9581bba56d6bd1bd447fb3b

Every number the note's charts draw is in one JSON file. The full pipeline, from signal generation through the walk-forward fits to the audit scripts, is available on request at daniel@daru.finance.

← Back to the note