Market maturity and alpha
This page gives the exact recipe behind every number in the article: data, universe, rule library, execution engine, costs, the matched estimand, the freeze and the descriptive tables. The snippets are short illustrations of the logic rather than the production code.
Data
Binance: Every series comes from the public Binance data archive (data.binance.vision), USD-margined futures, with no credentials, covering per contract:
- monthly 1-hour klines, the price series for the engine and the estimand;
- monthly 1-minute klines, reduced on arrival to one row per hour and then discarded;
- monthly funding-rate files, one row per payment, carrying the archive's own settlement-interval column;
- daily
bookTickerfiles (best bid and offer), published from 16 May 2023 to 30 March 2024; - daily
bookDepthfiles (cumulative resting notional at ±1% to ±5% of mid, roughly one snapshot every 30 seconds), used from 1 January 2023 to 9 August 2026.
Census: The contract list is rebuilt from the archive rather than taken from the live exchange. Every symbol that ever appeared under the monthly klines prefix is enumerated, which recovers contracts the exchange later removed: 986 symbols, 12 of which never traded.
For each symbol, the monthly files are walked forward and backward to the first and last bar with positive traded volume. File existence is not evidence of trading, so the bounds come from volume and never from the first or last file, after which the 832 USDT-quoted traded symbols form the study.
First traded day: A contract's age clock starts at 00:00 UTC of the first day on which its perpetual recorded positive traded volume on the venue. Age in days is the whole number of days since that midnight, so every bar of a UTC day shares one age.
first_day_ms = first_positive_volume_bar_ms // DAY_MS * DAY_MS age_days = (bar_open_ms - first_day_ms) // DAY_MS # one clock, used everywhere
Because the archive's earliest month is January 2020, 3 contracts whose first archived bar falls in that month have a left-censored listing date, all of them in the calibration cohort. The last traded day anywhere in the census, 31 July 2026, is the archive end used for survival.
Intrabar path: Each month of 1-minute bars collapses to one row per hour: the hour's high and low, the minute each printed, a high_first flag (the high's minute precedes the low's) and an ambiguous flag (both in the same minute).
Bybit: Hourly klines for 802 USDT linear perpetual files were frozen as a snapshot at 22:23:09 UTC on 21 September 2026 so the input could not change after the seal. Funding comes from Bybit's public v5 funding-history endpoint, paged back from each contract's last bar to its first.
Order books come from Bybit's public order-book archive (500-level files, 200-level from 2025), which starts in 2023. A Bybit contract's first traded day is the UTC day of its first hourly bar with volume above zero.
Universe
Binance cohorts: The split is on the first traded day.
| Cohort | Listed | Contracts | Use |
|---|---|---|---|
| Calibration | on or before 31 December 2021 | 140 | chooses the rule library, nothing else |
| Evidence | from 1 January 2022 | 692 | the Binance estimand (development sample) |
Of the 692 evidence contracts, 108 no longer trade. The Binance estimand runs on all 692; 688 have the 96 hourly bars the estimand needs (a 48-bar volatility window plus 48 bars) and 4 do not. Its mature reference is drawn from the same 692, with an evidence contract entering it on each day it is more than 365 days old.
Bybit universe: The confirmatory sample comes from Bybit USDT linear perpetuals first traded from 1 January 2022 to 31 July 2026, a listing window holding 620 contracts, keeping those whose young price path is not in the Binance data. Tokens are matched to Binance by identity instead of by symbol, because the two venues often quote the same token under different multiplier prefixes:
- Strip the
USDTsuffix and any leading1Mor power-of-ten prefix (10,100,1000, ...) to get the token name. - A name match that came from stripping a prefix is confirmed on one common hour, where the ratio of the two venues' opens must sit within 15% of a power of ten; a prefix match that fails is treated as a different token.
- An identical symbol always counts as the same token even when the price check fails, since calling it untouched would be the unsafe direction, while a name match with no common hour to check also counts.
def token(sym): # "1000000BABYDOGEUSDT" -> "BABYDOGE"
return re.sub(r"^(1M|10+)(?=[A-Z])", "", sym[:-4])
def same_token(bybit_open, binance_open):
r = bybit_open / binance_open
k = round(math.log10(r))
return abs(r / 10**k - 1) < 0.15 # 1000TAG vs TAG passes at 996.40Prefix matching confirmed 5 pairs (1000000BABYDOGE/1MBABYDOGE, 10000SATS/1000SATS, 1000TAG/TAG, 1000TOSHI/TOSHI, 1000TURBO/TURBO). Of the identical symbols, 4 failed the price check and were kept as the same token (HNT, ON, ORBS, SNT), and another 7 name matches had no common hour and were also kept as the same token.
Each contract then falls into one group.
| Group | Rule | Contracts |
|---|---|---|
| Bybit only | the token never had a Binance USDT perpetual | 79 |
| Bybit first | Binance listed the token at least 30 days after Bybit | 77 |
| Overlap | everything else | 464, excluded |
For the Bybit-first group, young days stop on the day before the Binance listing, with any position still open closed at that day's last bar. That gives 156 contracts and 4,524 young calendar contract-days at ages 2 to 30, of which 4,284 (from age 3 on) have a volatility estimate and 3,295 are matched.
Bybit mature reference: Bybit USDT contracts first traded from 1 January 2022 enter on days they are more than 365 days old, matching the listing window of the Binance mature leg. A Bybit-first contract joins the mature leg once it passes 365 days, like any other. Of the 802 files, 715 contracts load into the estimand; 80 are skipped as listed before 2022 and outside the universe; 7 have fewer than 96 bars.
Rules
Vocabulary: 34 rule variants make up the vocabulary, 11 transforms and 23 confluence gates. A transform rule z-scores a transform of the close over a rolling L-bar window and trades the sign when the z-score crosses a threshold. A confluence rule z-scores the raw close the same way and keeps the signal only on bars where the gate is true.
def desired_state(kind, name, L, thr, o, h, l, c):
if kind == "transform":
z = rolling_z(transform(name, c, L), L)
gate = True
else:
z = rolling_z(c, L)
gate = confluence_gate(name, o, h, l, c, L)
d = np.where(z > thr, 1, np.where(z < -thr, -1, 0))
return np.where(gate, d, 0) # 0 = no instruction this barTransforms: base (the close), slope, normalized_price, roc, bias, volZ, accel, disFromMedian, quant_stretch, rank_resid, fold_dev. Gates: RSIge40, RSIge50, Pge0.7, Pge0.8, BW_filter, pi, vr, kurtosis, skew, skew0.75, atr_pct, atr_pct0.8, burstfreq, TinyBody, NoNewLowGreen, RangeSpike, YesterdayPeak, DeadFlat10, InsideBar, SameDirection, TopOfRange, VolContraction, EMAHug.
Grid: Every variant is crossed with lookback L in {4, 8, 12} bars, threshold in {0.5, 1.0, 1.5}, maximum hold in {12, 48, 168} bars, stop distance in {1, 2, 4} × ATR and reward-to-risk in {1, 1.5, 2, 3, 5}, giving 34 × 405 = 13,770 configurations. Screening every configuration on the 140 calibration contracts makes the search 13,770 × 140 = 1,927,800 engine runs in total. Lookbacks stop at 12 bars so every rule can be warm before the youngest measured day.
Screen: The screen runs on contracts with at least 2,000 traded hourly bars, under the conservative intrabar convention (below), charging the 5 bp fee plus the 7.4 bp measured execution cost of a mature Binance contract on every fill. Young days never reach it, since signals on any bar where the contract is under 31 days old are set to zero before scoring. A configuration is eligible if at least 35 contracts (a quarter of the cohort) show at least 24 closed trades across at least 6 distinct calendar weeks, and at least 35 show a first signal within the contract's first 40 bars.
signals[age_days < 31] = 0 # selection never sees young days eligible = (n_contracts_active >= 35) & (n_contracts_warm >= 35) ranked = sorted(eligible_rows, key=lambda r: -r.pooled_net) library = first_per_variant(ranked, k=20) # one rule per variant
Under these filters 12,555 configurations are eligible, none with positive pooled net after costs on the calibration cohort and 73 with positive gross. The library is the top 20 by pooled calibration net, at most one per variant, with no positivity screen, because the estimand is a young-minus-mature difference that stays defined whatever the sign of either leg.
Frozen library:
| Variant | Kind | L | Threshold | Stop (× ATR) | Reward-to-risk | Max hold (bars) |
|---|---|---|---|---|---|---|
| BW_filter | gate | 4 | 1.5 | 4 | 1.5 | 168 |
| kurtosis | gate | 12 | 0.5 | 4 | 5 | 168 |
| NoNewLowGreen | gate | 12 | 1.5 | 4 | 5 | 168 |
| pi | gate | 12 | 1.5 | 4 | 5 | 168 |
| Pge0.8 | gate | 12 | 1.5 | 4 | 5 | 168 |
| Pge0.7 | gate | 12 | 1.5 | 4 | 5 | 168 |
| TinyBody | gate | 12 | 1.5 | 4 | 3 | 168 |
| skew0.75 | gate | 4 | 1.5 | 1 | 5 | 168 |
| InsideBar | gate | 4 | 1.5 | 4 | 5 | 168 |
| DeadFlat10 | gate | 4 | 1.5 | 4 | 5 | 168 |
| atr_pct0.8 | gate | 12 | 1.5 | 4 | 5 | 168 |
| vr | gate | 12 | 1.5 | 4 | 5 | 168 |
| atr_pct | gate | 12 | 1.5 | 4 | 5 | 168 |
| YesterdayPeak | gate | 12 | 1.5 | 4 | 5 | 168 |
| RangeSpike | gate | 8 | 1.5 | 4 | 3 | 168 |
| bias | transform | 12 | 1.5 | 4 | 5 | 168 |
| skew | gate | 4 | 1.5 | 4 | 5 | 168 |
| RSIge50 | gate | 12 | 1.5 | 4 | 5 | 48 |
| EMAHug | gate | 4 | 1.5 | 4 | 3 | 168 |
| SameDirection | gate | 4 | 1.5 | 4 | 5 | 168 |
Each gate in the library tests bars already closed: BW_filter, the previous bar's body is more than 70% of its range; TinyBody, the previous bar's body is under 10% of its range; pi, the previous bar's body divided by L-bar ATR is within 0.05 of π/4; kurtosis, rolling L-bar return kurtosis of at least 5; skew and skew0.75, absolute rolling return skewness of at least 0.5 and 0.75; Pge0.7 and Pge0.8, the prior close sits in the top 30% or 20% of its rolling L-bar range; atr_pct and atr_pct0.8, the same test on L-bar ATR within its own rolling range; vr, a 2-bar to 1-bar variance ratio of at least 1; NoNewLowGreen, the previous bar closed up and its low is above the close 5 bars before it; RangeSpike, the previous bar's range exceeds 1.3 times its 20-bar average; YesterdayPeak, the close 2 bars back is the highest of the last 3; DeadFlat10, the close moved less than 0.5% over the 10 bars to the previous close; InsideBar, the bar 2 back lies inside the previous bar's range; SameDirection, the last 2 bars closed in the same direction; RSIge50, the previous 14-bar RSI is at least 50; EMAHug, the previous close is within 0.92 of the previous bar's range from its 21-bar mean. The one transform, bias, is an L-bar exponential average of the L-bar slope of the close.
The library file's SHA-256 is 3b4c9b75e5ce3ddb66ba21c1d4fcdeda3ad8a6513881857295b0cbdfae528556.
Engine
One position runs per rule per contract, at fixed notional with no compounding. Position size is one unit of notional at entry, so every P&L figure is a fraction of the entry notional.
- Entry: A signal computed on completed bar
tenters at the open of bart+1, and a same-direction signal while in a position is ignored. An opposite signal closes at the next open and reverses there, paying 2 fills, while a zero is not an exit instruction. - Barriers: Stop and target are fixed at entry from Wilder ATR(14) through the bar before entry, with the stop at
stop × ATRfrom the entry price and the target atstop × ATR × reward-to-risk; neither moves afterward. - Maximum hold: The position exits at the open of the bar once
Hbars have passed since entry. - Gaps: An open through the stop fills at that open. An open through the target fills at the target, never better.
- Entry bar: Entry is at the open, so the whole bar's range comes after it and both barriers are live on the entry bar.
- Same bar touches both barriers: Under true order (Binance only) the 1-minute path decides, with the target first if the high came before the low for a long, the stop first otherwise and the mirror for a short; a bar whose extremes printed in the same minute, or whose path record does not reproduce the bar's high and low, is resolved as a stop. Conservative resolves every such bar as the stop, long and short alike. Bybit has no minute path and so runs conservative only, whereas Binance runs both.
- Re-entry after a barrier fill: If the rule still wants a position after a stop or target fills inside a bar, the new position fills at the barrier price, the price the book is actually flat at, instead of that bar's open. On that bar only the new stop can fire, judged on the bar's adverse extreme, while the new target is live from the next bar, because the part of the range before the fill cannot be attributed to the new position.
- Order within a bar: funding, gap barriers, reversal, maximum hold, new entry, intrabar barriers, forced close.
- Funding: Observed payments are charged at their true settlement time, each landing on the first bar opening at or after its settlement hour and charged on the position held immediately before settlement as
direction × rate × quantity × price, valued at that bar's open. Positive funding debits longs and credits shorts; a position opened on the settlement bar neither pays nor receives, whereas one carried into it does.
No signal ever uses funding. Binance rates come from the archive's funding files with their per-payment interval, and Bybit rates from the funding-history endpoint.
for i in range(1, n):
if pos: funding_cost += pos * rate[i] * qty * open_[i] # 1. funding
if pos:
hit = barrier(pos, open_[i], high[i], low[i], hi_first[i], ambiguous[i])
if hit: close(i, hit.price); reentry_px = hit.price # 2. gap / intrabar barrier
elif want[i-1] == -pos: close(i, open_[i]) # 3. reversal
elif i - entry_i >= max_hold: close(i, open_[i]) # 4. maximum hold
if not pos and want[i-1]:
enter(i, at=reentry_px if exited_this_bar else open_[i]) # 5. entry
check_barriers_on_entry_bar(i) # 6. intrabar barriersEvery trade carries gross P&L, fee, slippage and funding in separate columns, with gross - fee - slippage - funding = net checked on every trade. Across 7,234,659 Binance trades (true order), 7,234,637 (conservative) and 8,059,098 Bybit trades, no trade violates it and the largest residual is 0.0.
Under the true-order convention, of 7,555,948 Binance hourly bars, 7,525,107 carry a resolved minute order, 24,391 have both extremes in the same minute and 6,450 have a path record that does not match the bar's high and low, with the last 2 groups resolving as stops.
Costs
Every fill pays a taker fee plus a measured execution cost for the contract's age bucket at a registered position size of $10,000. The entry fill is charged at the entry age's bucket and the exit fill at the exit age's bucket, scaled by exit over entry price so the charge does not depend on the trade's outcome. A fill at age 0 or 1, which has no bucket, pays the 2-7 day cost.
slip = (cost_bp[bucket(age_entry)] + cost_bp[bucket(age_exit)] * exit_px / entry_px) / 1e4 fee = fee_bp / 1e4 * (1 + exit_px / entry_px) net = gross - fee - slip - funding
Fees: Binance charges 5 bp per side and Bybit 5.5 bp per side, the base taker tier.
Binance half-spread: From the bookTicker archive, sampled on a fixed age ladder: ages 0, 1, 2, 3, 5, 7, 10, 14, 21, 30, 45, 60, 120, 200, 300, 400, 500, 700, 900 and 1,200 days, for every USDT contract trading at any point inside the quote window, keeping whichever rungs fall inside it. Per contract-day the half-spread is the median over that day's quotes of (ask - bid) / (ask + bid) in basis points, dropping days with fewer than 100 valid quotes. The bucket value is the median over contract-days, 1,425 contract-days from 257 contracts.
Impact on Binance: A single depth sample on one sampling scheme covers every age, so the young and mature legs come from the same panel. For every contract it takes bookDepth days at ages 3, 7, 14, 28, 45, 75, 120, 160, 240 and 300, plus 8 days evenly spaced from age 366 (or 1 January 2023 if later) to the contract's last day, all inside the archive window.
Within each day, each band's cumulative resting notional is the median over that day's snapshots. A $10,000 buy is then swept up the 1% to 5% ask ladder with uniform density inside each band, and the cost is the notional-weighted average distance from mid. Per bucket the value is the median over each contract's days in the bucket, then the median over contracts.
def sweep_cost_bp(cum_notional, size=10_000, bands=(1, 2, 3, 4, 5)): # bands in % of mid
filled = paid = prev_n = prev_d = 0.0
for d, n in zip(bands, cum_notional):
take = min(size - filled, n - prev_n)
paid += take * (prev_d + (d - prev_d) * take / (n - prev_n) / 2) # uniform in band
filled += take; prev_n, prev_d = n, d
if filled >= size: break
return paid / size * 100 # % of mid -> bp| Binance bucket | Half-spread, bp | Contract-days (contracts) | $10,000 impact, bp | Contract-days (contracts) | Cost per fill, bp |
|---|---|---|---|---|---|
| 2-7d | 1.56 | 346 (88) | 4.56 | 1,273 (642) | 6.12 |
| 8-30d | 1.22 | 355 (94) | 3.98 | 1,266 (650) | 5.20 |
| 31-90d | 1.20 | 168 (89) | 4.80 | 1,160 (613) | 6.00 |
| 91-180d | 1.20 | 91 (91) | 5.69 | 1,022 (526) | 6.89 |
| 181-365d | 1.22 | 104 (64) | 7.39 | 848 (448) | 8.61 |
| over 365d | 1.18 | 189 (150) | 6.22 | 3,780 (477) | 7.40 |
Bybit: Costs are sampled from Bybit's public order-book archive. Young days cover every universe contract at ages 3, 7, 14 and 28, where that age is still young (for Bybit-first contracts, before the Binance listing). Mature days come from 60 Bybit USDT contracts outside the universe drawn with seed 3, 4 random dates each, at ages above 365 and from 1 June 2023 to 31 July 2026, with only those first traded from 2022 counting toward the mature bucket.
Each day's file is replayed from snapshot plus deltas, and every 60 seconds of exchange time the half-spread and the cost of a $10,000 market order against mid are recorded, the cost being the average of a buy and a sell walked through the book. Per contract-day the median of each is kept, and per bucket the median over contract-days. Impact is the full cost minus the half-spread.
Of the 788 sampled contract-days, 706 exist in the archive.
| Bybit source bucket | Contract-days (contracts) | Half-spread, bp | Impact, bp | Cost per fill, bp |
|---|---|---|---|---|
| 2-7d | 270 (135) | 3.81 | 25.20 | 29.01 |
| 8-30d | 272 (136) | 3.97 | 25.17 | 29.14 |
| over 365d | 164 (42) | 2.61 | 27.20 | 29.81 |
Since the sample measures 3 buckets, the others map to the nearest measured one: 31-90d takes the 8-30d cost, while 91-180d and 181-365d take the over-365d cost. Every cost constant for both venues lives in one file, which the estimand and the freeze both read (SHA-256 eb0e6aa42c367840b8d59fd8ac59ec43200bd968b4514ef53f594a0cd434e4eb).
Estimand
Unit: The observation is the contract-day, one UTC day on which the contract has at least one hourly bar; ages 0 and 1 are never observation days, although trades opened then still contribute to the days they are held from age 2 on. Each trade's gross P&L, fee, slippage, funding and net are spread equally across the live days from its entry day to its exit day inclusive. A contract-day's value is the sum over the 20 rules of their shares that day, divided by 20, in basis points, with days on which no rule holds a position counted as zero.
share = 1.0 / (exit_day_idx - entry_day_idx + 1) day_pnl[entry_day_idx : exit_day_idx + 1] += trade_pnl * share daily_bp = day_pnl.sum(over_rules) / 20 * 1e4 # fixed library size
Volatility: Realised volatility is the standard deviation of the last 48 hourly log returns, counted in bars rather than clock hours. The value for day d is read from the bar that opens at 23:00 UTC the day before, the bar that closes exactly as day d begins, so nothing inside day d reaches its own covariate.
If that bar is missing, the day has no volatility and cannot be matched. With a 48-bar window closed before the day starts, age 2 has no estimate, so the first young day with an estimate is age 3.
Buckets: Volatility buckets are cut on the natural log of hourly realised volatility at −7.0, −6.5, −6.0, −5.5, −5.0, −4.5, −4.25, −4.0, −3.75, −3.5, −3.25 and −3.0, giving 13 buckets with open ends. Age buckets run 2-7, 8-30, 31-90, 91-180 and 181-365 days, with mature at 366 days and over and the primary young window at ages 2 to 30.
Matching: Mature contract-days are averaged within each (calendar day, volatility bucket) cell first, so a crowded cell does not outweigh a thin one. Each young contract-day is differenced against the mature mean of its own cell. A young day whose cell has no mature contract is counted as unmatched and never compared against a different cell.
The estimand is the mean of the matched differences. A row-count check asserts that every young observation leaves as exactly one matched or one unmatched row.
ref = mature.groupby(["day", "vol_bucket"]).net.mean() diffs = young.join(ref, on=["day", "vol_bucket"], how="left", rsuffix="_ref") matched = diffs.dropna(subset=["net_ref"]) estimand = (matched.net - matched.net_ref).mean()
| Sample | Young days matched | Unmatched | Unmatched share |
|---|---|---|---|
| Binance, ages 2-30 | 15,244 | 3,573 | 19.0% |
| Bybit, ages 2-30 | 3,295 | 989 | 23.1% |
For the decomposition, the same matching runs separately on gross, fee, slippage and funding, on each age bucket (only when it has at least 30 young rows) and on the 3 arms. All arms come from one signal stream: long-short trades both signs, long-only flattens short signals to cash and short-only flattens long signals.
Inference: A week-clustered percentile bootstrap on the matched differences uses 2,000 replicates and seed 20260810. Weeks are 7-day blocks counted from the Unix epoch, so they run Thursday to Wednesday UTC, and every young observation inside a sampled block travels together.
Mature cell means are held at their full-sample values, with the resampling over the young differences only. The 95% interval is the 2.5th and 97.5th percentiles of the replicate means.
rng = np.random.default_rng(20260810) block = matched_day // 7 # epoch-day blocks groups = [diff[block == b] for b in np.unique(block)] draws = [np.concatenate(rng.choice(groups, len(groups))).mean() for _ in range(2000)] lo, hi = np.percentile(draws, [2.5, 97.5])
Readings: Binance is run under both intrabar conventions and is the development sample. Bybit is run once under the conservative convention and is the confirmatory test. The publishing rule was fixed before the Bybit run: an interval that excludes zero with the same sign as the Binance estimate is a replicated age effect, and anything else is a null reported with its measured interval width.
Freeze
Before the Bybit estimand ran, a bundle was sealed containing SHA-256 hashes of:
- the implementation, including the estimand, engine, screening engine, rule vocabulary, library screen, constants module, archive client, cost builder, cost file, Bybit universe builder, analysis driver and the freeze itself;
- the artifacts: the frozen library, the full 13,770-row screen, the Bybit universe, the Bybit cost sample, the census and the full archive symbol list;
- every input data series, file by file: each Binance panel series for all 832 contracts (hourly klines, mark price, premium index, funding, minute path, depth and the depth sample), the quoted-spread samples, the 986 census records, plus 802 Bybit kline files, 802 Bybit funding files and 872 Bybit order-book sample files;
- the registered constants as read from the modules that execute them: age and volatility buckets, volatility window, library size, position size, bootstrap seed and replicate count, the grid, eligibility thresholds and cost tables.
Sealing finished at 22:26:51 UTC on 21 September 2026 under bundle hash 23f5bf876015564577d880fe86f7a5ac22b370d05f42713a7ef4fb846c8d31e1. A copy was uploaded at 22:28:34 UTC to off-machine object storage, which records an upload time the local machine cannot alter, and the Bybit results were generated at 22:32:53 UTC, with the 2 Binance results following at 22:32:58 UTC.
Verification: The estimand re-hashes everything in the bundle before it runs and refuses to start if any item has moved, or if the seal time is not earlier than the run. A Bybit run also refuses a library other than the sealed one, a symbol subset, an output location other than the sealed one and any Bybit output file older than the seal. Each results file then records both the bundle hash and the seal time.
ok, moved, bundle = verify(bundle_path) # re-hash code, data, artifacts, constants
if not ok or bundle["created_utc"] >= now_utc():
raise SystemExit("freeze does not verify; run refused")
results["seal"] = {"bundle_sha256": bundle["bundle_sha256"], "sealed_utc": bundle["created_utc"]}Descriptive tables
Each table in the article's first half is computed as follows, with age always counted in whole days since the census first traded day.
How a contract changes as it ages: All 692 Binance evidence contracts enter with no minimum history, of which 691 contribute. Per contract and age bucket (0-1, 2-7, 8-30, 31-90, 91-180, 181-365, over 365 days), the columns use that contract's hourly bars with volume above zero and need at least 24 bars in the bucket:
- volatility: standard deviation of hourly log returns times √8,760;
- correlation to Bitcoin: Pearson correlation of hourly log returns with BTCUSDT on common timestamps;
- volume vs own mature level: the bucket's median hourly quote volume divided by the same contract's median past 365 days, only for the 352 contracts that reached that age.
Funding is computed per payment rather than per bar. Each payment is rescaled to an 8-hour equivalent as rate × 8 / interval (zeros included) and assigned to the bucket of the payment's age. Per contract the mean and standard deviation are taken over at least 3 payments, after which every column is the median across contracts.
r8 = rate * 8.0 / interval_hours # 1h, 2h, 4h and 8h payments comparable
per_contract = payments.groupby("bucket").r8.agg(["mean", "std"]) # needs >= 3 payments
table = per_contract_all.groupby("bucket").median() # median across contractsSettlement schedule: All 832 USDT contracts are covered from the archive's per-payment interval column, with the starting interval taken as the interval of a contract's first payment and tabulated by listing year. "Never settles at eight hours" and "changes interval at least once" are computed over each contract's full payment history. Payment shares by age pool every payment in the age bucket (0-7 days, 8-30 days and so on) across contracts.
Quoted spreads by age: These use the half-spread sample described under Costs, with bucket medians over contract-days. The same-contract table uses the 86 contracts listed inside the quote window: per contract, the median half-spread in each bucket; the ratio of its 0-1 day value to its value in the later bucket; then the median of those ratios and the share above one.
Depth and impact by age: These use the depth sample and sweep described under Costs, buy side, $10,000, with the first row pooling ages 3 and 7. The same-contract column divides each contract's median impact in the young bucket by its own median past 365 days for contracts with both, then reports the median ratio and the share below one.
Bybit execution costs: The order-book sample described under Costs, 706 contract-days.
Abdi-Ranaldo test: The 86 contracts listed inside the quote window contribute ages 0 to 60 on the fixed ladder, 987 contract-days. The estimate per contract-day is the median over that day's hourly bars of the Abdi-Ranaldo value (negative values clipped to zero), bars with volume above zero only. The real value it is compared against is the measured quoted half-spread of the same day.
Cells are medians over pairs in each age bin and the ratio column is the median of per-pair ratios, while the rank correlation is Spearman over all 987 pairs (+0.0349). The estimator returns the full effective spread, which accounts for about half the gap in level, and halving it does not change the rank correlation.
Survival: Archive end is the last traded day in the census (31 July 2026), and a contract is eligible for age mark k if it was listed at least k days before the archive end. It reached the mark if it traded for at least k days, or if it was still trading within 7 days of the archive end. Share is reached over eligible, computed separately for contracts listed from 2022 (692) and up to 2021 (140).
eligible = archive_end - first_traded >= k_days reached = (last_traded - first_traded >= k_days) | (last_traded >= archive_end - 7_days) share = reached[eligible].mean()
Requesting the full pipeline
The full pipeline (census, panel builder, order-book samplers, screen, engine, estimand, freeze and descriptive scripts) is not distributed from this page. To request it, email daniel@daru.finance.

