93 Commits

Author SHA1 Message Date
jgrusewski
a0181431b3 docs: plan — decommission dead Rust infra (Phase 1), inline+gated execution
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:24:41 +02:00
jgrusewski
feb0ae2541 docs: spec — decommission dead Rust infra (Phase 1), data-preserving (keep all .dbn)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:21:49 +02:00
jgrusewski
4afaa5e248 fix(infra): resolve double-declared NAT gateway (kapsule vs public-gateway)
The real gateway foxhunt-gw (42653780) was an unmanaged orphan: kapsule state never had a gateway,
public-gateway state referenced a deleted gateway (7a7ffaa5) -> BOTH modules' terragrunt apply would
have created a duplicate gateway (risking cluster egress). Fix:
- kapsule module: removed the legacy gateway/dhcp/gateway_network resources (pre-refactor duplicate,
  never in kapsule state; cluster only needs the private_network which stays). kapsule plan now clean.
- public-gateway module (canonical, IPAM mode): state rm the stale 7a7ffaa5 refs + imported the real
  gateway/gateway_network/IP (42653780 / 661c8543 / dc419a10); set bastion_enabled=true to match the
  live gw. public-gateway plan now 'No changes'. Live gateway untouched throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:05:50 +02:00
jgrusewski
e21a9a5da4 fix(gitlab): enable terraformState object storage (was 403 on all TF-state API calls)
Root cause: global.appConfig.terraformState was missing entirely from the GitLab Helm values, so the
chart rendered terraform_state.enabled=false + object_store disabled -> every /terraform/state API call
403'd ('Object Storage is not enabled for Terraform::StateUploader'), blocking terragrunt. State files
were intact in MinIO (foxhunt-gitlab-artifacts) the whole time. Added terraformState{enabled,bucket,
connection} mirroring artifacts/lfs/packages; helm upgrade -> revision 25 -> TF-state API now 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 13:54:13 +02:00
jgrusewski
b2efc58940 fix(ci): downsize ci-compile-cpu pool POP2-HC-32C-64G -> POP2-4C-16G (root-cause: zone stock-out)
Root cause of the stuck cockpit deploy: ci-compile-cpu (32C/64G POP2-HC) hit a fr-par-2 zone stock-out
('Resource POP2-HC-32C-64G is out of stock') -> autoscaler ScaleUpTimedOut -> kaniko build Pending forever.
The big node was sized for the now-dormant Rust cargo compile + precompute_features; the Python fxhnt
cockpit build only needs ~2-6 vCPU. Downsized to POP2-4C-16G (in stock in fr-par-2, far cheaper, scale-to-zero).
Apply blocked in-session: cluster GitLab tokens lack 'api' scope for the TF state backend (403).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:26:01 +02:00
jgrusewski
fbb7b429e3 infra(cert): ACME wildcard *.fxhnt.ai via cert-manager + Scaleway DNS-01 (replaces expired manual LE cert)
cert-manager ClusterIssuers (LE staging+prod) + Certificate writing gitlab-tls-cert, auto-renewing.
Scaleway webhook helm-installed (scaleway/scaleway-certmanager-webhook, cert-manager ns); DNS creds
in cert-manager/scaleway-dns-credentials. Proxy tls-certs volume repointed live to gitlab-tls-cert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:21:30 +02:00
jgrusewski
f4bd1e2432 research(surfer): crypto edge-falsification probes — intraday/cascade dead, stablecoin peg-reversion validated
- crypto_mft_xsec / mft_*: intraday/MFT XS price edge FALSIFIED (fee-trap)
- crypto_cascade_reversion: liquidation-cascade reversion FALSIFIED (continuation)
- crypto_trend_sizing: TS-trend return-engine reconfirmed (Sharpe ~1.25)
- crypto_stablecoin_dislocation/harden/intraday: short-rich peg-reversion VALIDATED (bounded, uncorrelated); long-cheap = death-spiral trap

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:40:34 +02:00
jgrusewski
6878aac950 fix(ci): grant ci-deploy SA serviceaccounts/pvc/cronjobs + deployments-watch
The fxhnt-cockpit deploy step applies manifests containing ServiceAccounts (dashboard/dagster), a PVC
(forward track), and a CronJob — none of which the ci-deploy Role permitted, so kubectl apply failed
partway. Also added 'watch' on deployments for 'kubectl rollout status'. Surfaced once the cockpit deploy
finally ran (it previously died at the MinIO log sink first). Rule: this Role must gain a grant for every
new resource KIND a deployed manifest introduces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:36:25 +02:00
jgrusewski
bccd814716 deploy(proxy): route dashboard.fxhnt.ai to the fxhnt cockpit (via its tailnet node)
Replaces the web-dashboard upstream. Proxies peer-to-peer over the tailnet (the kernel-mode proxy can't
reach platform-pool ClusterIPs due to the 100.64/15 pod-CIDR ↔ Tailscale CGNAT overlap, but reaches tailnet
nodes fine). Live CM already patched; this keeps the repo in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:42:55 +02:00
jgrusewski
cfa28368d5 fix(surfer): futures fetch weekly (Sunday only) in forward-track cron — cost control
Daily 16y all-expiry Databento refetch was ~$6/day (~$190/mo) for the 17% hedge sleeve. Now: crypto_pit
fetched daily (free, the 83% alpha), futures fetched only on Sundays (~$25/mo, within free credits), and
forward-track runs daily (books the combined book's new days). The alpha is validated daily-accurate; the
slow trend hedge weekly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:11:00 +02:00
jgrusewski
f9b62d108c feat(surfer): fxhnt combined-book forward-track cron + dynamic fetch END
fetch_daily.py: END now clamps to the GLBX dataset range-end (was hardcoded 2026-06-05; clamp handles the
~1-3d exchange lag — END=today failed get_cost). scripts/surfer/fxhnt_forward_cron.sh: daily wrapper —
fetch crypto_pit (free) + futures (Databento, cost-capped) + 'fxhnt forward-track' to book the new paper-
NAV day of the validated combined book (crypto X-sec momentum alpha + futures trend hedge). Crontab: daily
23:30 UTC -> data/surfer/fxhnt_forward.log. Sources DATABENTO key from ~/.secrets (never printed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 00:09:40 +02:00
jgrusewski
54e3d50db8 backtest: fusion (regime-adaptive QQQ<->crisis-alpha) FAILS OOS, keep static GD
Fused all ideas: QQQ core + foxhunt controllers as a ROTATOR (edge-decay-trust + resurrection +
drawdown-CB) rotating QQQ<->crisis-alpha by regime. Pre-registered criterion: beat static GD on OOS
Calmar. RESULT: full-sample fusion Calmar 0.41 > GD 0.34 (IS-flattered by 2008), but OOS (2018-2026)
static GD 0.64 > fusion 0.58, even pure QQQ 0.59 > fusion -- GD strictly dominates OOS (higher CAGR +
lower DD). Regime-rotation whipsaw + late-re-entry cost exceeds the protection benefit in bulls.
Verdict: fusion fails OOS -> keep the simpler static GD. Simple beats fancy; only OOS separates them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:01:04 +02:00
jgrusewski
d82439219d add: drawdown-profile analysis tool (multistrat_dd.py)
Full drawdown profile of the deployable book (depth/frequency/duration/recovery/ulcer) vs 60/40.
Analysis tool, not part of the deployed pipeline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:11:06 +02:00
jgrusewski
3f321bb4bf fix: broker-data sanity-check + drop incomplete intraday bar (two live-only bugs)
(1) IBKR paper reports summary fields (cash/GrossPositionValue) inconsistent with actual positions
(found: $141k/13% gap, fills+positions are ground truth). Bot now cross-checks NLV vs cash+position-
market-value, WARNs on >2% gap, sizes on the CONSERVATIVE NLV (never inflates), and a gate refuses to
trade if the gap exceeds MULTISTRAT_DATA_TOL (default 25%).
(2) build() included today's INCOMPLETE intraday bar when run mid-session -> spiked vol estimate ->
halved the leverage (1.0->0.5) -> would have sold half the book as churn at the 14:35 cron. Now drops
today's partial bar; uses completed closes only (matches the daily-close backtest). Verified live:
leverage back to 1.0, book recognized on-target (in_band), no spurious orders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:01:04 +02:00
jgrusewski
02b851ca8b feat: growth-discipline forward-tracker (NAV sim, alongside the live book)
Works exactly like multistrat_paper.py (daily cron, status/run/weights, idempotent, forward-start,
catch-up) but tracks the GD strategy: 70% QQQ + 30% multi-strat book, continuously rebalanced. NAV
simulation (no orders) running alongside the live IBKR-paper book to compare forward: GD = higher
CAGR, deeper drawdown by design. Cron 12/15/18 UTC. GD_EQUITY_WEIGHT configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:45:35 +02:00
jgrusewski
a3f309dd7e backtest: growth-discipline multi-premia (QQQ core + trend sleeve + book)
User's '10x boring' idea tested. GD-70 (70%QQQ/15%trend/15%book) has highest Sharpe-excess (0.67-0.84
vs QQQ 0.65, book 0.39); trend sleeve = genuine crisis-alpha. GD@1.3x cheap-lev ~= QQQ return with
shallower drawdown (-48 vs -53%). VRP(PUTW) does NOT add (dilutes); carry no clean retail ETF. Clean
stackable premia = equity-core + trend-sleeve + execution-alpha (behavior gap/tax/cheap-lev, on top,
not in backtest). GD genuinely beats both the over-conservative book and panic-prone raw QQQ.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:42:30 +02:00
jgrusewski
db8f5434a0 doc+tool: CAGR-maximizer reference plan + side-by-side strategy comparison
Honest reference plan (docs/.../2026-06-08-cagr-reference-plan.md): CAGR builds wealth not Sharpe;
more CAGR always = more drawdown; only free CAGR is drag reduction; book's 0.96 Sharpe was an rf=0
artifact (excess-over-financing, equities win); leverage moat = cheap financing. Recommendation:
max equity + low drag + contribute + never sell, optional 1.2-1.3x futures if you survive -65%.
strategy_compare.py: re-runnable side-by-side (book / SPY 1x / SPY 1.3x / 60-40 / 70-30 / overlay)
with backtest stats + the P0+PMT trajectory. Configurable --p0 --pmt --fin --years.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:34:49 +02:00
jgrusewski
1cdbc81390 tune: daily weekday rebalancer eval (35 14 * * 1-5, REBALANCE_DAYS=1)
Switch from weekly to daily (weekday) evaluation for faster risk response — the leverage (vol-target
+ drawdown CB + corr de-risk) re-evaluates each day so a vol spike de-levers within a day vs a week.
Hysteresis (3% NLV) still gates actual trades, so turnover stays low (in-band days = no-ops). No
return impact; tighter risk only. Weekends excluded (US market closed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:51:20 +02:00
jgrusewski
7662d82232 fix(bot): whole-share orders (IBKR API rejects fractional, err 10243) + state only on accepted
Eyes-on EXECUTE=true validation caught two bugs: (1) fractional qty -> all 6 orders cancelled (10243);
round to whole shares (skip if 0). (2) last_rebalance was set even when all orders were rejected,
which would block retry; now only marked when >=1 order is accepted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:34:01 +02:00
jgrusewski
2a468e4b5e arm: multistrat-rebalancer EXECUTE=true (autonomous paper trading)
After full validation (in-cluster connect via 4004, paper account DU9600528, gates pass, clean logs
post NaN-fix), arm the CronJob to place paper orders. Triple-guarded against real money: paper
account + Gateway paper-mode + bot refuses non-DU without MULTISTRAT_ALLOW_LIVE_CONFIRMED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:31:18 +02:00
jgrusewski
8ebb65f27e fix: NaN-safe correlation de-risk in book_series (zero-variance windows)
np.corrcoef on a window where a vol-normalized stream is constant (warmup / non-moving stream)
divided by zero std -> RuntimeWarning + NaN in the corr matrix -> ac=NaN -> propagated via chist
into the z-score and could poison leverage L and book with NaN (silent corruption). Fix: correlate
only streams with std>0 in the window, nansum over the actual valid count, fall back to ac=0 when
<2 vary. Verified: no RuntimeWarning (tested with warnings-as-errors), no NaN, identical L/weights.
Affects all scripts importing book_series + the cluster ConfigMap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:29:36 +02:00
jgrusewski
f320c5f09f fix(infra): rebalancer connects to ib-gateway:4004 (socat bridge), not 4002
Pod-to-pod on 4002 reaches IB Gateway with the pod IP as source -> not in the trusted-IP allowlist
(default 127.0.0.1) -> TCP connects but API handshake times out. The gnzsnz image's socat on 4004
relays via localhost so IB Gateway trusts it. (Local port-forward worked on 4002 because it enters
the pod netns = localhost.) Egress netpol now allows 4002+4004; IB_PORT=4004.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:20:28 +02:00
jgrusewski
dc2b7214f7 feat(infra): K8s CronJob for multi-strat rebalancer (production deployment)
Weekly in-cluster CronJob running multistrat_bot.py on IBKR paper via the existing ib-gateway
(connects to ib-gateway:4002 directly, no port-forward). Lean: code via ConfigMap (no image build),
python:3.12-slim + runtime pip. Includes PVC (state/HWM persistence across runs), egress
NetworkPolicy (ib-gateway:4002 + 443 for pip/Yahoo + DNS), and ib-gateway ingress patched to allow
the rebalancer. Schedule Mon 14:35 UTC (~1h after US open). MULTISTRAT_EXECUTE=false by default
(cluster dry-run); flip to true after validating logs. Drawdown CB + leverage cap + idempotency +
paper-guard enforced by the bot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:16:36 +02:00
jgrusewski
4bbf07bddf feat: production multi-strat rebalance bot (lean enterprise core)
Single-file cron-ready IBKR rebalancer for the adaptive book. Money-touching core only: env config
(no secrets), paper/live guard (refuses live without MULTISTRAT_ALLOW_LIVE_CONFIRMED), leverage cap,
HWM drawdown circuit-breaker, fat-finger per-order cap, once-per-period idempotency, stale-price
guard, post-trade reconciliation, structured JSON audit log. Default dry-run; run needs
MULTISTRAT_EXECUTE=true. Verified vs cluster ib-gateway paper (DU9600528): gates pass, 6-order plan,
JSON audit trail + HWM state persisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:10:24 +02:00
jgrusewski
66e37955ce feat(ibkr-bot): mini test mode — validate paper env + order routing with 1 share
'mini' mode: confirms account is paper (DU prefix; aborts on live U-prefix), places BUY 1 SPY,
reports fill/routing. Validated against cluster ib-gateway: account DU9600528 [PAPER], order routed
(PreSubmitted, queued for next open — market closed on weekend). Confirms the full chain works:
cluster Gateway -> port-forward 4002 -> bot -> paper account.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:59:27 +02:00
jgrusewski
63d4f7f61d fix(ibkr-bot): entry-from-zero uses 0.5% floor, not full 3% hysteresis
The 3% hysteresis blocked the IBIT crypto sleeve (target 2.3% < 3%) from ever entering. Hysteresis
is for damping rebalances of existing positions; first entry from zero should use a small floor.
Now all 6 instruments deploy. Verified live against the cluster ib-gateway paper account ($1M NLV):
all 6 orders present (SPY/IEF/GLD/PDBC/DBMF/IBIT). Connected via kubectl port-forward svc/ib-gateway 4002.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:50:49 +02:00
jgrusewski
ba543ea85e feat: IBKR paper-trading bot for the adaptive multi-strat book (ib_async)
Runs the book on IBKR PAPER (realistic fills, no capital): gets target weights from the harness
(Yahoo), connects to IB Gateway/TWS paper, rebalances the ETF book (SPY/IEF/GLD/PDBC/DBMF + IBIT
for crypto = no exchange counterparty tail). Default dry-run (shows intended orders); 'rebalance'
places on paper. Hysteresis 3% of NLV. User runs Gateway + logs in (their creds); bot connects on
127.0.0.1:4002 (paper Gateway) / 7497 (TWS). Best paper-forward: realistic fills + real API, zero
risk. Setup steps in the docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:20:55 +02:00
jgrusewski
e35b79a531 spec: add concrete micro-live runbook (Appendix A)
Phase-2 runbook: $500-1k unlevered, all-in-one-brokerage fractional ETFs (SPY/IEF/GLD/PDBC/DBMF +
IBIT for crypto -> removes the exchange counterparty tail). Get weights via multistrat_paper.py
weights 1000, place manually, rebalance MONTHLY at micro scale (weekly deltas are rounding-dominated)
with 5% hysteresis, track actual-vs-intended. Gate to scale (~4-8wk): live ~= paper, cost <20bp,
clean ops. Objective is execution validation not return (pennies on $1k). Validation context: +1.20
in-sample / +1.41 holdout-OOS / +0.96 20yr (survived 2008/2020/2022) / bootstrap p5 +0.62.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:50:15 +02:00
jgrusewski
40c6e2f2ab test: multi-strat 20yr robust (survived 2008/2020/2022) + bootstrap CI (p5 +0.62)
Faster confidence without waiting forward: long-history (2006-2026, 20y incl all crises) on
SPY/IEF/GLD/DBC+trend (conservative, no DBMF/crypto). Full Sharpe +0.96, maxDD -9.9%; survived
EVERY crisis (2008 +1.1, 2020 +1.4, 2022 -0.2), shallow DDs; 16/21 yrs positive. Block-bootstrap
CI: Sharpe p5 +0.62 / median +0.99 / p95 +1.34, P(>0.5)=99%. 2006-2018 quasi-OOS held +1-2/yr.
This is the weaker version (full book +1.20/+1.41 OOS). Robust across 20y + every crisis with
positive CI floor -- strongest/fastest-confirmed deliverable. Faster-confidence levers: longer
history, bootstrap, micro-live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:47:32 +02:00
jgrusewski
4817bd4e7c test: FinBERT news-sentiment (real LLM, real FNSPID data) -> dead for retail
User wanted to download a small open LLM and test news. Did it: FinBERT scoring 10408 FNSPID
headlines (7 tickers, 1332 obs). corr(sentiment, same-day ret) +0.082 (weak, contemporaneous);
next-day +0.026 (~0); next-day trade net 10bp OOS Sharpe +0.05 (dead). News real but reaction
INSTANT (HFT-captured); no tradeable next-day drift for retail; net of cost = nothing. Matches
Lopez-Lira/Tang + FinGPT research. Model was never the constraint -- market structure is. Closes
the news/LLM thread; deployable answer remains the adaptive multistrat book (news = risk-managed).
(note: pin transformers<5 for torch 2.6.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:13:54 +02:00
jgrusewski
a9a1c3c511 test: diversification maxed in base-6; more streams dilute (they're beta-repackaging)
What to do with diversification? Tested sqrt(N) lever: base-6 vs expanded-12 (+REITs/TIPS/HY/intl/
EM/vol-premium). Expanded WORSE (+1.20->+1.04, 2022 +0.1->-1.4): candidates are equity/bond-beta in
disguise -> concentrate not diversify. Trust layer manages edge-health not redundancy. Genuine
uncorrelated net-positive streams are scarce; base-6 already spans the distinct premia. Diversification
is maxed in the deployable book; the lever is capital, not more streams.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:50:27 +02:00
jgrusewski
247e469a31 test: cross-market connectedness = diversification, not alpha
User intuited markets are connected -> exploitable? Measured equity/bond/gold/commod/trend/crypto
(2019-26): contemporaneous corr low (+0.03 avg; equity-crypto +0.43, equity-trend -0.28), crisis
corr STABLE (didn't spike -> diversification held in stress), lead-lag ~0 (trade OOS Sharpe -1.01).
Connectedness is priced-in -> value is diversification (already in the book, validates it beating
60/40 low-DD), not a predictive signal. Same efficient wall for cross-market alpha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:46:38 +02:00
jgrusewski
ac01db39fa spec: update perf with validated real-ETF backtest (+1.20 Sharpe, -5.4% DD, every year positive)
Replaces the ~0.5-0.7 estimate with the validated 2019-2026 real-ETF backtest: +1.20 Sharpe
(realistic fwd ~0.8-1.0), -5.4% maxDD, positive every year incl 2022, beats 60/40 (+0.87/-21%)
on Sharpe AND drawdown. Robust signals: low maxDD + every-year-positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:29:17 +02:00
jgrusewski
9a90720500 backtest: deployable ETF book Sharpe +1.20, maxDD -5.4%, every year positive (beats 60/40)
Backtested the EXACT live pipeline on REAL ETFs (SPY/IEF/GLD/PDBC/DBMF+BTC), 2019-2026 multi-regime.
Adaptive unlevered: Sharpe +1.20, ann +6.1%, maxDD -5.4%, positive EVERY year incl 2022. Beats
60/40 (+0.87, -21%) and equity (+0.85, -34%) with ~1/4 the drawdown. Better than the ~0.7 framing
because real ETFs (DBMF/PDBC) >> roll-zeroed futures proxies + adaptive risk layer. Caveats: Sharpe
partly low-vol-flattered (maxDD -5.4% + every-year-positive are the robust signals); 2019-26 favorable
for premia; mild param-contamination -> realistic fwd ~0.8-1.0. Modest absolute return (~.1k/yr on
5k, very smooth); lever for $ is capital. The genuine deliverable of the search, validated on real
instruments across regimes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:28:37 +02:00
jgrusewski
ce8f8cbd46 test: ML vol-nowcaster (Mamba2/CfC/TLOB proxy) doesn't help risk layer
User asked if advanced architectures help. Tested ML vol-nowcaster (GB proxy) vs EMA for risk-layer
de-lever timing: GB OOS vol-corr +0.09 < EMA +0.18 (EMA captures clustering better); downstream
identical Sharpe +0.72, GB maxDD slightly worse. ML doesn't help -> CfC/Mamba2 won't either (more
overfit). Return-prediction already dead (IC 0.004); TLOB's order-book signal is latency-gated.
Fancy architectures add nothing; engine value = simple adaptive risk/allocation. Simple beats fancy,
confirmed once more. Adaptive multistrat book (EMA-vol, ~0.72 Sharpe, -8.5% maxDD) is the answer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:14:25 +02:00
jgrusewski
ba7e7dca79 feat: adaptive multi-strat paper-forward harness (spec Phase 1)
Full 6-stream adaptive book live paper-forward: SPY/IEF/GLD/PDBC/DBMF + BTC-USD via Yahoo, with
edge-decay trust allocation + adaptive risk layer (EMA vol/Kelly-floor/self-recovering DD/z-score
corr), unlevered. status/run/weights modes, forward-start, idempotent, catch-up. Cron 12/15/18 UTC.
Live weights sensible: trust down-weights bond(7%)+crypto(2%), gold/commod/trend healthy(~23%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:02:51 +02:00
jgrusewski
60e900e43a spec: adaptive multi-strat book — deployable (the honest endpoint)
Diversified, edge-decay-adaptive, adaptively-risk-managed, UNLEVERED multi-asset ETF book
(SPY/IEF/GLD/PDBC/DBMF + small BTC). ~0.5-0.7 Sharpe, ~-7-12% maxDD, ~$2-3k/yr on $35k, scales
with capital. Edge-decay-trust allocation + adaptive risk layer (EMA vol, Kelly-floor, continuous
self-recovering DD de-lever, z-score corr, leverage-floor) = the foxhunt engine as risk/allocation
brain (validated). Unlevered because retail financing (6-7%) kills the leverage benefit (1x Sharpe
+0.48 vs 2x +0.14); the hedge-fund moat is cheap prime-brokerage financing, structural not algo.
Premium harvest, not alpha; engine value = risk-mgmt. Phased rollout: paper-forward -> micro-live ->
scale. The honest deployable endpoint of the whole search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:51:37 +02:00
jgrusewski
be084b5154 result: hedge-fund reframe -> moat is cheap leverage, not the strategy
Multi-strat with foxhunt's own ideas. (1) Combine uncorrelated premia: ~0.72 Sharpe 2019-26 but
~=60/40, only when streams net-positive (traditional 2010-26 combine +0.51 < equity +0.68 = dilution).
(2) Edge-decay-trust allocation (Page-Hinkley theta, resurrection) genuinely helps: +0.16->+0.27,
correctly down-weights decayed streams. (3) Static risk layer crushed returns (one-way latch);
ADAPTIVE layer (continuous self-recovering DD de-lever + Kelly-floor + z-score corr + EMA vol)
beat it (+0.03->+0.14, maxDD -18.7->-14.5) -- value is drawdown control. (4) THE MOAT = cheap
financing: adaptive 1x Sharpe +0.48 vs 2x +0.14; retail 6-7% margin kills leverage benefit. Funds
lever ~0.7 Sharpe only via prime-brokerage SOFR+1-2%. Deployable best = ~1x adaptive-risk-managed
diversified book (~0.5-0.7 Sharpe, unlevered), scales with capital. Foxhunt ideas improve execution
(validated); engine value = risk-mgmt not alpha. Ceiling ~0.7 ironclad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:48:46 +02:00
jgrusewski
764fd99480 correction(crypto): cross-venue arb is MARGINAL not breakthrough (honest fixed-pair OOS)
The +14.9 OOS Sharpe was the max-min optimism (assumes daily free re-positioning to optimal venue
pair). Honest fixed-pair version (hold the entry-chosen pair, book its actual funding diff): OOS
Sharpe collapses +14.9->+0.8, OOS ann +15.3%->+1.1% (realistic ~breakeven net of cost), 0/12
configs robust OOS, feb month -8.2. Capture 0.68 was the tell (32% reshuffle loss). Cross-venue
arb joins the cost-walled/in-sample-only list. User's OOS insistence caught it. Only deployable
thing remains simple 60/40 (~0.7). Every crypto-funding variant marginal once honestly tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:10:13 +02:00
jgrusewski
e731c2fc2f result(crypto): cross-venue funding arb PASSES clean OOS (the breakthrough)
Full-year (331d Binance-HL) clean OOS: config picked on first 60% (IS Sharpe +12.0) applied
BLIND to last 40% -> OOS Sharpe +14.9 (held), OOS +15.3%/yr, 14/14 configs robust, all 4 OOS
months positive (+11..+18), capture 0.68. The ONLY edge in the whole search to clear the clean
OOS horde that killed PEAD/equity-ML/AI4Finance. Market-neutral, no spot leg, persistent, carry
not prediction. HONEST: Sharpe 6-15 inflated (low ~1% vol -> realistic /2.5-4 -> ~3-6; real
number is return ~8-15%/yr) + sim books daily max-min assuming optimal-pair-held (capture 0.68 =
~32% reshuffle loss) -> needs held-pair-realized fix for true number, then micro-live + counterparty
mgmt. Edge EXISTS and is OOS-proven; deployable magnitude ~8-15%/yr pending fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:08:08 +02:00
jgrusewski
2b36929b5f test(crypto): clean OOS for cross-venue arb (IS-pick config -> OOS-blind apply)
The hysteresis params were chosen by looking at the data = in-sample. Clean OOS protocol (same
that killed PEAD): pick best config on first 60% by IS Sharpe, apply EXACT config blind to last
40%. Reports IS-best -> OOS Sharpe, robustness (configs IS>2 that also OOS>1), OOS per-month.
Decisive: OOS holds -> real; OOS collapses -> in-sample fit. Monitor armed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:03:30 +02:00
jgrusewski
ee2215f4d3 feat(crypto): cross-venue harness -> hysteresis (deployable) + deep full-year history
(1) Live cross_venue_funding.py paper harness now uses HYSTERESIS (enter >10bp/day, hold until
spread decays <5bp) instead of naive daily top-K -> tracks the deployable low-turnover version
(naive was cost-killed -4.5 Sharpe; hysteresis nets +10-14%/yr). State reset. (2) fetch_xvenue_hist2
now paginates Binance fundingRate -> full ~330d history (was 67d) so Binance-HL overlap = full year
(330 days) for the regime test across ~11 months. Monitor armed for the full-year backtest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:58:24 +02:00
jgrusewski
027d73a504 research(crypto): cross-venue funding arb survives net-of-cost with hysteresis
Backtested the cross-venue funding arb on historical funding (Binance/OKX/Hyperliquid). Gross
+21%/yr, spreads persist (capture 0.71), but NAIVE daily rebalance is cost-killed (net Sharpe
-4.5, negative every month). HYSTERESIS (hold winners until spread decays, entry>10bp/exit>5bp)
flips net to +10-14%/yr market-neutral (Sharpe +11-15, but inflated by ~1% vol + idealized fills;
realistic ~3-6 / return ~10-14%). Turnover is the swing factor. First edge of the whole search to
survive the net-of-cost horde -- market-neutral, persistent, no spot leg (solves hedgeability),
operational not predictive. Caveats: 94d/one period, idealized fills, counterparty. Next: switch
live harness to hysteresis, deepen history to full year, micro-live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:33:28 +02:00
jgrusewski
4f73e99225 feat(crypto): cross-venue funding-arb scanner + paper-forward (solves hedgeability)
Short high-funding-venue perp + long low-funding-venue perp (same coin) = price-neutral, collect
the funding DIFFERENCE, NO spot leg needed -> fixes the single-venue hedgeability block. Binance/
Bybit/Hyperliquid bulk funding, normalized to daily (HL is hourly!). Sanity-filtered (liquid >$10M
both legs, exclude |funding|>50bp/day distress/artifacts -- raw scan top was 9000%/yr delisting
junk). Clean spreads: ETH ~20%, ARB/ONDO ~20%, up to ZEC 167%; 16 coins >5bp/day. scan/run/status,
idempotent daily, cron 9/12/15/18 UTC. Decisive open question = PERSISTENCE (snapshot != edge;
paper-forward measures whether spreads last long enough to collect net of ~10bp fees).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:32:35 +02:00
jgrusewski
9bf67e731d research: AI4Finance debunked + PEAD dies OOS (efficient-market wall confirmed again)
3 omnisearch researchers + clean PEAD test answer 'why does AI4Finance find what we cant':
they dont. FinRL flagship (Sharpe 1.30) = single-split, slippage-free, no-deflation, bull-market,
survivorship, hand-coded crash rule; their own people (Gort/Liu AAAI'23) published the overfitting
rebuttal; zero live track record. ML-trading decays 73%+ backtest->live, faster for complexity.
PEAD tested properly (real Nasdaq surprises x DBEQ, leak-free, 23bp cost, OOS): full-sample looked
good (20d +0.29% t=2.9) but pure in-sample bull-beta -> OOS NEGATIVE every horizon; surprise-size
signature fails. Strongest classic anomaly dies OOS. Untested real pulse left: prediction markets
(uncorrelated). Tooling: fetch_earnings.py, pead_real.py, dbeq_symbology resolved (17605 tickers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:15:22 +02:00
jgrusewski
f203998613 feat: 60/40 paper-forward harness (the ~0.7-Sharpe non-crypto baseline)
Local CLI paper-forward test of 60% SPY / 40% IEF (ETF analog of the ES/ZN 60/40 backtest,
+0.72 Sharpe), mirroring the funding harness. Free Yahoo adjusted closes (dividends+coupons =
true total return). Subcommands snapshot/run/status. Books each real trading day once with
catch-up (handles weekends/missed runs); forward-start (no history backfill). Idempotent via
last_date. Daytime cron 12/15/18 UTC. Always-invested baseline -> confirms forward Sharpe
tracks the ~0.7 backtest, the clean no-phantom option available regardless of crypto regime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:55:44 +02:00
jgrusewski
107bcc6648 fix(crypto): MANDATORY hedgeability filter — funding edge was partly phantom
Phase-2 setup (orders command) revealed 0/7 qualifying coins were hedgeable: all perp-only (no
spot leg = cannot build the delta-neutral hedge). Diagnostic: of 191 liquid crypto-native perps,
135 hedgeable / 56 perp-only. HEDGEABLE median funding -0.12bp/day, 0 of 134 clear 5bp (arbed flat
by existing cash-and-carry). PERP-ONLY median +2.59bp/day, all 8 qualifiers live there. The carry
survives ONLY where it can't be hedged. crypto_pit backtest never checked hedgeability -> the
~2-3.6 Sharpe was inflated by un-capturable perp-only funding. Fix: universe() now requires a spot
market (hedgeable only). Current regime: 0/135 hedgeable qualify -> nothing to harvest (deleverage
arbed flat). Real edge = classic basis trade on hedgeable majors: regime-dependent (rich in bull
leverage, ~zero now), more competed, lower true Sharpe than backtest. Phase-2 deploy correctly
BLOCKED by reality, not process. State reset (prior bookings were on un-hedgeable coins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:03:41 +02:00
jgrusewski
24ac921cd3 fix(crypto): daily cron reliability — once-per-UTC-day guard + daytime multi-attempt
Nighttime cron (01:17 UTC) silently never fired: workstation suspends at night and standard
cron does not catch up missed jobs on wake (uptime hides suspend). Fix: (1) idempotent
once-per-UTC-day guard in 'run' (last_run_date in state) so multiple attempts cannot double-book;
(2) cron now fires at 09:10/12:10/15:10/18:10 UTC daily -> first attempt while the machine is
awake books the day, the rest no-op. Robust to the machine being off at any single slot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:35:46 +02:00
jgrusewski
42e9621c47 feat(crypto): consolidate funding harvest into a local CLI (snapshot/run/status/gate/log)
Replace the agent/cloud approach with one self-contained local CLI over Binance's public API.
Subcommands: snapshot (live liveness check), run/paper (daily step, cron-compatible), status
(book + cumulative), gate (Phase-1 -> Phase-2 assessment vs backtest band), log. No key, no
capital, no agents. Crypto-native filter retained. Cron 'paper' alias preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:53:13 +02:00
jgrusewski
04e7a61320 fix(crypto): restrict funding harness to crypto-native perps (match validated universe)
Live Binance fapi now lists tokenized-stock perps (MSTR/CRCL/INTC/AMD/SOXL, underlyingType=
EQUITY) which dominated the high-funding qualifiers (12 of 19) but were NEVER in the validated
crypto_pit backtest. Filter to exchangeInfo underlyingType==COIN (crypto-native) to keep the
paper-forward test measuring the edge we actually proved. Post-filter: 7 of 212 liquid qualify
(thin = current deleverage regime, filter sitting mostly in cash as designed). Tokenized-stock
funding may be a separate unvalidated carry; not contaminating the Phase-1 gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:41:20 +02:00
jgrusewski
902eb1c85f feat(crypto): paper-forward harness for funding harvest (spec Phase 1)
Daily no-capital forward test. Pulls live Binance USDT-perp funding (public API, no key),
applies validated filter (liquid + trailing-30d mean daily funding > 5bp, hysteresis), computes
intended equal-weight delta-neutral book, tracks realized funding on prior book -> forward track
record on unseen data. Modes: paper|status. cwd-independent, state-persisted, cron-ready.
Day-1 live run: 19 of 247 liquid perps qualify (regime filter working in real time). Cron added
at 01:17 UTC daily (after 00:00 funding settlement). Validates the one open question: does
positive funding carry persist forward? Honest scope: tracks gross carry + turnover; does NOT
model basis vol (needs live fills) or counterparty risk (un-simulable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:30:03 +02:00
jgrusewski
df5c591441 spec(crypto): funding harvest — clean OOS validated + deployable spec
Clean OOS test (filter chosen on 2019-24, applied blind to 2025-26): IS-best filter holds
OOS Sharpe +5.1; 9/12 strong-IS filters also OOS-positive; naive no-filter FAILS OOS (-5.8)
= validated, not curve-fit. Wrote deployable spec: delta-neutral long-spot/short-perp harvest,
tf30>5bp regime filter, equal-weight, daily rebalance, 10bp cost budget. Counterparty/exchange
tail (-100pct) is THE risk (Sharpe-blind) -> venue selection + collateral spreading + withdrawal
discipline + deleverage kill-switch. Realistic ~2-3.6 Sharpe market-neutral (after basis-vol
haircut). Phased rollout: paper-forward -> micro-live -> small-live -> scale, gate each.
The one validated edge past the 0.7 ceiling; price of admission is crypto + counterparty risk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:25:23 +02:00
jgrusewski
9c2c38eb5d feat(crypto): funding carry liveness — ALIVE (regime not decay), regime filter rescues 2025-26
Decisive liveness investigation. Decay diagnostic: positive-funding yield intact (mean_pos
3.0-3.4bp in 2025-26 = same as 2019/2023), NOT compressed. What changed is BREADTH: frac_pos
0.88(2024)->0.59(2026), avg funding negative = deleverage regime. Naive harvest whipsawed
(2025-26 Sharpe -5.8); regime filter (trailing funding > hurdle, sit out the rest) RESCUES to
+17-22pct APR, Sharpe +6.7-9.1. Edge is alive = cross-sectional selection, not death. Caveats:
raw Sharpe inflated by missing basis vol (realistic ~2.5-4); hurdle needs OOS confirm;
counterparty tail unchanged. Strongest deployable edge of the search: ~2.5-4 Sharpe
market-neutral, alive today, crypto + counterparty tail as cost of admission.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:20:04 +02:00
jgrusewski
c10ebe0257 feat(crypto): delta-neutral funding harvest — the one edge that breaks 0.7
Investigated crypto funding carry (researchers' flagged retail path to >1 Sharpe). DELTA-NEUTRAL
cash-and-carry (short perp+long spot, collect funding, price cancels) -- different from the
directional carry trap. Funding positive 75pct of time. Backtest net of cost: APR 9.9pct, raw
Sharpe +5.68, maxDD -7.4pct, survives to 20bp cost, worst month -1.5pct (delta-neutral held).
Discipline finds: (1) real; (2) 5.68 inflated (funding-only model, 1.8pct vol; real basis vol
-> honest ~2-3 Sharpe = literature's 1.5-2.5); (3) regime-dependent + DECAYING (negative 2022,
2025, 2026). Caveats: counterparty/exchange tail (FTX -100pct, not in backtest = real killer),
decay, operationally real. The ONE genuine path past the 0.7 ceiling -- but it's crypto +
counterparty tail is the price of admission. The journey converges: higher Sharpe exists,
reachable, but lives where the user didn't want to go with a tail the Sharpe doesn't show.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:16:46 +02:00
jgrusewski
0fc3eac2ab feat(harvest): Sharpe-ceiling question answered — triangulated ~0.7-0.9 for retail
4 omnisearch researchers + 60/40+trend-sleeve test. Three independent confirmations converge:
(1) our lab: 60/40+trend cuts maxDD (-20.5->-16.5pct) but no Sharpe lift (naive trend=0.04
standalone); (2) literature: retail ceiling ~0.7-1.0, AQR QSPIX 0.46 live vs 0.7 target,
~73pct backtest->live haircut, 35-58pct alpha decay; (3) adversarial: Sharpe 2-6 is
infrastructure-gated (HFT/Medallion), not software. Exceptions: crypto funding carry (only
retail ~1.5-2.5 path, but crypto+tail); proper trend ETF (DBMF) on 60/40 (~0.8 + lower DD).
Vol-selling = Sharpe mirage. Deployable best: 60/40 (~0.71) or 60/40+DBMF (~0.8). The lever
for more dollars is capital, not Sharpe. Definitive: ~0.7-0.9 is the retail ceiling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:00:27 +02:00
jgrusewski
244ccaaf0b feat(harvest): premium-harvesting tested — simple 60/40 beats the sophisticated system
The strategy class we never tried: harvest structural premia + risk system, not predict.
Diversified futures + risk-parity/vol-target/trend; clean 5-asset-class version (ES/ZN/GC/CL/BTC).
Result: plain 60/40 (Sharpe +0.72, 2010-2026) BEATS risk-parity+vol-target (+0.46) and RP+trend
(+0.33, trend hurts); 5-asset+crypto RP only ties 60/40 and loses to buy-hold equity. Meta-pattern
now complete in BOTH games: simple beats/equals sophisticated in prediction AND harvesting.
Constructive deliverable: a simple premium harvest (60/40 / risk-parity) IS a real deployable
robust strategy (~0.5-0.72 Sharpe, low DD, no prediction, minimal complexity). The engine's
sophistication was never the return-generator -- deployable path is light (harvest + risk overlay),
engine's value is infra/discipline/product. (Also: CAISO intraday gate blocked by OASIS plumbing.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 00:42:32 +02:00
jgrusewski
7457ef679c feat(energy): repurpose the engine — battery arbitrage is real, growing, forecasting-shaped
First repurpose target with real legs. DE day-ahead battery arbitrage (free Energy-Charts,
2019-2025, 2464 days). 4h/1MW/85pct: perfect-foresight e93k/yr/MW (>> e50-80k viability),
naive only e10k -> forecasting/RL gap = 89pct of value. Per-year GROWING (e28k 2019 ->
e141k 2025) = not competed away (batteries slow to build = operational moat). Structurally
opposite of trading: real economic value (not zero-sum), growing (not arbitraged), and 89pct
of value is forecasting/RL = exactly the engine's job. Caveats: perfect-foresight is upper
bound (realistic ~60-85pct capture -> ~e55-80k/yr/MW); needs a battery (capital) or sell the
dispatch software to operators (engine-as-product) or aggregator. NEXT: forecaster + RL
dispatch, measure realized-capture vs foresight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:58:54 +02:00
jgrusewski
6c113b0df2 feat(surfer): ML multi-signal combination — definitive (market not tools)
Ran the quant-fund method (gradboost+ridge+IC+equal combining 13 weak cross-sectional
signals) on liquid US equities. Single-split gradboost looked amazing (OOS +1.14, DSR 0.62)
but leak-free WALK-FORWARD diagnostic: gradboost OOS predictive IC = 0.0041 (statistically
ZERO; no leak; successful equity ML is 0.02-0.05). Single-split was overfit; WF +32 Sharpe
was a variance-degeneracy; equal/ridge/IC all fail OOS. The ML combination does NOT work on
efficient equities -- not because the ML is bad (works perfectly) but because there's no
signal (IC 0.004) to combine. DEFINITIVE answer to 'millions of LOC of ML, why nothing?':
the ML is not the missing piece, MARKET ACCESS is. Pointed the actual RenTech/TwoSigma
method at liquid equities -> IC 0.004 = noise. ML amplifies signal, cannot create it;
efficient markets have none. Crypto (less-efficient) is the one place the same machinery
finds robust signal. Sophistication was never the bottleneck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:44:43 +02:00
jgrusewski
d43fb5e61f feat(surfer): equity-index VRP frontier — real+affordable, measurement needs engineering
Bought XSP (mini-SPX) options ohlcv-1d 2013-2026 = $66.86 (hard-capped $70, get_cost-gated,
year-chunked vs 504, 2025 backfilled) + SPY $0.01. CORRECTION: index VRP is NOT $900 (that was
SPY root); clean instrument XSP $66.86/13y or SPX $143/13y. THREE measurement attempts all gave
spurious NEGATIVE VRP (RV 28-33% vs implied ~16%) = MEASUREMENT ERROR not finding (contradicts
decades of SPX VRP evidence). Bugs: noisy parity underlying; ~469/2800 days survive -> multi-day
gaps inflate RV; SPY!=XSP divergence corrupts ATM. Root: XSP EOD ohlcv too sparse to reconstruct
clean underlying+ATM-IV. Implied (16.7%) reads right. Frontier real+affordable+in-hand but proper
extraction is a real options-quant build (denser SPX +$76 or IV-surface w/ quotes), not a gate.
Did NOT record -2.7 as a result (artifact). Crypto momentum+VRP remains only deploy-grade edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:13:34 +02:00
jgrusewski
0c5af2d9a0 feat(surfer): +0.74 autopsy — real anomaly, un-harvestable for our constraints
Decomposed the equity low-vol +0.74. Deployable half DEAD: long leg (low-vol, borrow-free)
alpha +0.04 / DSR 0.02 = just cohort beta. ALL edge is shorting high-vol decile (needs
expensive borrow). Episodic: top-10 days = 133pct of P&L (a few crash events). Just broke:
2026 = -2.72 (anomaly reversed). Beta-neutral residual +0.98 = genuine betting-against-beta
anomaly but lumpy/short-only/recently-inverted. Turnover low (cost was fair). Verdict: real
anomaly, un-harvestable by a small retail borrow-constrained book. Detail killed the lead
honestly. Crypto momentum+VRP remains the only deploy-grade edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:21:39 +02:00
jgrusewski
30f944f015 feat(surfer): equity low-vol gauntlet — real structure, marginal after realistic cost
Full gauntlet on the lottery-aversion lead (liquid high-vol cohort). BASE L/S +0.74/OOS
+0.72 but: name-bootstrap frac>0 only 0.77 (fragile vs momentum's 1.00); edge lives at
$5-20M floor (smaller names, real spreads > the 10bp assumed), dies at $50M; the L/S edge
is mostly shorting hard-to-borrow hype names (30%/yr borrow -> +0.04 gone); long-only
realistic = +0.33, DSR 0.07 (not significant). corr to crypto book -0.05 (uncorrelated ->
would diversify IF real). DSR 0.22/0.07 -- neither clears 0.5. Closest non-crypto market,
theoretically sound, uncorrelated -- but marginal after realistic borrow+small-name cost,
NOT deploy-grade. A lead on the shelf, not a strategy. Crypto momentum+VRP remains the only
deploy-grade edge. Re-confirms the boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:55:17 +02:00
jgrusewski
05a81c5e5f feat(surfer): the challenge — liquid high-vol cohort fits (low-vol/lottery-aversion)
Reasoned about WHERE our edge structure fits (reflexivity+breadth+low-cost overlap) ->
picked the liquid high-vol equity cohort (>$10M/day liquid AND high realized vol =
reflexive story-stocks, ~87 names) = equity analog of crypto's reflexive cohort, avoiding
both walls. Momentum/reversal still negative (regime+efficiency), but LOW-VOL (lottery-
aversion) FITS: L/S net +0.74, OOS +0.72, CPCVmed +0.73, positive 3/4 years, LongOnly +0.33.
First equity signal to survive net cost (broad test had lowvol +0.02). Theoretically sound
(fade overpriced hype/lottery names in the reflexive cohort). LEAD not deploy-grade: DSR
0.28<0.5, short 3.2y data, thin cohort, short-side borrow cost unmodeled. Non-crypto,
low-cost, Databento. Next: full gauntlet (bootstrap/cohort-robustness/borrow/deflation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:40:58 +02:00
jgrusewski
2065c98c25 feat(surfer): small/mid-cap equity factor gate — no factor survives small-cap cost
DBEQ.BASIC all US equities daily 2023-2026 ($67.85, 17305 instruments). Small/mid-cap band
(excl top-50 mega, $2M floor, ~576 names/day), weekly rebal+smooth, illiquidity-scaled cost
(30-150bp). NO factor survives net: momentum negative even gross (2023-24 momentum-crash);
reversal gross +0.15 eaten by cost -> NET -0.86 (Amihud paradox); low-vol best but NET ~0
(DSR 0.03). Didn't even charge short-borrow. Confirms Databento tradeable universe (equities+
futures) is efficient+cost-walled -> no retail edge. Honest tension: cheap-to-trade markets too
efficient; the inefficient market with real edge (crypto) is the disliked one. Caveat: 3.2y only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:24:43 +02:00
jgrusewski
5ed94ddc97 feat(surfer): cross-exchange funding frontier (a) — fails (carry trap re-found)
Fetched Bybit funding (OKX fetch failed, so Binance+Bybit) for 18 majors. Cross-venue
DISPERSION & Binance-premium signals all negative. Only positive = carry_multivenue +0.84
(OOS+0.80, corr_mom+0.01, marg_t+2.58) -- BUT Binance-only control on the SAME 18 majors =
+0.69, proving the edge is MAJOR-COIN SELECTION not the cross-venue angle (multi-venue adds
only +0.15 noise-reduction). This is the known cherry-picked carry trap (majors +0.8 ->
broad-clean -0.03). Almost re-fooled by the seductive number; the Binance-only control
disconfirmed it. Orthogonality doesn't rescue universe-fragility. Databento has no crypto/
on-chain data either. Both orthogonal frontiers closed. Product = residual-momentum + VRP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:55:55 +02:00
jgrusewski
a7d6671dea feat(surfer): Reflexivity Harvester design — killed by marginal-alpha gate
Synthesized all findings into a novel crypto-native design (residual-momentum long +
exhaustion/death-weighted short + vol-regime gross-gate), spawned 4 research agents,
ran the decisive test the critic+exhaustion-researcher both named. Exhaustion-short is
momentum RE-SPELLED: corr +0.82 to residual momentum, marginal alpha t -0.37 (negative),
combined OOS +0.50 < momentum-alone +0.77 (adding it HURTS). Momentum ALREADY harvests
the crypto death-alpha (it already shorts dying coins -- the survivorship finding);
explicit death-features double-count. Regime-gate untestable (n~4 crashes). Ship Comp-1.
Product unchanged: residual-momentum (OOS +0.77, CPCVmed +0.82, DSR 0.57 deflated-by-50)
+ tail-managed VRP. The marginal-alpha gate killed our own creative design = discipline working.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:43:42 +02:00
jgrusewski
b664673df3 feat(surfer): Fear&Greed gate — sentiment regime-dependent, not robust
F&G market-timing: full-sample IC negative (fear->continuation), recent OOS positive,
IS/OOS sign-flip (-0.65->+0.75), 2026 flipped -0.8 = regime-dependent, not robust.
Free non-price is mostly market-wide timing (low-breadth, regime-prone); cross-sectional
on-chain (the valuable kind) needs paid data. Non-price free frontier mostly dead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:35:37 +02:00
jgrusewski
a9d1af9ec8 feat(surfer): minute-horizon gate — deep ML (Mamba2/CfC/TLOB) has no fuel
Before building deep sequence models, tested whether cost-surviving signal exists at
the minute horizon for them to extract. 12 perps, 60d free 1m klines (OFI proxy from
takerBuyQuote/quoteVol), features -> forward 1/5/15min returns net of 5bp taker. ALL
features negative net (best gross ~0.2bp vs 5bp cost = 25x gap); reversal IC -0.027 and
vol-continuation +0.009 are real but ~25x too small; OFI/taker-imbalance ~zero predictive
power. -> No minute-horizon signal -> Mamba2/CfC/TLOB have nothing to extract -> don't
build (no model lifts IC 25x). Crossing-cost wall holds in crypto (sec-ES 100x, min-crypto
25x); maker=adverse-selection wall, full-LOB=colocation territory. Gate saved a multi-week
GPU build for $0/10min. Product remains the daily two-sleeve book.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:09:04 +02:00
jgrusewski
bc4cc46775 feat(surfer): residual (beta-stripped) momentum upgrade — net +0.40->+0.57 @$20M
Phase-1 upgrade applied to the PoC base sleeve: rank residual momentum (each coin's
return stripped of its rolling beta to the equal-weight market) instead of raw momentum.
Removes high-beta coins dominating the sort by market co-movement. Backtest: gross
+0.82->+0.99, net @$20M +0.40->+0.57, capacity $5M +0.60->+0.78, CPCV-med +0.47->+0.59,
combined two-sleeve book +1.56->+1.66. Same edge, cleaner construction. Live panel depth
bumped to cover the 60d beta window. No lookahead (beta + cumsum use closed bars).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:41:40 +02:00
jgrusewski
74b9d092ad feat(surfer): Phase 1 RL-gate — no rich signal set; RL not justified (+ resid-mom upgrade)
Tested 5 theoretically-distinct new crypto signals (residual-momentum, MAX/lottery,
idiosyncratic-vol, Amihud-illiquidity, acceleration) on PIT top-50 with full gauntlet
(OOS/CPCV/deflation/coin-bootstrap) + correlation to momentum. NO new DISTINCT validated
signal: survivors = momentum + residual-momentum (corr 0.95 = same edge). Others fail
(lottery/accel negative; ivol/amihud fail bootstrap-robustness, amihud closest miss
corr+0.20/boot0.75). DECISIVE: validated signal set is thin (one alpha + VRP diversifier)
-> RL/PPO has no rich interacting set to combine -> would not beat the linear baseline OOS
-> don't build the RL (the falsifiable gate returns NO; avoids the 65th-commit trap).
SMALL WIN: residual (beta-stripped) momentum +0.72 > raw +0.57 -- better base construction.
Product = simple two-sleeve book. RL needs non-price signals to ever justify itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:36:21 +02:00
jgrusewski
f986510099 feat(surfer): wire tail-managed VRP into PoC as second sleeve (two-sleeve book)
backtest now reports momentum + VRP (tail-managed, corr -0.04) + COMBINED book
(momentum 70% / VRP 30% risk) Sharpe +1.56, all years positive. paper marks BOTH
sleeves + combined equity each run; status shows all three + live Sharpes. VRP gate
('don't sell vol when DVOL rising/elevated') runs live -- currently 'flat (gated)'.
cwd-independence fixes: pit_sweep.load + _vrp_calib now use abs paths so the cron paper
run calibrates VRP correctly from $HOME (was silently falling back to zero-VRP). Live
panel depth bumped to cover the 60d VRP gate window. Honest: VRP +2.85 is the no-Greeks
proxy (real ~1-1.5) so combined is optimistic (~0.8-1.0 realistic) -- still > momentum alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:29:40 +02:00
jgrusewski
ad7feb61ff feat(surfer): tail-managed VRP — real second sleeve (with honest haircut)
'Don't sell vol when implied vol rising' (point-in-time, no lookahead verified) takes
crypto short-vol baseline +1.03 (skew -8.3, 2025 -0.26/2026 -0.77) to combo +2.89,
ALL years positive (2026 +2.26), worst -21sigma->-17sigma. Effect robust across gate
variants (rising/level/combo/loss_cap all fix 2025-26) = real economic effect (implied
leads realized), not one lucky rule. Confirms the -21sigma tail + recent decay were the
same mis-timing. Diversifier: corr -0.04, combined +2.57. HONEST HAIRCUT: +2.9 optimistic
(gate-selection best-of-5 + no-Greeks proxy idealization + residual -15sigma tail + 5y
sample) -> realistic ~1.0-1.5; but corr+edge robust -> combined ~1.37 > momentum 0.66.
Real second sleeve, size SMALL (tail reduced not gone). Best diversifier found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:17:09 +02:00
jgrusewski
9642ad3156 feat(surfer): crypto VRP diversifier check — first real diversifier, but tail+decay
Vol-risk-premium (short-vol BTC+ETH, Deribit DVOL implied minus realized, short-variance
daily P&L proxy). VRP Sharpe +1.03, CORRELATION to momentum -0.03 (genuinely uncorrelated),
combined +1.22 (~doubles momentum-alone +0.66). FIRST genuine diversifier found -- has BOTH
edge AND low-corr (futures trend had corr but no edge -> diluted). CAVEATS: (1) catastrophic
negative skew -8.32, worst-day -21sigma (short-vol blowup risk; Sharpe flatters; must
tail-manage + size small) (2) recent decay -- negative 2025/2026 (+1.22 is a 2021-24 artifact).
Real and promising, categorically better than futures, but pursue carefully not naive deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:13:06 +02:00
jgrusewski
dbbe8f7b22 feat(surfer): multi-asset futures test — uncorrelated but no edge (dilutes)
Expanded futures universe 22->39 liquid CME roots ($16.39 credits, 16y daily). (A)
breadth did NOT unlock an edge: XS momentum + TS trend all negative/zero, DSR 0
(efficient market, unlike crypto). (B) diversifier check: futures-trend +0.01,
crypto-momentum +0.88, CORRELATION -0.01 (genuinely uncorrelated) but combined +0.63
< +0.88 -> an uncorrelated sleeve with zero standalone edge DILUTES, not diversifies.
A diversifier needs low-corr AND positive edge; futures trend has only the former.
Caveat: roll-zeroing understates futures trend/carry (floor work ~+0.05-0.14 w/ proper
rolls) -> marginal at best. Crypto momentum still the only real edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:08:40 +02:00
jgrusewski
ec308346b2 fix(surfer): cwd-independent PoC + fixed-phase weekly rebalance (cron-ready)
- Rebalance keyed on FIXED calendar phase (epoch-day%7==0 -> Thursdays) instead of
  drifting days[0]%K, so backtest and live agree on rebalance timing.
- STATE path absolute (relative to script) -> paper mode runs from any cwd (cron-safe).
- Backtest unchanged (net +0.40@$20M, CPCV-med +0.47).
Installed system crontab (daily 12:07): marks paper PnL daily, rebalances Thursdays,
logs to data/surfer/paper_runs.log. Forward-test seeded inception 2026-06-06.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:55:42 +02:00
jgrusewski
d6eb5b2ada feat(surfer): end-to-end Python PoC (backtest|regime|paper|status)
One-file PoC of the validated crypto XS-momentum edge. ONE shared compute_weights()
drives backtest AND live (no skew); no lookahead; funding-as-PnL; sqrt-impact cost.
Backtest reproduces research (gross +0.83, net +0.43@$20M, capacity curve, turnover
9.8%). Regime diagnostic = clean NEGATIVE (no feature predicts momentum favorability,
IC~0 IS/OOS) -> de Prado meta-labeling/regime overlay empirically unwarranted, runs
flat-sized. Paper mode = live forward-test (Binance ASCII perps, market-neutral book,
intended trades + paper PnL persisted). Python is the right tool for weekly crypto MFT;
foxhunt Rust/CUDA is HFT-ES-specific (concepts reused, plumbing not).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:44:41 +02:00
jgrusewski
4336a71e26 feat(surfer): diversifier hunt — no robust partner (TS-trend real but stale)
Directional TS-trend (fixed earlier demeaning bug) is a real edge (bootstrap frac>0=1.00)
with low corr to momentum (+0.12) and lifts full-sample Sharpe +0.67->+0.71 -- but per-year/
OOS expose it as STALE (value all in 2021 +2.65; recent 2024 -0.16/2026 -0.89), so it DRAGS
the book OOS to +0.24 vs momentum's strong recent. 2022 crisis-alpha hypothesis failed
(choppy bear whipsawed trend). Within-crypto diversifiers exhausted (carry/lowvol/reversal/
size/TS-trend all fail) -> momentum is a robust single-sleeve edge; real diversifier needs a
different market/data. Discipline: full-Sharpe said add, OOS said stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:24:55 +02:00
jgrusewski
99b747ad2c feat(surfer): turnover optimization resolves capacity — deployable momentum spec
Weekly rebalance + 5d weight-smoothing + top-30 liquid: gross Sharpe +0.80->+0.94
(smoothing cuts whipsaw), turnover 30%->10.6%, capacity dead-by-$20M -> net +0.76/$5M,
+0.58/$20M, +0.38/$50M, viable ~$100M. top-30 = breadth/liquidity optimum. The
small-capacity verdict was a daily-rebalance artifact. DEPLOYABLE SPEC: crypto XS
momentum, top-30 liquid perps, 20d signal, weekly rebal + 5d smoothing, market-neutral,
net Sharpe ~0.6-0.8 at $5-20M, survivorship-confirmed. Single edge (no validated diversifier).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:20:12 +02:00
jgrusewski
a2cbed4a83 feat(surfer): capstone — low-vol fails battery; momentum is small-capacity
(2) Low-vol FAILS its full battery (standalone ~0/neg all windows, coin-bootstrap
frac>0=0.45 coin-flip) -> its earlier diversifier value was a winsor artifact ->
NO validated second edge, momentum stands alone. (1) Slippage/capacity model
(sqrt-impact eta=1, daily rebalance): book gross +0.62 but net M +0.41 / M +0.22 /
0M -0.11 / 0M -0.50 -> real but small-capacity (~$1-5M), lives in smaller alts.
Favorable caveats: eta=1 conservative, daily rebalance of 20d signal wastes turnover.
Key untested lever: weekly rebalance + smoothing + top-20 liquid -> ~5x less turnover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:17:33 +02:00
jgrusewski
7edeec9106 feat(surfer): proper (a)robust-construction + (b)diversifier research
(a) All 6 momentum constructions collapse to ~0.2 under aggressive +/-20% winsor
(momentum leans on large moves); volscaled/winsorinput degrade least, none robust to
the cap. (b) LOWVOL is genuinely uncorrelated (corr -0.13) and lifts the stressed book
+0.19->+0.34; TStrend correlated; carry/reversal/size don't help. Book=momentum+lowvol.
Honest deployable bracket ~0.2 (harsh winsor) to ~0.6 (fair); +/-20% blanket winsor
double-counts (death-spiral already excluded) so truth ~0.5. Deciding next: per-coin
ADV slippage model, not blanket winsor. Corrected the script's over-claiming verdict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:12:55 +02:00
jgrusewski
ca3877c328 feat(surfer): execution-realism stress — honest momentum magnitude ~0.4-0.6
Realism layers on PIT mom_20 TOPK=50: robust to 20bp (+0.59) and liquidity floor
(top-50 already deep, no effect); death-spiral exclusion +0.60. Winsorize +/-20%/day
cuts to +0.50 and weakens 2021/22 -> edge leans partly on large moves. COMBINED
realistic stress +0.18 (CPCV~0); HARSH negative. winsor likely over-conservative ->
true deployable Sharpe ~0.4-0.6, not the 0.76 headline. Real but modest edge; recent
years strongest. Deploy needs signal-robustness refinement + diversifying 2nd edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:07:31 +02:00
jgrusewski
7495eaa286 feat(surfer): point-in-time survivorship test PASSED — momentum is real
Built survivorship-free universe: 135 perps incl 28 known-dead (LUNA/SRM/MATIC...),
universe rebuilt daily as top-K by trailing dollar-volume (dead coins in while trading,
drop out after crash), no lookahead. mom_20 TOPK=30: full +0.76, IS+0.85/OOS+0.56,
CPCV-med +0.73, DSR 0.86, POSITIVE EVERY YEAR 2020-2026 incl 2022 +0.72. Counterintuitive:
survivorship was HIDING the edge (survivor-only 2022 -0.40 -> PIT +0.72) because momentum
shorts the dying coins and profits from crashes. Decisive survivorship confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:04:19 +02:00
jgrusewski
0d6d58428d feat(surfer): momentum robustness battery — crypto XS momentum is a robust real edge
Lookback sweep smooth+positive 5-120d (peak mom_20 DSR 0.95); coin-bootstrap 200
random half-universes frac>0=1.00, 5th-pct +0.34 (survivorship-proxy passed);
vol-scaled approx raw; survives 2x cost; IS+0.89/OOS+0.88. Crypto cross-sectional
momentum (>=45-coin breadth) passes every test that killed carry. One weakness:
negative 2019/2022 (momentum-crash) -> regime overlay. Develop-grade strongly met.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:18:59 +02:00
jgrusewski
0f1a04d16d fix(surfer): universe-robustness exposes carry as cherry-picked; momentum is robust
Broadened crypto universe 28 majors -> programmatic top-80 (64 usable), added
funding-coverage/history filter. Carry COLLAPSED (+0.82->-0.03) and stayed dead even
on broad-but-clean subset (fundcov>0.9, 45 coins): carry was cherry-picked to majors,
not robust. Cross-sectional MOMENTUM is the robust edge: XS_mom_30 IS+0.89/OOS+0.88,
CPCV-med +0.75, DSR 0.75, survives 2x cost, holds across 64- and 45-coin universes.
Momentum needs breadth (weak on 28 correlated majors). Same regime failure (2019/2022).
Methodology win: broadening killed a cherry-picked claim before a build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:16:56 +02:00
jgrusewski
af5e6d523b feat(surfer): broaden crypto sweep + regime/cost robustness — carry crash found
13 crypto signals, deflated by cumulative 30. Carry family all develop-grade, survive
2x cost; per-year reveals THE failure mode = 2022 deleveraging carry-crash (carry_7
-1.2 in 2022, +0.4..+2.1 other years). Momentum OOS-negative (decayed); chasing rising
funding strongly negative. Carry is the robust core; its one tail (carry crash) is
exactly what the ML regime overlay is built to manage -> the two threads unite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:10:40 +02:00
jgrusewski
cb402ab81f feat(surfer): crypto carry+momentum sweep — FIRST real edge (funding carry)
Batch 2 of the deflated signal sweep: cross-sectional funding carry + momentum on
28 major Binance USDT perps (2019-26, funding baked into return, 10bp cost, deflated
by cumulative 25 trials). XS_carry_7 = Sharpe +0.82, IS+0.77/OOS+0.93 (consistent),
CPCV-median +0.78 (robust across 45 paths), DSR 0.71. First signal all session that is
positive + IS/OOS-consistent + CPCV-median-positive. Develop-grade met, not yet deploy
(DSR<0.95, 5th-pct<0). Caveats: survivorship (current majors), confirm point-in-time.
Forward-paginated funding fetch (fundcov ~1.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:05:20 +02:00
jgrusewski
1283812c54 feat(surfer): deflated signal-sweep engine + Batch 1 (futures factor zoo = null)
Broad signal search judged against the FULL search (Deflated Sharpe deflated by
N_trials) — the multiple-testing discipline that makes breadth safe. Batch 1: 17
signals (XS/TS momentum, reversal, low-vol, seasonality) on 22-futures daily ->
ZERO survivors (best DSR 0.29); CPCV+deflation correctly kill the lucky cells.
SEAS_overnight invalid on 24h futures (equity-cash anomaly). Next batches: crypto, COT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:54:06 +02:00
jgrusewski
1cf38232e8 fix(surfer): full-16y MFT settle — regime-adaptive surfer significantly negative
Pulled 16y ES ohlcv-1m continuous (year-chunked, $20 credits, no 504) + light
to_ndarray loader (to_df OOM'd at 20GB). On 1.1M 5-min bars the 1.3y +0.50/t=0.67
top-5% hint collapsed: regime-adaptive top-5% = -0.52 ticks/trade t=-2.51
(significantly negative); ALL cells/signals/convictions significantly negative.
Decisive: no capturable intraday directional edge for crossing/non-colocated setup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:15:37 +02:00
jgrusewski
fa700c7b5f feat(surfer): MFT regime-adaptive quality test (intraday, GPU, 4GB-safe)
Tests the actual surfer thesis: intraday 5-min decisions/15-min holds, flat-by-close,
regime-adaptive (ride trend / fade range / stand aside), quality-over-quantity
(top-conviction only). Clean ES OHLCV-1m -> 5-min bars, per-trade net edge in ticks,
IS/OOS + t-stat. Result: regime-adaptive top-5% = +0.50 ticks/trade OOS, sign-consistent,
but t=0.67 (not significant); all else significantly negative. Adaptive+selective is the
only non-losing structure (thesis directionally right, within noise). 4GB-safe: cumsum
rolling-std (no unfold) + alloc cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:08:16 +02:00
jgrusewski
2bb2b50fd1 fix(surfer): correct roll handling (held-contract return stitching) + cost stress + dual gates
The +0.32 'edge' was a roll-zeroing artifact (discarded held-contract roll-day P&L).
Correct roll stitching + 2bp cost: floor Sharpe ~0.05 (sign) / 0.06 (cont) / 0.14
(cont+band), all NEGATIVE at 2x cost. Marginal, implementation-dominated edge.
Adds FLOOR_SIGNAL mode (sign|cont), no-trade band, 2x-cost stress, develop/deploy gates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:43:06 +02:00
jgrusewski
8df1c7eea2 feat(surfer): Phase 0 GPU floor + validation harness — first real (weak) OOS trend edge
PyTorch-GPU diversified-trend floor (TSMOM 1/3/12mo + inverse-vol + 10% vol-target)
+ CPCV/Deflated-Sharpe validation, over 22 CME futures x 19.7y (Databento GLBX
ohlcv-1d via budget-capped fetcher, ~$32 credits). Verdict: Sharpe +0.32, CPCV
median +0.30, IS +0.36/OOS +0.08 (sign-consistent), Deflated Sharpe ~0.92 at honest
n_trials. Real but weak edge; fails deploy-grade gates (correct), passes edge-exists.
Includes roll-neutralization fix (max-vol outright + zero roll-day returns) that
eliminated 988%/day continuous-contract artifacts (RB vol 550%->31%). Plus
install_torch_gpu.sh. Data gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:31:37 +02:00
jgrusewski
e07c409950 revise(surfer): Phase 0 to CUDA-only per foxhunt architecture (CPU read-only)
Per feedback_cpu_is_read_only + pearl_cold_path_no_exception_to_gpu_drives +
feedback_no_cpu_test_fallbacks: NO CPU compute paths even for validation/cold
paths. Rewrites Phase 0 as CUDA kernels + Rust orchestration in crates/ml-alpha
(reusing build.rs cubin pipeline, mapped-pinned, determinism): floor kernels
(TSMOM signal, EWMA vol, portfolio backtest) + validation kernels (CPCV Sharpe,
PBO, Deflated Sharpe). CPU only enumerates split masks + reads final gate scalars
via mapped-pinned. GPU-oracle tests (no CPU reference). Runs on the local GPU.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:23:15 +02:00
jgrusewski
565511c5f5 plan(surfer): Phase 0 — diversified-trend floor + validation harness
CPU-only/Python TDD plan: continuous-contract builder, TSMOM floor (1/3/12mo +
inverse-vol + vol-target), validation harness (CPCV/purge/embargo + PBO +
Deflated Sharpe + futures cost model), broad-universe data acquisition, and the
decisive verdict run (does the floor clear SV gates OOS after costs?). Tasks A-C
run immediately on local 4 instruments; D-E need Databento broad-universe history.
No ML, no GPU, no cluster.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:08:44 +02:00
jgrusewski
ce13a72ba1 docs(surfer): design spec — the surfer over an uncorrelated universe
Strategic pivot from seconds-horizon ES order-book RL (measured structurally
unprofitable) to diversified days-to-weeks multi-asset ML trend. ML as bounded
deviations from a deterministic diversified-trend floor; gated by CPCV/PBO/
Deflated-Sharpe. Phase 0 (build+validate the floor on Databento micros, CPU-only,
no ML, no cluster) is the immediate decisive test. Synthesizes 8-agent omnisearch
research (pearl_surfer_universe_build_synthesis).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:05:57 +02:00
106 changed files with 11159 additions and 31 deletions

1
.gitignore vendored
View File

@@ -204,3 +204,4 @@ crates/ml/ml/
# Foxhunt audit hook dedup state (cleared at SessionStart)
.claude/.foxhunt-audit-state
/config/ml/alpha_logits_cache.bin
data/surfer/

View File

@@ -0,0 +1,192 @@
# Surfer Phase 0 — Diversified-Trend Floor + Validation (CUDA/Rust) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (or subagent-driven-development).
> Steps use checkbox (`- [ ]`). TDD throughout — **GPU-oracle tests only, NO CPU reference oracle**
> (`feedback_no_cpu_test_fallbacks`). **CUDA-only, CPU read-only** (`feedback_cpu_is_read_only`,
> `pearl_cold_path_no_exception_to_gpu_drives`): every formula is a CUDA kernel; CPU reads only final gate scalars
> from mapped-pinned buffers and only enumerates split index-sets as control flow.
**Goal:** Build the deterministic diversified-trend FLOOR and the CPCV/PBO/Deflated-Sharpe validation, **entirely as
CUDA kernels + Rust orchestration in `crates/ml-alpha`**, then measure whether the floor has a real OOS edge after
costs. The cheap, falsifiable gate before any ML (Phases 1-3). Runs on the local GPU (data is tiny: ~30 instruments
× ~5000 days = ~150k floats).
**Architecture:** Reuse ml-alpha's CUDA pipeline — `build.rs` cubin precompile (`KERNELS` list), `pinned_mem.rs`
mapped-pinned buffers, cudarc `load_cubin`/`launch_builder`, the determinism foundation (`FOXHUNT_DETERMINISTIC`,
single-thread-per-series sequential sweeps, no `atomicAdd`, no nvrtc). GPU-drives / CPU-reads, uniformly.
**Tech Stack:** Rust 1.85 (`crates/ml-alpha`), CUDA 12.4 (pre-compiled cubins via build.rs, NO nvrtc), cudarc,
mapped-pinned host buffers. Data via `crates/data` dbn decode (the L0-fixed `dbn_parser`). NO Python in the built
system. NO GPU↔CPU roundtrips except the final gate-scalar readback.
**Spec:** `docs/superpowers/specs/2026-06-05-surfer-uncorrelated-universe-design.md`
---
## GPU-contract compliance (apply to EVERY task)
- **No `atomicAdd`** (`feedback_no_atomicadd`) → reductions are tree-reduce or single-thread-per-series sequential.
- **Mapped-pinned only** for any host visibility (`feedback_no_htod_htoh_only_mapped_pinned`); no raw HtoD/DtoH for
compute results. Data UPLOAD uses the existing mapped-pinned dataset path.
- **Determinism**: per-series sequential sweeps (EWMA vol, portfolio carry) are single-thread-per-instrument with a
sequential time loop — mirror `gae_backward_sweep.cu` (deterministic, no inter-thread race). Verify with
`determinism-check`-style same-seed bit-equality.
- **No CPU compute**: signals, vol, sizing, returns, Sharpe, PBO, Deflated-Sharpe are kernels. CPU may only:
(a) enumerate combinatorial split index-sets (control flow, like `epoch_idx`), uploading them as int arrays;
(b) read the FINAL gate scalars from mapped-pinned at the end.
- **No CPU test oracle**: tests assert kernel outputs against HAND-DERIVED analytic constants read from mapped-pinned
(e.g. "rising series → TSMOM signal == +1.0"), never a CPU re-implementation of the algorithm.
## File structure
| File | Change | Responsibility |
|---|---|---|
| `crates/ml-alpha/cuda/surfer_continuous_adjust.cu` | new | element-wise backward ratio-adjust of stitched closes |
| `crates/ml-alpha/cuda/surfer_tsmom_signal.cu` | new | per (instrument,day) 1/3/12-mo sign-ensemble signal |
| `crates/ml-alpha/cuda/surfer_ewma_vol.cu` | new | per-instrument EWMA vol (single-thread-per-instrument seq) |
| `crates/ml-alpha/cuda/surfer_floor_portfolio.cu` | new | inverse-vol size + vol-target + no-trade band → daily port. returns |
| `crates/ml-alpha/cuda/surfer_cpcv_sharpe.cu` | new | per (path,config) Sharpe over returns matrix (parallel) |
| `crates/ml-alpha/cuda/surfer_overfit_stats.cu` | new | PBO + Deflated-Sharpe scalars on GPU (erff cdf) |
| `crates/ml-alpha/build.rs` | edit `KERNELS` | register the 6 cubins |
| `crates/ml-alpha/src/surfer/mod.rs` | new | Rust orchestration (load→upload→launch→read gates) |
| `crates/ml-alpha/src/surfer/continuous.rs` | new | roll detection + stitch (data-assembly; decode via `data` crate) |
| `crates/ml-alpha/examples/surfer_phase0.rs` | new | the verdict-run binary |
| `crates/ml-alpha/tests/surfer_floor_invariants.rs` | new | GPU-oracle invariant tests |
---
## Task A: Continuous-contract series (data-assembly + ratio-adjust kernel)
**Files:** `crates/ml-alpha/src/surfer/continuous.rs`; `crates/ml-alpha/cuda/surfer_continuous_adjust.cu`; build.rs.
- [ ] **Step 1:** Add `"surfer_continuous_adjust"` to `KERNELS` in `crates/ml-alpha/build.rs`.
- [ ] **Step 2: kernel** `surfer_continuous_adjust.cu` — given stitched raw closes `[N_days]` and a roll-ratio array
`ratio[N_days]` (cumulative back-adjust factor per day, =1.0 after the last roll), output `adj[i]=raw[i]*ratio[i]`.
Element-wise, one thread per day. (Roll DETECTION — which contract is active per day by max volume, and the per-roll
ratio — is data-assembly control flow in `continuous.rs`: it decides WHAT was loaded, not a compute-over-results.
It reads per-expiry volume from the decoded dbn, picks argmax-volume contract per day, and builds the cumulative
`ratio` array, which is then UPLOADED mapped-pinned; the multiply is the kernel.)
- [ ] **Step 3: `continuous.rs`** — `pub fn build_continuous(root, decoder) -> ContinuousSeries { ts, raw_close_d,
adj_close_d, volume… }` decoding per-expiry OHLCV via the `data` crate, picking active contract per day by volume,
computing cumulative ratio, uploading raw+ratio mapped-pinned, launching the adjust kernel → `adj_close_d` on GPU.
Keep BOTH `raw_close_d` (fills/reward USD) and `adj_close_d` (signals/returns). Per spec §4: NEVER Panama; reward on raw.
- [ ] **Step 4: GPU-oracle test** (`surfer_floor_invariants.rs`): upload raw=[100,101,102,202,204], roll at day3,
ratio=[r,r,r,1,1] with r=202/102; launch; read `adj` mapped-pinned; assert `adj[3]==202`, `adj[2]==102*r`, and the
seam log-return `ln(adj[3]/adj[2])≈0`. (Analytic constants, no CPU re-impl.)
- [ ] **Step 5:** `SQLX_OFFLINE=true cargo test -p ml-alpha --test surfer_floor_invariants continuous` → PASS.
- [ ] **Step 6: Commit** — `feat(surfer): continuous-contract assembly + ratio-adjust kernel`.
---
## Task B: Floor kernels (TSMOM signal, EWMA vol, portfolio backtest)
**Files:** `surfer_tsmom_signal.cu`, `surfer_ewma_vol.cu`, `surfer_floor_portfolio.cu`; `src/surfer/mod.rs`; build.rs.
- [ ] **Step 1:** Register the 3 kernels in build.rs `KERNELS`.
- [ ] **Step 2: `surfer_tsmom_signal.cu`** — input `logc[N_inst × N_days]`; per (inst, day) output
`sig = clip( mean_{L∈{21,63,252}} sign(logc[end]-logc[end-L]), -1, 1)`, where for L=252 `end = day-21` (skip recent),
else `end=day`; 0 before warmup. Grid over (inst×day), branch-free. (Sign-inversion for yield-based rates handled
by a per-instrument `sign_flip[inst]` multiplier uploaded as config.)
- [ ] **Step 3: `surfer_ewma_vol.cu`** — per instrument, single thread, sequential over days:
`v[t] = a·r[t]^2 + (1-a)·v[t-1]`, `a=1-exp(ln0.5/halflife)`, `sigma[t]=sqrt(v[t]·252)`. One thread per instrument
(deterministic sequential carry — mirror `gae_backward_sweep.cu`'s single-thread-per-series pattern). No atomics.
- [ ] **Step 4: `surfer_floor_portfolio.cu`** — per day (sequential single-thread for the portfolio vol-target carry;
per-instrument inner loop): target weight `w_i = sig_i · (risk_budget / sigma_i)`; apply no-trade band (skip change
if |w_i - w_i_prev| < band); portfolio gross scaled to `vol_target/realized_port_vol` (realized from a trailing
window kernel-side); daily portfolio return `R[t] = Σ_i w_i[t-1]·r_i[t]`. Output `port_ret_d[N_days]`,
`turnover_d[N_days]` on GPU.
- [ ] **Step 5: `src/surfer/mod.rs`** — `pub struct Floor` loads the 3 cubins; `pub fn run_floor(&mut self,
series: &[ContinuousSeries], cfg: FloorCfg) -> FloorOutput` launches signal→vol→portfolio, returns GPU handles
(`port_ret_d`, `turnover_d`). No host math.
- [ ] **Step 6: GPU-oracle tests** (analytic): (a) monotonic-rising logc → `tsmom_signal` last value == +1.0;
monotonic-falling → 1.0; (b) constant-vol synthetic → `ewma_vol` converges to the known σ (hand value); (c) a
2-instrument hand-built case → assert `port_ret_d[t]` equals the hand-derived `Σ w·r` for one step. Read all via
mapped-pinned; no CPU re-impl of the formulas.
- [ ] **Step 7:** Run tests → PASS. **Smoke**: `run_floor` on the local 4 instruments (6E/ES/NQ/ZN), read
`port_ret_d` mapped-pinned, print annualized Sharpe (expect noisy/underpowered on 2y — proves the machinery).
- [ ] **Step 8: Commit** — `feat(surfer): floor kernels (TSMOM signal + EWMA vol + portfolio backtest)`.
---
## Task C: Validation kernels (CPCV Sharpe, PBO, Deflated Sharpe) + cost
**Files:** `surfer_cpcv_sharpe.cu`, `surfer_overfit_stats.cu`; `src/surfer/validation.rs`; build.rs.
- [ ] **Step 1:** Register both kernels in build.rs `KERNELS`.
- [ ] **Step 2: cost in the portfolio kernel** — extend `surfer_floor_portfolio.cu` to subtract per-rebalance cost:
`cost[t] = turnover[t]·(per_contract_usd/notional + half_spread_bps/1e4)` (+ roll cost on roll days), producing a
NET `port_ret_d`. Costs are config scalars uploaded mapped-pinned. (No host arithmetic.)
- [ ] **Step 3: `surfer_cpcv_sharpe.cu`** — inputs: net `port_ret_d[N_days]` (or `[N_cfg × N_days]` if sweeping
configs), and CPU-enumerated `test_mask[N_path × N_days]` (uint8, uploaded). Per (path[, cfg]) compute mean/std →
Sharpe over the path's OOS days via tree-reduce (no atomics). Output `oos_sharpe[N_path(× N_cfg)]` on GPU.
- [ ] **Step 4: `surfer_overfit_stats.cu`** — on GPU: (a) Deflated Sharpe `DSR = Φ((SRSR0)√(T1)/√(1γ3·SR+
(γ41)/4·SR²))` with `Φ` via `0.5·erfcf(-x/√2)`, `SR0` from `n_trials` (Euler-γ order-statistic formula), `n_trials`
uploaded; (b) PBO from the per-split IS-argmax / OOS-rank arrays (the CSCV split sets enumerated CPU-side, the
per-split argmax+rank reduced GPU-side). Output `{dsr, pbo, oos_sharpe_5pct, rank_consistency}` to a mapped-pinned
gate-scalar struct.
- [ ] **Step 5: `src/surfer/validation.rs`** — `pub fn validate(net_ret_d, cfg_grid) -> Gates`: CPU ENUMERATES the
CPCV combinations and CSCV splits (control flow), uploads index masks mapped-pinned, launches the two kernels,
reads the final `Gates` struct from mapped-pinned. The ONLY host readback.
- [ ] **Step 6: GPU-oracle tests**: (a) feed a returns series with known mean/std → assert `oos_sharpe` equals the
hand-derived value; (b) Deflated-Sharpe: same SR, n_trials=1 vs 50 → assert `dsr` decreases (monotonic, hand-checked
direction); (c) PBO: construct IS-best=OOS-worst returns → assert `pbo > 0.5`. All via mapped-pinned; no CPU oracle.
- [ ] **Step 7:** Run tests → PASS. **Determinism**: run `validate` twice same inputs → gate scalars bit-equal.
- [ ] **Step 8: Commit** — `feat(surfer): CUDA validation (CPCV Sharpe + PBO + Deflated Sharpe + cost)`.
---
## Task D: Acquire broad-universe deep-history data
**Files:** reuse `services/data_acquisition` / the existing Databento pipeline; cache decoded series.
- [ ] **Step 1:** Target ≥15 FULL-SIZE roots across classes (equity ES/NQ/YM/RTY; rates ZN/ZB/ZF/ZT; FX 6E/6J/6B/6A/6C;
metals GC/SI/HG; energy CL/NG/RB; ags ZC/ZS/ZW), max history via Databento `GLBX.MDP3 ohlcv-1d`. (Edge is identical to
micros; micros are a deployment-sizing concern only.)
- [ ] **Step 2:** Fetch per-expiry daily OHLCV (existing data pipeline), land on the PVC/local; the surfer's
`continuous.rs` builds continuous series at load.
- [ ] **Step 3:** Validate each root: ≥10y, decode clean via the L0-fixed `dbn_parser`, ratio seam-returns sane.
- [ ] **STOP-if:** Databento history/cost prohibitive → document and proceed with the largest free continuous set,
noting the data-quality caveat in the verdict.
---
## Task E: Phase-0 verdict run (the decisive gate)
**Files:** `crates/ml-alpha/examples/surfer_phase0.rs`.
- [ ] **Step 1:** Wire: load all continuous series → upload → `run_floor` (net of cost) → `validate(net_ret_d, grid)`
over the lookback/vol-target config grid → read the `Gates` struct (mapped-pinned, the only readback) → print a
verdict block (per `tier1_5_verdict.py` STYLE, but the binary is Rust reading GPU scalars):
- SV-G1: CPCV (≥50 paths) 5th-pct OOS Sharpe after costs > 0.
- SV-G3: Deflated Sharpe > 0.95, `n_trials` = ALL trials ever (record the count: 64 commits + session harnesses +
every floor lookback/vol-target variant in the grid).
- SV-G4: IS↔OOS config rank-consistency > 0.
- SV-G5: edge survives 2× cost + roll (re-run with doubled cost scalars).
- [ ] **Step 2:** Determinism: same-inputs run twice → identical verdict.
- [ ] **Step 3:** Record verdict in spec §9 + a memory pearl.
- [ ] **Step 4: Commit.**
**VERDICT BRANCH:**
- **PASS** (floor clears SV-G1/G3/G5 OOS net of costs) → diversified-trend premium is real & capturable on this
universe → proceed to Phase 1 (regime overlay). The floor becomes the live benchmark.
- **FAIL** → even the floor has no powered OOS edge → do NOT build ML. Reassess universe/history/thesis. Cost: a few
days of local-GPU dev, $0 cluster.
---
## Self-review
- **CUDA-only / CPU-read-only**: every formula is a kernel (Tasks A-C); CPU only enumerates split masks + reads the
final `Gates` struct (Task C Step 5, Task E Step 1). Complies with `feedback_cpu_is_read_only`,
`pearl_cold_path_no_exception_to_gpu_drives`. ✓
- **No CPU test oracle**: all tests assert kernel outputs vs hand-derived analytic constants read from mapped-pinned
(`feedback_no_cpu_test_fallbacks`). ✓
- **GPU-contract**: no atomicAdd (tree-reduce / single-thread-per-series), mapped-pinned only, cubins via build.rs
(no nvrtc), determinism via sequential-per-series sweeps + same-input bit-equality. ✓
- **Spec coverage**: floor §3.1 → Task B ✓ | validation gates §5 → Task C+E ✓ | universe+pipeline §4 → Task A+D ✓ |
Phase-0 STOP §6 → Task E verdict ✓ | edge-validation on full-size (not micros) → Task D ✓ | reuse ml-alpha infra §3.5 → file structure ✓.
- **Placeholders**: kernel bodies are given as signature + exact formula + determinism pattern + the existing kernel to
mirror (gae_backward_sweep / ema_update_per_step / the reduce kernels) — a competent engineer following ml-alpha's
`cuda/` conventions writes them directly. Not silent gaps.
## Execution note
Tasks A-C run on the local GPU against the 4 local instruments to prove the kernels + machinery; D-E need the broad
universe. No cluster, no Python in the built system. The floor + CUDA validation are the reusable core the ML surfer
(Phases 1-3) sits on.

View File

@@ -0,0 +1,263 @@
# Decommission Dead Rust Infra — Implementation Plan (Phase 1)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (INLINE, with checkpoints).
> **Do NOT run this subagent-driven** — every task performs irreversible production deletes that need
> human confirmation at each gate. Steps use checkbox (`- [ ]`) syntax.
**Goal:** Remove the now-unused Rust-specific infra (GPU pools, GPU CI runners, build-cache PVCs, Rust
artifact buckets, training/GPU manifests) — preserving ALL `.dbn` market data and every fxhnt/platform resource.
**Architecture:** Verify-then-delete, gated. Terraform destroys the GPU/precompute pools (plan reviewed
for exactly-3); helm uninstalls the GPU runners; kubectl deletes the unmounted build-cache PVCs; mc deletes
the no-`.dbn` Rust buckets; repo + cluster Argo templates cleaned up. Cockpit/dagster/fund verified healthy.
**Tech Stack:** terragrunt + OpenTofu (Scaleway provider), helm, kubectl, mc (MinIO), Scaleway Kapsule.
**Spec:** `docs/superpowers/specs/2026-06-21-decommission-rust-infra-design.md`
---
## Shared env (export at the start of each shell session that runs terragrunt/scw)
```bash
cd /home/jgrusewski/Work/foxhunt
export TG_TF_PATH=tofu TERRAGRUNT_TFPATH=tofu
export SCW_ACCESS_KEY=$(kubectl get secret scaleway-credentials -n foxhunt -o jsonpath='{.data.access-key}'|base64 -d)
export SCW_SECRET_KEY=$(kubectl get secret scaleway-credentials -n foxhunt -o jsonpath='{.data.secret-key}'|base64 -d)
export SCW_DEFAULT_PROJECT_ID=$(kubectl get secret scaleway-credentials -n foxhunt -o jsonpath='{.data.project-id}'|base64 -d)
export SCW_DEFAULT_REGION=fr-par SCW_DEFAULT_ZONE=fr-par-2
export TF_HTTP_USERNAME=root TF_HTTP_PASSWORD=$(kubectl get secret gitlab-pat -n foxhunt -o jsonpath='{.data.token}'|base64 -d)
```
---
## Task 0: Branch
- [ ] **Step 1**
```bash
cd /home/jgrusewski/Work/foxhunt && git checkout -b chore/decommission-rust-infra
```
---
## Task 1: Destroy the GPU + precompute pools (Terraform)
**Files:** Modify `infra/live/production/kapsule/terragrunt.hcl`
- [ ] **Step 1: Set the three enable flags to false**
In `infra/live/production/kapsule/terragrunt.hcl`, change:
```
enable_ci_compile_cpu_hm_pool = true
```
to `false`; and
```
enable_ci_training_l40s_pool = true
```
to `false`; and
```
enable_ci_training_h100_pool = true
```
to `false`. (Leave `enable_ci_compile_cpu_pool = true` — Python cockpit builds use it.)
- [ ] **Step 2: Plan and GATE on exactly-3-destroys**
Run (with shared env, in `infra/live/production/kapsule`):
```bash
cd /home/jgrusewski/Work/foxhunt/infra/live/production/kapsule
terragrunt plan -input=false -no-color 2>&1 | sed 's/.*tofu: //' | grep -iE "^Plan:|will be destroyed|# scaleway"
```
Expected: `Plan: 0 to add, 0 to change, 3 to destroy.` and the 3 destroyed are EXACTLY
`scaleway_k8s_pool.ci_compile_cpu_hm[0]`, `scaleway_k8s_pool.ci_training_l40s[0]`,
`scaleway_k8s_pool.ci_training_h100[0]`.
**STOP if the count ≠ 3 or any other resource is destroyed.**
- [ ] **Step 3: Apply**
```bash
terragrunt apply -input=false -no-color -auto-approve 2>&1 | sed 's/.*tofu: //' | grep -iE "Apply complete|Destroy complete|Error"
```
Expected: `Apply complete! Resources: 0 added, 0 changed, 3 destroyed.`
- [ ] **Step 4: Verify pools gone**
```bash
scw k8s pool list cluster-id=34a1e3c4-ac35-48c8-ab49-5f6ec4df32c1 -o json 2>/dev/null | python3 -c "import sys,json;print([p['name'] for p in json.load(sys.stdin)])"
```
Expected: `['ci-compile-cpu', 'platform']` (no l40s/h100/hm).
- [ ] **Step 5: Commit**
```bash
cd /home/jgrusewski/Work/foxhunt
git add infra/live/production/kapsule/terragrunt.hcl
git commit -m "chore(infra): destroy unused Rust GPU + precompute pools (L40S/H100/hm)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 2: Uninstall the GPU CI runners (helm)
- [ ] **Step 1: Uninstall the 3 GPU runners**
```bash
for r in gitlab-runner-h100 gitlab-runner-h100-sxm gitlab-runner-h100x2; do
helm uninstall "$r" -n foxhunt 2>&1 | tail -1
done
```
Expected: `release "<r>" uninstalled` for each.
- [ ] **Step 2: Decide the main `gitlab-runner`**
Check whether anything still uses it (fxhnt has no `.gitlab-ci.yml`; cockpit deploys via Argo):
```bash
kubectl get pods -n foxhunt | grep gitlab-runner # expect: no running runner pods
# Inspect what the main runner is registered for (manual judgement):
helm get values gitlab-runner -n foxhunt 2>/dev/null | grep -iE "tags|runUntagged|description" | head
```
- [ ] **Step 3: If confirmed dead, uninstall it; else keep (document the decision):**
```bash
helm uninstall gitlab-runner -n foxhunt 2>&1 | tail -1 # ONLY if Step 2 confirms no consumer
```
- [ ] **Step 4: Verify**
```bash
helm list -n foxhunt | grep -i runner # expect: none (or only a deliberately-kept one)
```
---
## Task 3: Delete the build-cache PVCs (re-verify unmounted first)
- [ ] **Step 1: Re-confirm each PVC is unmounted (safety)**
```bash
for p in cargo-target-cpu cargo-target-cuda cargo-target-cuda-test sccache-cpu sccache-cuda; do
echo -n "$p: "; kubectl describe pvc $p -n foxhunt 2>/dev/null | grep -A1 "Used By" | tr '\n' ' '; echo
done
```
Expected: every line shows `Used By: <none>`. **STOP on any PVC that lists a pod.**
- [ ] **Step 2: Delete them**
```bash
kubectl delete pvc cargo-target-cpu cargo-target-cuda cargo-target-cuda-test sccache-cpu sccache-cuda -n foxhunt 2>&1 | tail
```
Expected: `persistentvolumeclaim "<name>" deleted` ×5.
- [ ] **Step 3: Verify reclaim**
```bash
kubectl get pvc -n foxhunt | grep -E "cargo-target|sccache" || echo "all build-cache PVCs gone (~175Gi reclaimed)"
```
---
## Task 4: Delete the Rust artifact buckets (re-verify 0 `.dbn` first)
- [ ] **Step 1: Start a MinIO port-forward + mc alias**
```bash
pkill -f "port-forward svc/minio" 2>/dev/null; sleep 1
kubectl port-forward svc/minio -n foxhunt 19000:9000 >/tmp/minio-pf.log 2>&1 &
sleep 4
ak=$(kubectl get secret minio-credentials -n foxhunt -o jsonpath='{.data.access-key}'|base64 -d)
sk=$(kubectl get secret minio-credentials -n foxhunt -o jsonpath='{.data.secret-key}'|base64 -d)
mc alias set fxl http://localhost:19000 "$ak" "$sk"
```
- [ ] **Step 2: Re-verify the 3 target buckets contain ZERO `.dbn`/market data, and the KEPT one does**
```bash
for b in foxhunt-binaries foxhunt-training-results foxhunt-models; do
echo -n "$b dbn-count="; mc ls --recursive fxl/$b 2>/dev/null | grep -icE "\.dbn|\.zst|databento|mbp"
done
echo -n "KEEP foxhunt-training-data dbn-count="; mc ls --recursive fxl/foxhunt-training-data 2>/dev/null | grep -icE "\.dbn|\.zst|databento|mbp"
```
Expected: the 3 targets show `0`; `foxhunt-training-data` shows `>0`. **STOP if any target shows >0.**
- [ ] **Step 3: Delete the 3 Rust buckets**
```bash
for b in foxhunt-binaries foxhunt-training-results foxhunt-models; do
mc rb --force fxl/$b 2>&1 | tail -1
done
```
Expected: `Removed '<bucket>' successfully.` ×3.
- [ ] **Step 4: Verify market data intact**
```bash
mc du fxl/foxhunt-training-data # expect: still ~38GiB, 54+ objects
kubectl get pvc training-data-pvc test-data-pvc feature-cache-pvc -n foxhunt # expect: all 3 still Bound
pkill -f "port-forward svc/minio" 2>/dev/null
```
---
## Task 5: Remove dead manifests + Argo templates (repo + cluster)
**Files:** Remove `infra/k8s/training/`, `infra/k8s/gpu-overlays/`, Rust Argo templates, the databento job, GPU-runner helm values.
- [ ] **Step 1: Identify the Rust Argo WorkflowTemplates in the cluster (keep fxhnt-cockpit!)**
```bash
kubectl get wftmpl -n foxhunt -o name 2>/dev/null
```
Note the Rust ones (e.g. train-multi-seed, lob-backtest-sweep, ci-pipeline, alpha-rl-*). **Do NOT touch `fxhnt-cockpit`.**
- [ ] **Step 2: Delete the dead WorkflowTemplates from the cluster** (substitute the exact names from Step 1):
```bash
# example — replace with the actual Rust template names from Step 1:
kubectl delete wftmpl -n foxhunt <rust-template-1> <rust-template-2> ... 2>&1 | tail
```
- [ ] **Step 3: Remove dead manifests + the now-unused Terraform pool blocks from the repo**
```bash
cd /home/jgrusewski/Work/foxhunt
git rm -r infra/k8s/training infra/k8s/gpu-overlays 2>/dev/null
git rm infra/k8s/jobs/download-trades-job.yaml infra/k8s/argo/train-multi-seed-template.yaml infra/k8s/argo/lob-backtest-sweep-template.yaml infra/k8s/argo/ci-pipeline-template.yaml 2>/dev/null
# (also git rm any GPU-runner helm value files + alpha-rl argo templates found under infra/)
ls infra/k8s/argo infra/k8s/jobs # eyeball what remains; keep cockpit/fxhnt/fund + cert-manager etc.
```
- [ ] **Step 4: Remove the dead pool resource blocks + variables from the kapsule module** (optional tidy):
in `infra/modules/kapsule/main.tf` remove the `scaleway_k8s_pool.ci_training_l40s`,
`...ci_training_h100`, `...ci_compile_cpu_hm` resource blocks (now count=0); in `variables.tf` remove
their `enable_*`/`*_type`/`*_max_size` vars; in `terragrunt.hcl` remove the 3 dead `enable_*`/`*_type`
lines. Then re-plan to confirm still clean:
```bash
cd infra/live/production/kapsule && terragrunt plan -input=false -no-color 2>&1 | sed 's/.*tofu: //' | grep -iE "^Plan:|No changes"
```
Expected: `No changes` (removing count=0 blocks is a no-op against the cluster).
- [ ] **Step 5: Commit**
```bash
cd /home/jgrusewski/Work/foxhunt
git add -A infra
git commit -m "chore(infra): remove dead Rust training/GPU manifests, Argo templates, pool config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 6: Final verification (cluster + fund still healthy)
- [ ] **Step 1: Platform/fund health**
```bash
curl -sS -m 12 -o /dev/null -w "dashboard HTTP %{http_code}\n" https://dashboard.fxhnt.ai/
kubectl get pods -n foxhunt | grep -E "dagster|fxhnt-dashboard|gitlab-webservice" | grep -vE "Completed"
kubectl get pvc training-data-pvc test-data-pvc feature-cache-pvc fxhnt-surfer-data fxhnt-backtest-data -n foxhunt
```
Expected: dashboard 200; dagster/dashboard/gitlab Running; all kept PVCs Bound.
- [ ] **Step 2: Terraform clean**
```bash
cd /home/jgrusewski/Work/foxhunt/infra/live/production/kapsule && terragrunt plan -input=false -no-color 2>&1 | sed 's/.*tofu: //' | grep -iE "^Plan:|No changes"
```
Expected: `No changes`.
- [ ] **Step 3: Merge to main**
```bash
cd /home/jgrusewski/Work/foxhunt
git checkout main && git merge --ff-only chore/decommission-rust-infra && git branch -d chore/decommission-rust-infra
git push origin main
```
---
## Notes for the executor
- **Data safety is paramount:** never delete `training-data-pvc`, `test-data-pvc`, `feature-cache-pvc`, the
`foxhunt-training-data` bucket, or any fxhnt/platform resource. Re-verify (`Used By`, `.dbn` count) at each
delete gate; STOP on any surprise.
- **Terraform gate:** Task 1 Step 2 must show exactly the 3 GPU/precompute pools destroyed — stop otherwise.
- `pkill -f "port-forward svc/minio"` after Task 4 to clean up the forward.
- If the main `gitlab-runner` is ambiguous, KEEP it (a kept idle runner is harmless; a wrongly-removed one breaks CI).

View File

@@ -0,0 +1,144 @@
# The Surfer over an Uncorrelated Universe — Design Spec
> **For agentic workers:** This spec defines WHY and WHAT for foxhunt's strategic pivot from
> single-instrument seconds-horizon order-book RL to a diversified, days-to-weeks, multi-asset
> ML trend system. It does NOT implement; an implementation plan follows (Phase 0 first).
**Status:** Draft 1 (2026-06-05)
**Supersedes (direction):** the seconds-horizon ES order-book RL thesis (measured structurally unprofitable).
**Branch target:** new branch `surfer-universe` off the current branch (keeps the session's validation harnesses).
**Capital stance:** $35k is a PILOT to prove the pipeline + edge (paper / tiny live); real book targets ~$100k.
**Linked pearls (load-bearing):**
- `pearl_surfer_thesis_is_uncorrelated_trend_recognition` — the thesis
- `pearl_surfer_universe_build_synthesis` — the 8-agent research synthesis behind every choice here
- `pearl_ofi_edge_uncapturable_by_crossing` / `pearl_passive_mm_knife_edge_ofi_conditioning_promising` — why seconds-horizon ES is dead
- `pearl_lowfreq_no_robust_edge_on_2y_es` — why breadth/universe is the bottleneck; validation discipline
- `pearl_omnisearch_synthesis_signal_first_and_mtm_reward` — dense-MtM → portfolio Differential Sharpe reward
---
## §1. Why this, why now
Measured this session (zero cluster runs, ~5 cheap harnesses): on ES, seconds-horizon crossing edge is ~100× smaller
than the spread; passive MM is adversely-selected negative (winner's curse); intraday has no IC; daily/cross-asset
edges sign-flip IS↔OOS (overfit). **The bottleneck was the configuration — single instrument × seconds-horizon ×
order-book microstructure — not the model.** That is the worst possible config for a non-colocated ML participant:
most-efficient market, most speed-dependent horizon, zero diversification.
The fix is to invert all three: **many uncorrelated instruments × days-to-weeks horizon × trend/cross-sectional
recognition.** This is also foxhunt's actual "surfer" thesis ("no wave is the same") finally pointed at an ocean
with enough waves to ride. Breadth = opportunity sourcing: across many uncorrelated markets, some subset always
has a rideable trend, and uncorrelated rides don't sink together.
## §2. The thesis
A fixed trend rule applies identical logic to every wave. The ML "surfer" reads each unique wave — is it a real
trend or chop? building or exhausting? what's the sea-state (regime)? — and adjusts. That recognition is the
legitimate ML edge ON TOP of the trend risk premium. **Breadth gives the floor; ML earns its keep above it.**
## §3. Architecture
**Core design principle (the eval-collapse cure):** the ML surfer outputs **bounded DEVIATIONS from a deterministic
diversified-trend floor**: `w_i = floor_i + clamp(ML_delta_i, ±δ)`. The model can only add value above a proven
baseline and degrades gracefully to the floor when it has no edge. It structurally cannot run off a cliff.
### §3.1 The floor (deterministic)
Diversified time-series momentum: `signal_i = mean over h∈{1,3,12}mo of sign(return_{i,h})` (12mo skips last ~21d);
inverse-vol size with 63-day EWMA vol; portfolio vol-target 10% annual (rescale weekly on realized portfolio vol);
weekly rebalance + ≥10%-change no-trade band. Realistic net Sharpe 0.40.5 (small universe), DD 1525%.
### §3.2 The ML surfer (bounded deltas), in confidence order
1. **Regime / change-point overlay (build FIRST)** — recognizes trend-vs-chop, throttles gross, cuts drawdowns.
ML's most defensible edge (DD reduction, NOT return prediction). Lowest overfit surface.
2. **Cross-sectional tilt (panel)** — ONE shared encoder over all instruments (N×T samples) + per-instrument
embedding → continuous dollar-neutral tilt on the floor; gated on rank-IC / ICIR. NOT discrete ranks (a <20-name
universe is statistically starved; breadth is recovered through time + pooling).
3. **Direct return prediction → DEFERRED** (the 64-commit trap; cost-cliff at 23 bps = turnover noise).
### §3.3 Reward / objective
Portfolio **Differential Sharpe Ratio** (Moody-Saffell) + drawdown penalty + turnover/cost penalty — the
portfolio-level, risk-adjusted generalization of the dense per-step mark-to-market reward (Φ=unrealized-PnL).
### §3.4 Risk / sizing
Vol-target 10%; inverse-vol within asset class + ERC across classes; fractional Kelly (half/quarter, reuse the
existing Kelly controller clamped [0,0.5]); drawdown throttle θ; reuse the CMDP overlay as the tail kill-switch.
Discreteness is binding at micro size: target <0.5 contract → hold 0, carry the residual; $35k sustains ~35
simultaneous 1-lot positions.
### §3.5 Reuse of existing foxhunt assets + architecture discipline
CfC/Mamba2 encoder (shared across instruments + instrument embedding + cross-sectional attention head); RL stack;
Kelly + CMDP controllers; the determinism foundation; ml-alpha's CUDA build pipeline (build.rs cubins, mapped-pinned
buffers, cudarc launch). **All compute is CUDA-only, CPU read-only** (`feedback_cpu_is_read_only`,
`pearl_cold_path_no_exception_to_gpu_drives`, `feedback_no_cpu_test_fallbacks`): every formula — signals, vol,
sizing, backtest returns, and the validation statistics (CPCV per-path Sharpe, PBO, Deflated Sharpe) — is a CUDA
kernel; CPU only reads final gate scalars from mapped-pinned buffers and only enumerates split index-sets as control
flow. No CPU compute, no cold-path exception, no CPU test oracle. The session's Python `scripts/measure_*.py` were
one-off edge AUDITS, not part of the built system; the system is Rust+CUDA. The new surfer lives in `crates/ml-alpha`
(reuses its build.rs/cuda/mapped-pinned/determinism infra and, in Phases 1-3, its encoder + RL).
## §4. Universe + data pipeline
**Universe (~8 micros, maximal asset-class diversification):** MES, M2K (equity); 10Y micro-yield (rates — note
YIELD-based = sign inversion vs price); M6E, M6A (FX); MGC (metals); MCL (energy); MBT (crypto).
**Data:** Databento `GLBX.MDP3`, `ohlcv-1d` (+ `ohlcv-1h`). **Continuous contracts built in-house:** volume-roll
(validate vs OI) + **backward RATIO adjustment for the encoder's returns**; keep an **UNADJUSTED** series for
actual fills + USD reward. NEVER Panama-adjust for a trend learner; NEVER compute reward on adjusted prices.
## §5. Validation gates (the guardrail — non-negotiable)
| Gate | Metric | Pass |
|---|---|---|
| SV-G0 | Leakage audit: purge+embargo covers full hold + feature lookback | verified |
| SV-G1 | CPCV (≥50 paths): 5th-percentile OOS pnl after costs | > 0 |
| SV-G2 | PBO (CSCV) | < 0.20 |
| SV-G3 | Deflated Sharpe, deflated by N = ALL trials ever (64-commit + session + this build) | > 0.95 |
| SV-G4 | IS↔OOS config rank-consistency (Spearman) — the sign-flip detector | > 0 |
| SV-G5 | Edge survives 2× cost + roll cost | net OOS pnl > 0 |
| SV-G6 | Capacity/turnover: cost × turnover < fixed fraction of gross | pass |
Every config / seed / hyperparameter is a logged trial feeding N. The 64 failed commits prove N is large.
## §6. Phases (signal-first, cheapest-first, zero cluster until validated)
- **Phase 0 — Floor + validation harness (CUDA kernels + Rust orchestration, CPU read-only, no ML, no cluster — per foxhunt GPU-drives discipline; `feedback_cpu_is_read_only`, `pearl_cold_path_no_exception_to_gpu_drives`). Runs on the local GPU. FULLY SPECIFIED in the plan.**
Build the deterministic floor on real Databento micro data; build the CPCV/PBO/DSR harness; measure whether the
floor itself clears the gates OOS after costs. **STOP-if:** floor fails SV-G1/G3/G5 → the universe has no
capturable trend premium at this scale; reassess (more instruments / more history) before any ML.
- **Phase 1 — Regime overlay.** Must beat the floor OOS (DD reduction first). Gates SV-G1..G6 vs floor.
- **Phase 2 — Cross-sectional panel tilt** (shared encoder). Must beat floor+regime OOS via rank-IC/ICIR.
- **Phase 3 — Full RL surfer** (floor + bounded delta, DSR reward). Cluster only here, gated by all above.
## §7. Scope boundary
**In (this spec):** the floor, the bounded-delta architecture, the 3 ML layers (regime → cross-sectional → RL),
the universe + continuous-contract pipeline, the validation gates, the phased build. **Phase 0 is the immediate
deliverable.**
**Out:** seconds-horizon / order-book microstructure (measured dead); direct return-prediction sequence models
(deferred until floor+regime+cross-sectional prove out); live-capital deployment beyond a pilot (until ~$100k and
gates pass); broker/execution automation (separate spec once a validated strategy exists).
## §8. Risks
1. **Capital ($35k below viable minimum)** — under-diversified → trend's drawdowns without its smoothing; micro-cost
drag eats Sharpe. MITIGATION: treat as a pilot to prove pipeline/edge; scale to ~$100k for the real book.
2. **ML adds nothing over the floor** — entirely possible; the bounded-delta design makes that a graceful
no-op (you keep the floor), and the gates catch it before any spend.
3. **Floor itself has no edge at this scale/universe** — Phase 0 STOP catches this cheaply.
4. **Re-overfitting (commit #65)** — the SV gates + deflation-by-all-trials are the explicit defense; honor them.
5. **Roll / continuous-contract artifacts** — ratio-adjust + volume-roll + reward-on-unadjusted; validated in Phase 0.
## §9. Decision log
```
Decision: Pivot foxhunt to "the surfer over an uncorrelated universe" — diversified days-to-weeks ML trend across
~8 uncorrelated micro futures, ML as bounded deviations from a deterministic trend floor, gated by CPCV/PBO/Deflated-
Sharpe. Phase 0 (build+validate the floor, CPU-only) is the immediate, decisive, near-free test.
Rejected: continuing seconds-horizon ES order-book RL (measured structurally unprofitable for a non-colocated ML
participant — crossing edge 100× < spread; passive MM adversely-selected; daily/cross-asset edges sign-flip).
Capital: $35k = pilot to prove the approach; real book ~$100k (Carver retail floor; under-diversification = main risk).
ML scope: regime overlay first (DD reduction = ML's defensible edge); cross-sectional tilt second; direct return
prediction deferred (the 64-commit trap).
Date: 2026-06-05
Falsification (Phase 0): floor fails SV-G1/G3/G5 OOS → no capturable trend premium at this scale; reassess before ML.
```

View File

@@ -0,0 +1,226 @@
# Adaptive Multi-Strat Book — Deployable Spec
**Date:** 2026-06-07
**Status:** Validated design, pre-deployment (paper-forward running)
**Branch:** surfer-universe
**Engine:** `scripts/surfer/multistrat_book_v3.py` (validated), `sixtyforty_paper.py` (baseline, live)
---
## 1. Executive summary
A **diversified, edge-decay-adaptive, adaptively-risk-managed, unlevered** multi-asset book — the
honest endpoint of an exhaustive search. It is *not* alpha; it is **premium harvesting with superior
risk management** (the foxhunt engine's genuine, validated strength). Realistic **~0.50.7 Sharpe,
~7% max drawdown**, ~$23k/yr on $35k, scales linearly with capital.
**Why this and not the alternatives** (all tested to OOS exhaustion, all in memory):
- Single predictive edges (equities, futures, ML, AI4Finance, PEAD): no OOS alpha for non-colocated retail.
- Crypto funding (single-venue): hedgeability-blocked. Cross-venue: marginal/breakeven OOS (the +14.9 Sharpe was a max-min artifact; honest fixed-pair = ~breakeven).
- Leverage: **doesn't pay at retail financing** (67% margin vs prime-brokerage SOFR+12%) — adaptive 1x Sharpe +0.48 vs 2x +0.14. The hedge-fund moat is *cheap financing*, not the strategy.
**What we keep** (validated, measurable): the **edge-decay-adaptive allocation** (+0.11 Sharpe, lower
DD vs static risk-parity) and the **adaptive risk layer** (beat the static one decisively; halves
drawdown). The engine's value is *risk management + adaptive allocation*, and that is real.
---
## 2. Strategy overview
Hold a small set of **uncorrelated return streams**, each vol-normalized to equal risk, weighted by
a **per-stream edge-health trust** (down-weight decaying streams, resurrect recovered ones), combined
and run through an **adaptive risk-management layer**, **unlevered (~1x)**, rebalanced on a schedule.
Return source = the structural premia (equity, term, gold/inflation, commodity, trend/crisis-alpha,
a small crypto sleeve). The *alpha* is the disciplined, adaptive risk allocation — not prediction.
---
## 3. Instruments (deployable at $35k via fractional-share ETFs + small crypto)
| Stream | ETF | Role |
|---|---|---|
| Equity | **SPY** (or VTI) | equity risk premium |
| Bonds | **IEF** (710y Treasuries) | term premium, equity diversifier |
| Gold | **GLD** (or IAU) | inflation/crisis diversifier |
| Commodity | **PDBC** (or DBC) | inflation/commodity premium |
| Trend / managed futures | **DBMF** (or KMLM) | crisis-alpha, uncorrelated (use the *real* CTA ETF — our DIY trend was ~0 Sharpe) |
| Crypto (small) | **BTC spot** (or IBIT) | high-return uncorrelated sleeve; trust-layer auto-caps it (currently down-weighted in deleverage) |
All ETFs are marginable, fractional-shareable, ~zero commission. **Unlevered** (cash account is fine;
no margin needed). Crypto held separately (spot/IBIT), sized small.
---
## 4. Allocation engine — edge-decay-adaptive trust (foxhunt idea, validated)
For each stream `i`, maintain a **trust** `θᵢ ∈ [0.1, 1]`:
- Compute trailing-126d risk-adjusted return (Sharpe) of the stream.
- Map: Sharpe ≥ +0.5 → θ=1.0; ≤ 0.5 → θ=0.1 (floored — **resurrection-capable**); linear between.
- EMA-smooth (α≈0.06) so it adapts gradually, not jumpily.
Weight each (vol-normalized) stream by `θᵢ`, normalize to sum 1. This **down-weights decaying premia
and re-weights recovered ones** automatically — currently: equity 0.73, commod 1.00, trend 0.79
(healthy) vs bond 0.11, crypto 0.10 (correctly de-emphasized). Validated: +0.11 Sharpe, lower DD vs
static equal-risk. Source: `pearl_edge_decay_detection_is_a_missing_abstraction_layer`,
`pearl_dead_signal_resurrection_discipline`.
---
## 5. Risk-management layer — adaptive controllers (the foxhunt port; the load-bearing piece)
Static thresholds **crush returns** (one-way latch, `pearl_cmdp_consec_loss_counter_is_one_way_latch`).
The adaptive layer (validated: beat static +0.03→+0.14 Sharpe, maxDD 18.7%→14.5%). Daily leverage
`L = clip( min(L_vol, L_kelly) × dd_mult × corr_mult , LEV_FLOOR, MAXLEV )`, applied to the combination:
1. **EMA online vol** (α≈0.03) → `L_vol = TARGET_VOL / realized_vol` (responsive, no window edges).
2. **Kelly with floor + bootstrap**`L_kelly = clip(EMA_mean·252 / EMA_var·252, KELLY_FLOOR=0.5, MAXLEV)` — adapts to edge strength, **never dies** (`pearl_bootstrap_must_respect_clamp_range`).
3. **Drawdown de-lever, CONTINUOUS + self-recovering**`dd_mult = clip(1 3·max(0, dd 0.05), 0.40, 1.0)` — scales down as drawdown deepens, **recovers immediately as it heals** (NOT a latch — this is the fix).
4. **Correlation de-risk, z-scored** → when avg cross-stream correlation is unusually high vs its own trailing distribution (diversification breaking in a crisis), reduce: `corr_mult = clip(1 0.2·max(0, z), 0.5, 1.0)`.
5. **Leverage floor** `LEV_FLOOR=0.3`, **MAXLEV=1.0** (unlevered — see §8).
Defaults: `TARGET_VOL=10%`. The whole layer is the engine's true job — *survive and adapt*, not maximize.
---
## 6. Rebalance & execution
- **Weekly rebalance** (fixed-phase): recompute vol-normalization, trust weights, and `L`; trade to target.
- **Hysteresis / no-churn:** only trade a stream if its target weight moved > ~2% (cut turnover/cost).
- **Cost budget:** ETFs ~0 commission; the only friction is spread/slippage (minimal on SPY/IEF/GLD/DBMF). Crypto sleeve: small, spot.
- **Cash account, unlevered** — no financing cost, no liquidation risk.
---
## 7. Expected performance (honest)
**Validated** on the real ETFs (SPY/IEF/GLD/PDBC/DBMF + BTC) over 2019-05..2026-06, exact live
pipeline (`multistrat_etf_backtest.py`), multiple regimes (COVID, 2022 bear, bulls):
| Metric | Backtest 201926 | Realistic forward |
|---|---|---|
| Sharpe (unlevered) | **+1.20** | ~0.81.0 (haircut: favorable period + low-vol-flatter) |
| Annual return | +6.1% | ~57% |
| Realized vol | 5.1% | ~58% |
| Max drawdown | **5.4%** | ~8 to 12% |
| Per-year | **positive every year** incl 2022 (+0.1) | — |
| vs 60/40 (SPY/IEF) | +0.87 Sharpe, 21% DD | book wins on Sharpe AND drawdown |
| vs equity buy-hold | +0.85 Sharpe, 34% DD | — |
| On $35k | ~$2.1k/yr, very smooth (5% DD) | — |
| On $500k | ~$30k/yr (Sharpe scale-invariant — capital is the lever) | — |
The robust signals are the **5.4% max drawdown and every-year-positive** (not just the low-vol
Sharpe). The book genuinely beats 60/40 on risk-adjusted terms with ~1/4 the drawdown.
Honest caveats: it's **long beta** (falls in everything-down 2022-style, though trend + adaptive
de-lever cushion it); returns are *modest* in dollars at small capital (the lever is capital, not
Sharpe); crypto sleeve validated on one window — keep it small and let the trust layer cap it.
---
## 8. Why unlevered (the key finding)
Leverage scales return but **at retail financing (67%) it lowers risk-adjusted return**: adaptive 1x
Sharpe +0.48 vs 2x +0.14 — the financing drag eats the leverage benefit, while drawdown grows. Hedge
funds lever ~0.7-Sharpe books profitably **only because prime brokerage finances at ~SOFR+12%.** That
financing access is the institutional moat, and it's structural — not replicable at retail. **So: run
it 1x.** Re-evaluate leverage only if you ever access institutional-rate financing.
---
## 9. Validation status
**Validated:** edge-decay-adaptive allocation (>static), adaptive risk layer (>static, halves DD),
combination Sharpe ~0.50.72 across windows, the unlevered-is-best financing finding. Scripts:
`multistrat.py`, `multistrat_book.py`, `multistrat_book_v2.py`, `multistrat_book_v3.py`.
**Live baseline:** `sixtyforty_paper.py` paper-forward running (the 60/40 core).
**Not yet:** live execution of the full 6-stream adaptive book (paper-forward harness for it = next),
real ETF fills/spreads (minor), the crypto sleeve operationally.
---
## 10. Phased rollout
1. **Paper-forward (48 wk):** extend the live harness to the full 6-stream adaptive book (ETF closes
via Yahoo + BTC), logging target weights + realized — confirm the design forward. (60/40 core
already runs via `sixtyforty_paper.py`.)
2. **Micro-live (small):** deploy the ETF book unlevered at small size; validate rebalance discipline,
spreads, the trust/risk layer operating live.
3. **Scale:** full capital, same unlevered book. Returns scale with capital, Sharpe unchanged.
Gate each phase: forward Sharpe ~consistent, drawdown controlled, trust/risk layer behaving.
---
## 11. Go / no-go + risk limits
- **Go:** you accept a modest (~0.50.7 Sharpe, ~$23k/yr on $35k) but *robust, well-risk-managed,
unlevered* diversified book whose return scales with capital — and that the engine's role is risk
management, not alpha.
- **No-go / reconsider:** if you need higher absolute return at $35k (then the lever is *more capital*,
not more strategy), or if you want market-neutral (this is long-beta — accept the equity-correlated
drawdowns).
- **Risk limits:** unlevered (MAXLEV 1.0); crypto sleeve ≤ ~15% target (trust-capped); weekly rebalance;
the adaptive layer de-risks automatically in vol spikes / correlation crises / drawdowns.
**The honest bottom line:** after exhausting every alpha avenue (incl. the engine's own advanced
ideas), this is what's real and deployable — a simple, diversified, adaptively-risk-managed harvest.
The engine earns its keep as the risk/allocation brain, not as an oracle. The path to *meaningful*
money is capital, not a better signal.
---
## Appendix A — Micro-live runbook (Phase 2, concrete)
**Validation so far (historical, exhaustive):** in-sample Sharpe +1.20; holdout-OOS +1.41; 20-year
(200626, conservative no-DBMF/crypto version) +0.96 surviving 2008/2020/2022 with 10% maxDD;
bootstrap CI p5 +0.62 / median +0.99, P(Sharpe>0.5)=99%. The *only* thing history can't give is
forward/live confirmation — that's what micro-live provides.
**Objective:** validate *execution* — fills, spreads, the weekly/monthly rebalance, the live adaptive
weighting — and start the real-money clock. **NOT to make money** (on $1k the P&L is pennies). It
de-risks the scale step; that's its whole job.
**Capital:** $5001,000, fully at-risk, **unlevered**. Worst case ≈ the book's maxDD (~7%) ≈ $70.
Trivial by design — that's the point of micro.
**Instruments (all in ONE brokerage, fractional shares):**
| Stream | Ticker | Note |
|---|---|---|
| equity | SPY | |
| bonds | IEF | |
| gold | GLD | |
| commodity | PDBC | |
| trend / CTA | DBMF | the real managed-futures ETF (our DIY trend was ~0) |
| crypto | **IBIT** | iShares Bitcoin ETF — use instead of BTC spot: in-brokerage, fractional, **NO crypto-exchange counterparty tail** |
Broker: any fractional-share, ~zero-commission (IBKR / Fidelity / Schwab / Robinhood). IBKR or
Fidelity recommended (clean fractional fills).
**Step 1 — today's target:**
```
python3 scripts/surfer/multistrat_paper.py weights 1000
```
→ exact % and $ per instrument (the adaptive trust + risk-layer output). Map the crypto line → IBIT.
**Step 2 — place orders manually** (micro scale → eyeball the fills). Marketable-limit or market on
these liquid ETFs. Record actual fill prices.
**Step 3 — rebalance MONTHLY (not weekly) at micro scale.** At $1k, weekly deltas are ~$3 —
rounding-dominated and operationally silly. Rebalance monthly, and only trade an instrument if its
target weight drifted >~5% (hysteresis). The adaptive layer's value shows over months, not days.
**Step 4 — track actual vs intended.** Each rebalance, log the harness's intended weights vs your
actual fills → measure (a) tracking error, (b) realized cost (spread/slippage), (c) live-vs-paper match.
**Gate to scale (~48 weeks):**
- Live cumulative ≈ paper-forward cumulative (small tracking error).
- Realized cost ≤ ~1020 bp/rebalance (should be tiny on these ETFs).
- Rebalance executes cleanly; adaptive weights behave sensibly; no operational surprises.
- → all green: **scale to full capital, same unlevered book** (Sharpe is scale-invariant; capital is the lever).
- → tracking error large / cost high: diagnose before scaling.
**Risk limits:** unlevered (no margin); $5001k at-risk; adaptive layer auto-de-risks in vol/DD;
IBIT removes the counterparty tail. **What it does NOT do:** make meaningful money — it proves the
machine runs cleanly with real fills before you commit real capital. The honest bridge from
validated backtest to scaled deployment.

View File

@@ -0,0 +1,183 @@
# Crypto Funding Harvest — Deployable Strategy Spec
**Date:** 2026-06-07
**Status:** Validated (4 gates passed), pre-deployment
**Branch:** surfer-universe
**Author:** research campaign synthesis
---
## 1. Executive summary
A **delta-neutral crypto perpetual-funding harvest**: hold `long spot + short perp` on coins
whose funding rate is reliably positive, collecting the funding payment as carry with **no price
exposure**. No prediction, no ML — a pure cross-sectional carry harvest gated by a regime filter.
**Why this and not everything else:** a multi-week, multi-market search (efficient equities/futures
= no alpha, leak-free ML IC 0.004; simple 60/40 = ~0.7 Sharpe ceiling; energy = capital not
algorithm) found this is the **only edge that breaks the ~0.7 retail Sharpe ceiling**, and it does
so because crypto funding is structurally inefficient (retail perp longs over-pay) and the harvest
is *latency-insensitive* (no colocation needed — a carry trade, not a race).
**Validated metrics (this dataset, 135 coins, 20192026):**
| Gate | Result |
|---|---|
| Structural | funding positive 75.5% of the time; 78.8% of coins pay carry on an avg day |
| Delta-neutral | worst monthly bucket 1.5% (price risk genuinely hedged) |
| Liveness | 202526 weakness is *regime* (deleverage), not decay; positive-funding yield intact (~3bp, = 2019/2023 levels) |
| **Clean OOS** | filter chosen on 201924, applied blind to 202526 → **OOS Sharpe +5.1 raw** (IS-best); 9/12 strong-IS filters also OOS-positive; naive no-filter FAILS OOS (5.8) |
**Honest expected performance (realistic, after basis-vol haircut ÷~2.5):**
- **Sharpe ~23.6** (raw funding-only model shows 69; realistic accounts for basis/tracking-error vol we cannot model with single-price data)
- **APR ~1522%** on deployed capital (sensible-hurdle filter), regime-dependent
- **Max drawdown ~7 to 15%** (price-neutral; drawdowns are funding-flip + churn, not crashes)
- **The Sharpe does NOT show the real risk: counterparty/exchange failure (100% tail).**
---
## 2. The edge (why it exists, why it persists)
- **Perpetual futures funding** is the mechanism that tethers perp price to spot. When perps trade
at a premium (bullish retail leverage), **longs pay shorts** a periodic funding rate (typically
8-hourly on Binance/Bybit/OKX).
- **Net structural bias is positive**: retail over-leverages long → funding is positive ~75% of
the time. A `long spot + short perp` position is **delta-neutral** (spot and perp price moves
cancel) and **collects the funding** the short perp leg receives.
- **Why it persists** (not arbitraged to zero): capturing it requires capital *pre-positioned on
each venue*, active management, and acceptance of counterparty risk — operational/risk frictions,
not informational ones. It is the crypto analogue of an insurance premium.
- **Latency-insensitive:** funding accrues over 8h periods; seconds of execution delay are
immaterial. This is the single reason it is reachable by a non-colocated retail trader, unlike
cross-exchange *latency* arb (which needs colocation and is NOT this strategy).
---
## 3. Strategy rules (precise, implementable)
### 3.1 Universe
- Perpetual contracts on a **tier-1 venue** (see §4) that offers both spot and perp for the coin.
- **Liquidity floor:** trailing-30d mean quote volume **> $5M/day** (validated threshold).
- Exclude stablecoin-pair anomalies, delisting candidates, and coins without a spot leg.
### 3.2 Signal (the regime filter — the load-bearing piece)
- For each eligible coin, compute **trailing-30-day mean funding rate** `tf30`.
- **Qualify a coin iff `tf30 > 5 bp/day`** (≈ persistently well-paid carry). This is the validated
filter: strong both IS (+7.9) and OOS (+9.1), and it sits the book out of the deleverage regime
that whipsaws the naive "harvest anything positive" version (which FAILED OOS).
- Causality: position decisions use **yesterday's** funding (`tf30` through t1) to harvest day t.
Funding is persistent, so this is both realistic and leak-free.
### 3.3 Positioning
- For each qualifying coin: **long spot notional X + short perp notional X** (delta-neutral).
- **Equal-weight** across qualifying coins (validated; funding-weighting did not improve risk-adj).
- **Cash when nothing qualifies** — in a deleverage regime few/no coins clear the 5bp hurdle; the
book correctly de-risks to stablecoin (do NOT force-harvest a dead regime).
### 3.4 Rebalance & cost
- **Daily** check: enter coins crossing above the hurdle, exit coins falling below.
- Cost budget: strategy validated net of **10 bp round-trip** (perp + spot, both legs); survives to
~20 bp. Use **maker/limit orders** on entry/exit where possible to stay inside budget. Avoid
rebalancing on marginal hurdle-crossings (add hysteresis: enter >5bp, exit <3bp, to cut churn).
### 3.5 Sizing
- Per-coin notional = (deployed capital) / (number of qualifying coins), capped by §4 per-venue
limits.
- **Leverage:** the short-perp leg uses exchange margin; keep effective leverage low (≤23×) so a
funding-flip + basis move cannot trigger liquidation. The spot leg is the hedge — never let the
perp get liquidated while holding spot (that converts delta-neutral into naked long).
---
## 4. Risk management — counterparty is THE risk (read this twice)
Delta-neutrality removes *price* risk. It does **not** remove **counterparty/exchange risk**, which
is the actual way this strategy produces a 100% (FTX, Mt. Gox, QuadrigaCX). The Sharpe ratio is
blind to it. **This section matters more than the backtest.**
1. **Venue selection:** only tier-1 venues with **proof-of-reserves**, deep liquidity, and a
solvency track record. No yield-farming protocols, no obscure CEXs chasing higher funding.
2. **Collateral spreading:** spread capital across **≥23 venues** so no single failure is fatal.
*Caveat at $35k:* small capital makes spreading hard (per-venue minimums + cost). Below ~$50k,
counterparty concentration is unavoidable — treat the whole strategy as at-risk capital you can
lose entirely, and start tiny (§7).
3. **Withdrawal discipline:** keep only working collateral on exchanges; sweep profits to
self-custody on a schedule. Never let the on-exchange balance grow unmonitored.
4. **Liquidation guard:** low leverage (≤23×), automated margin-top-up alerts; the spot hedge must
always survive a perp-leg margin call.
5. **Deleverage kill-switch:** if fleet-wide funding goes broadly negative (the 2022/2025-26 signal:
`frac_pos < 0.5` or `mean_all < 0`), **go fully to cash** — the filter does this automatically,
but add a hard override.
6. **Stablecoin risk:** the cash leg sits in stablecoins (USDT/USDC) — itself a (smaller)
counterparty/depeg risk; diversify stable holdings.
7. **Position cap per venue:** no more than (venue risk budget) on any one exchange.
---
## 5. Expected performance (honest)
| Metric | Raw model (funding-only) | Realistic (after basis vol) |
|---|---|---|
| Sharpe | 69 | **~23.6** |
| APR (deployed) | ~1722% | ~1522% |
| Max drawdown | 7 to 15% | similar (price-neutral) |
| Worst month | 1.5% | basis noise adds some |
| Regime behavior | cash in deleverage | cash in deleverage |
- **The realistic Sharpe (~23.6) is still 35× the 0.7 retail ceiling** for everything else found.
- **Dollar reality at $35k:** ~1520% APR ≈ **$57k/yr** expected, market-neutral — but with full
counterparty-loss tail. Scales linearly with capital (Sharpe is scale-invariant; capacity is far
above retail size).
- **Unmodeled / open:** basis-vol (need spot+perp tick pairs to measure precisely), and the
counterparty tail (un-backtestable — managed via §4, not measured).
---
## 6. Validation status
**Passed:**
- Structural (funding positive 75.5%), delta-neutral (worst month 1.5%), liveness (regime not
decay), clean OOS (IS-chosen filter holds OOS +5.1, 9/12 robust, naive no-filter fails OOS).
- Scripts: `scripts/surfer/crypto_funding_harvest.py`, `crypto_funding_liveness.py`,
`crypto_funding_oos.py`.
**Not yet validated (must close before scaling):**
- **Basis vol** — current Sharpe is funding-only; get spot+perp price pairs and remodel realized P&L
including tracking error → confirm realistic Sharpe.
- **OOS length** — only ~1.5y OOS (single deleverage regime); held through the *hard* case, but
extend as data accrues.
- **Counterparty** — un-backtestable; validated only by §4 design discipline.
- **Live execution** — fills, slippage, funding-timestamp capture, margin mechanics on the real
venue (the paper-forward test, §7).
---
## 7. Phased rollout (gate each phase before the next)
1. **Paper-forward (48 weeks):** run the daily algorithm against *live* funding feeds, record
intended positions + realized funding, **no capital**. Confirms the edge persists on truly
unseen forward data (mirrors the surfer PoC's cron paper-test). Gate: forward funding-APR
positive and consistent with backtest.
2. **Micro-live ($13k, 1 venue):** smallest real size to validate execution, fills, funding
capture, margin mechanics, cost budget. Gate: realized net ≈ paper, cost ≤ 15bp round-trip.
3. **Small-live ($515k, 2 venues):** add collateral spreading, confirm counterparty controls and
withdrawal discipline operate. Gate: clean operation through a funding-flip / mini-deleverage.
4. **Scale ($35k+):** deploy full capital across ≥3 venues, full §4 risk stack engaged.
**Stop at any gate that fails.** This is the same diagnostic-first discipline that caught every
false positive in the search (the inflated 5.68 Sharpe, the carry trap, the equity factors).
---
## 8. Go / no-go
**Go** if: paper-forward (§7.1) confirms positive forward funding-APR AND you accept that this is
**at-risk crypto capital with a counterparty 100% tail** in exchange for a realistic ~23.6 Sharpe
market-neutral return.
**No-go** if: you cannot accept the counterparty tail, OR paper-forward shows the edge has decayed
(funding compression to ~cost), OR you cannot spread across ≥2 reputable venues at your capital.
**The honest framing:** this is the one validated way past 0.7 the whole search found. It is real,
alive, and reachable — and the price of admission is crypto + counterparty risk, not market risk.
That trade-off is a values decision, now made with complete information.

View File

@@ -0,0 +1,86 @@
# CAGR-Maximizer — Honest Reference Plan
**Date:** 2026-06-08
**Purpose:** the honest answer to "how do we build the most wealth?" after exhaustively testing strategies,
the adaptive multi-strat book, leverage, and risk-management. Comparison tool: `scripts/surfer/strategy_compare.py`.
---
## The core truth (measured, not assumed)
1. **CAGR builds terminal wealth, not Sharpe.** Over a long horizon with contributions, the highest-CAGR
asset wins — even at lower Sharpe. (SPY 0.64 Sharpe but 12.4% CAGR beats the book's 0.96 Sharpe / 5.1% CAGR on money.)
2. **More CAGR = more risk. Always. No exception.** Every risk-managed variant (the book, the overlay,
blends) gives up CAGR for lower drawdown. Risk management is *insurance you pay for*, not a free improvement.
3. **The only "free" CAGR is drag reduction** — taxes, fees, and (biggest) not selling at the bottom.
4. **The book's apparent Sharpe edge (0.96 vs 0.64) was an rf=0 artifact.** Measured as excess-over-financing
(what matters for leverage), equities (0.49) actually beat the diversified book (0.40) in this era —
so levering the book does NOT beat buy-hold equity. The institutional risk-parity edge needs the book's
*excess* Sharpe > equity's, which it wasn't.
5. **The leverage moat is cheap financing.** Futures/box ≈ SOFR+spread (~3%); retail margin ~6.5% destroys it.
---
## The candidates (measured 20062026, incl. 2008 55%)
| Strategy | CAGR | vol | Sharpe | maxDD | $360k+$8k/mo → 20y |
|---|---|---|---|---|---|
| **SPY buy-hold (1.0x)** | ~10.6% | 19% | 0.64 | **55%** | ~$7.8M |
| SPY 1.21.3x (cheap futures) | ~1213% | 2325% | ~0.6 | **63 to 67%** | ~$8.89.2M |
| SPY 1.5x | ~14.5% | 28% | — | 73% | ~$10.1M |
| 60/40 | ~8% | 14% | 0.73 | 20 to 30% | ~$6.5M |
| 70/30 SPY+book | ~10% | 14% | 0.73 | 40% | ~$9.0M |
| SPY + adaptive overlay | ~8% | 11% | 0.77 | 18% | ~$6.7M |
| **Adaptive multi-strat book** | ~56% | 5% | **0.96** | **10%** | ~$4.34.9M |
(SPY ≥2x: CAGR peaks ~2x then volatility-drag falls; 84%+ drawdown = ruin/margin-call territory. Not viable.)
---
## CAGR levers, ranked by sense
| Lever | Effect | Honesty |
|---|---|---|
| **Max equity allocation, held** | base ~1011%/yr | highest-return asset; reliable |
| **Reduce drag** (tax-advantaged account, cheap index funds, never sell, stay invested) | **+13%/yr** | **free + reliable — most people leave this on the table** |
| **Time + contributions ($8k/mo)** | dominant terminal driver | your strongest card |
| **Modest cheap leverage (~1.21.3x via futures)** | +~1.52%/yr (+$11.4M/20y) | amplifies drawdown to 6367% + margin-call risk |
| Factor tilts (small-cap value, momentum, quality) | hist. +12%/yr | less reliable forward |
| ~~Risk-managed book / overlay~~ | **4%/yr CAGR** | a risk *reducer*, not a CAGR maximizer |
---
## The leverage reality (the only knob with real upside — handle with care)
- 1.21.3x boosts CAGR ~+1.52%/yr (~$11.4M over 20y) but turns the 55% crash into **6367%**.
- **67% on a $2M pot = $660k at the 2009 bottom**, while still contributing into the abyss.
- **Margin calls:** fixed-notional futures in a 55% crash force liquidation at the bottom → *realized* ruin
(the backtest "survives" only because it's daily-rebalanced math). Lived experience is worse than backtest.
- Above ~1.5x: reckless (73%+); above ~2x: ruin.
- **The real constraint is not the math — it's whether you survive a 65% drawdown + margin without forced/panic selling.**
---
## Recommendation
For a disciplined accumulator with a strong savings rate (~$8k/mo), long horizon, and stable income:
1. **Max equity (100%), in a tax-advantaged account, cheap index funds, contribute monthly, never sell.**
Base case ~$7.8M over 20y. Simple, cheap, the most for the disciplined.
2. **Optional ~1.21.3x via futures (cheap financing)** — only if you can survive (financially + emotionally)
a 65% drawdown + margin calls without forced selling. Adds ~$11.4M over 20y.
3. **Always: reduce drag** (tax-efficiency, low fees, stay invested) — the free CAGR.
4. **The adaptive multi-strat book is NOT the wealth-maximizer** — it's the *capital-preservation / low-stress / withdrawal-phase* tool. Use it if you'd panic-sell equities, are near withdrawal, or value sleep over ~4%/yr.
**The decision hinges on one honest question: do you sell at the bottom of a crash, or hold?**
- Hold → high equity (±1.3x), simplest, most money.
- Unsure → 70/30 or the overlay (pay return for a tolerable ride).
- Need capital preservation → the book.
---
## What's deployed
- **Adaptive multi-strat book:** live on IBKR paper via the K8s CronJob (autonomous, daily eval) — the *low-drawdown* reference.
- **Comparison tool:** `scripts/surfer/strategy_compare.py` — re-runnable side-by-side of all candidates (backtest + trajectory).
- **Honest bottom line:** there is no secret signal and no clever leverage trick that beats "high-return assets, cheap, held long, funded heavily." We tested them all. The wealth levers are savings rate, time, low costs, and the discipline to not sell.

View File

@@ -0,0 +1,80 @@
# Decommission Dead Rust Infra — Design (Phase 1 of infra consolidation)
**Date:** 2026-06-21
**Status:** Design (pending approval)
**Repo:** foxhunt (where the platform IaC currently lives)
## Motivation
The active trading work is now the Python **fxhnt** fund; the Rust **foxhunt** ML/HFT system is dormant.
Its dedicated infra (GPU training pools, Rust build/training caches, GPU CI runners, training artifact
buckets, training manifests) is unused but still declared/provisioned. Phase 1 decommissions it — for
cost/clutter savings and to shrink the surface before Phase 2 (relocating the remaining *platform* IaC
into `fxhnt/infra`, a separate spec).
**Verified unused (2026-06-21):** L40S + H100 pools have 0 nodes; `cargo-target-{cpu,cuda,cuda-test}` +
`feature-cache-pvc` are `Used By: <none>`; no gitlab-runner pods running.
## Scope — what gets removed (all verified unused)
1. **Terraform (kapsule module + terragrunt.hcl):** set to `false`
- `enable_ci_training_l40s_pool` (L40S-1-48G GPU pool)
- `enable_ci_training_h100_pool` (H100-1-80G GPU pool)
- `enable_ci_compile_cpu_hm_pool` (POP2-HM-32C-256G precompute pool — Rust `precompute_features` only)
`terragrunt apply` destroys exactly these 3 pools. Also delete their now-dead var blocks/resources.
2. **Helm uninstall** (foxhunt ns): `gitlab-runner-h100`, `gitlab-runner-h100-sxm`, `gitlab-runner-h100x2`
(Rust GPU CI runners). **Main `gitlab-runner`:** verify nothing non-Rust uses it (fxhnt has no
`.gitlab-ci.yml`; cockpit deploys via Argo) → uninstall if confirmed dead, else keep. (confirm step)
3. **PVCs delete** (foxhunt ns — PURE BUILD CACHES only, unmounted-verified, ~175 GB reclaimed):
`cargo-target-cpu` (60Gi), `cargo-target-cuda` (45Gi), `cargo-target-cuda-test` (30Gi),
`sccache-cpu` (20Gi), `sccache-cuda` (20Gi). These hold zero data — regenerated on any build.
4. **MinIO buckets delete** (NO market data — verified 0 `.dbn` objects): `foxhunt-binaries` (2.3 GiB
compiled binaries), `foxhunt-training-results` (4.4 GiB run logs/outputs), `foxhunt-models` (empty).
5. **Repo manifests/templates remove** (foxhunt repo): `infra/k8s/training/`, `infra/k8s/gpu-overlays/`,
the Rust Argo workflow templates (`train-multi-seed-template.yaml`, `lob-backtest-sweep-template.yaml`,
`ci-pipeline-template.yaml`, alpha-rl train templates), `infra/k8s/jobs/download-trades-job.yaml`
(databento), the 3 GPU-runner helm value files. Also remove the dead Argo `WorkflowTemplate`s from the
cluster (`kubectl delete wftmpl`) for the Rust train/backtest pipelines.
## 🛑 EXPLICITLY PRESERVE (do NOT delete — market data + reusable assets)
- **`training-data-pvc` (500 GiB)** — the raw Databento MBP-10 `.dbn` market data. KEEP.
- **`foxhunt-training-data` bucket (38 GiB, 54 `.dbn`/`.zst`)** — Databento market data (expensive to
re-acquire; the fund's `.dbn` backtests read it). KEEP.
- **`test-data-pvc` (50 GiB)** — `.dbn` test subsets (tier-1.5 smoke uses test_data). KEEP (verify, don't delete).
- **`feature-cache-pvc` (100 GiB)** — derived ML features; regenerable but compute-costly. KEEP (conservative).
- All fxhnt/platform data: `fxhnt-backtest-data`, `fxhnt-surfer-data`, `multistrat-state`, `questdb-pvc`,
`tempo-data`, `netbird-data`, `foxhunt-gitlab-*` + `foxhunt-backups` buckets.
General rule: delete only **pure compute/build artifacts** (caches, compiled binaries, run logs, GPU pools);
**never** anything holding `.dbn`/market data or any potentially-reusable dataset.
## Out of scope (KEEP — shared platform / Python fund)
`ci-compile-cpu` pool (Python cockpit builds), platform pool, GitLab, MinIO, monitoring, Mattermost,
Stalwart, Kanidm, NetBird, DNS, databases, cert-manager, tailscale proxy, public-gateway, dagster/cockpit,
the fxhnt forward-track jobs. Phase 2 (IaC relocation to `fxhnt/infra` + TF-state move) is a separate spec.
## Execution order (each destructive step verified + confirmed)
1. **Terraform first**: edit terragrunt.hcl (3 `enable_*=false`), `terragrunt plan` → **STOP unless the
plan shows ONLY the 3 pools destroyed + 0 other destroys**; then `apply`.
2. **Helm uninstalls** (GPU runners; main runner only after the use-check confirms dead).
3. **PVC deletes** (re-confirm `Used By: <none>` immediately before each delete — irreversible).
4. **Bucket deletes** (list contents first; irreversible — explicit confirm; `mc rb --force` via the
port-forward + minio creds).
5. **Repo cleanup**: remove the dead manifests/templates + module var blocks, `kubectl delete wftmpl` the
dead templates, commit.
## Risks / safety
- **Irreversible**: PVC + bucket deletes, GPU-pool destroys. Mitigation: verify-unused immediately before
each; Terraform plan reviewed for 0-unexpected-destroys; explicit confirm on PVC/bucket deletes.
- **Mis-scope risk**: a kept resource accidentally listed. Mitigation: the Terraform plan gate (only the 3
pools) + the `Used By` re-check + bucket content listing before delete.
- **Main gitlab-runner**: don't uninstall without confirming no active consumer (could break a CI path).
- Cluster health unaffected: nothing here touches the cockpit/dagster/fund/platform services.
## Acceptance criteria
- `terragrunt plan` (kapsule) shows the 3 GPU/precompute pools gone, then clean (no changes).
- GPU-runner helm releases uninstalled; main runner resolved (kept or uninstalled per check).
- The 5 build-cache PVCs deleted (cargo×3 + sccache×2, ~175 GB reclaimed); 3 Rust buckets deleted
(binaries, training-results, models). **`.dbn` market data untouched** (training-data-pvc + the
foxhunt-training-data bucket + test-data-pvc still present, byte-for-byte).
- Dead training/gpu manifests + Argo templates removed from repo + cluster; committed.
- Cockpit (`dashboard.fxhnt.ai` 200), dagster, and the fund tracks still healthy post-cleanup.

View File

@@ -7,18 +7,25 @@ metadata:
labels:
app.kubernetes.io/part-of: foxhunt
rules:
# apps: roll + apply Deployments. watch is required by `kubectl rollout status`.
- apiGroups: ["apps"]
resources: [deployments]
verbs: [get, list, patch]
verbs: [get, list, watch, patch]
- apiGroups: ["argoproj.io"]
resources: [workflowtemplates, eventsources, sensors, eventbus]
verbs: [get, list, create, update, patch]
# core: services/configmaps, plus serviceaccounts (tailscale-dashboard, dagster) and the forward-track PVC
# — added so the fxhnt-cockpit deploy can `kubectl apply` its full manifest set without a partial-apply failure.
- apiGroups: [""]
resources: [services, configmaps]
resources: [services, configmaps, serviceaccounts, persistentvolumeclaims]
verbs: [get, list, create, update, patch]
- apiGroups: ["networking.k8s.io"]
resources: [networkpolicies]
verbs: [get, list, create, update, patch]
# batch: the fxhnt-forward CronJob
- apiGroups: ["batch"]
resources: [cronjobs]
verbs: [get, list, create, update, patch]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: [roles, rolebindings]
verbs: [get, list, create, update, patch, bind, escalate]

View File

@@ -0,0 +1,72 @@
# ACME (Let's Encrypt) wildcard cert for *.fxhnt.ai via cert-manager + Scaleway DNS-01.
# Replaces the prior MANUAL, unrenewed LE cert that expired 2026-05-26 (the tailscale-proxy
# fell back to the GitLab self-signed cert). DNS-01 is required: the services are tailnet-only
# (*.fxhnt.ai -> 100.x Tailscale CGNAT) so HTTP-01 is unreachable, and wildcards need DNS-01.
#
# Solver creds: cert-manager/scaleway-dns-credentials (SCW_ACCESS_KEY/SCW_SECRET_KEY, copied from
# foxhunt/scaleway-credentials). Webhook: scaleway-certmanager-webhook (helm, cert-manager ns).
# The issued secret gitlab-tls-cert is mounted by infra/k8s/gitlab/tailscale-proxy.yaml.
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: jeroen@grusewski.nl
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- dns01:
webhook:
groupName: acme.scaleway.com
solverName: scaleway
config:
accessKeySecretRef:
name: scaleway-dns-credentials
key: SCW_ACCESS_KEY
secretKeySecretRef:
name: scaleway-dns-credentials
key: SCW_SECRET_KEY
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: jeroen@grusewski.nl
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
webhook:
groupName: acme.scaleway.com
solverName: scaleway
config:
accessKeySecretRef:
name: scaleway-dns-credentials
key: SCW_ACCESS_KEY
secretKeySecretRef:
name: scaleway-dns-credentials
key: SCW_SECRET_KEY
---
# Wildcard cert for the tailscale-proxy. issuerRef flips staging->prod after staging validates;
# secretName flips to gitlab-tls-cert (the secret the proxy mounts) for the prod issuance.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: fxhnt-wildcard
namespace: foxhunt
spec:
secretName: gitlab-tls-cert # takes over the secret the tailscale-proxy mounts
privateKey:
rotationPolicy: Always # old manual key had a mismatching algorithm; regenerate on issue/renew
dnsNames:
- fxhnt.ai
- "*.fxhnt.ai"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer

View File

@@ -226,7 +226,11 @@ data:
}
}
# Web Dashboard — dashboard.fxhnt.ai
# fxhnt cockpit — dashboard.fxhnt.ai
# Proxies to the cockpit's Tailscale node (peer-to-peer over the tailnet) rather than the cluster Service:
# the pod CIDR 100.64.0.0/15 overlaps Tailscale CGNAT, so this kernel-mode proxy can't reach platform-pool
# pods via ClusterIP, but it CAN reach another tailnet node. IP is stable while the cockpit's TS state
# secret (fxhnt-dashboard-ts-state) persists. (Was web-dashboard.foxhunt.svc — replaced by the fxhnt cockpit.)
server {
listen 443 ssl;
server_name dashboard.fxhnt.ai;
@@ -236,7 +240,7 @@ data:
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://web-dashboard.foxhunt.svc.cluster.local:80;
proxy_pass http://100.81.150.18:80; # fxhnt-dashboard tailnet node (TCP:80 -> cockpit :8080)
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -57,6 +57,16 @@ global:
connection:
secret: gitlab-s3-credentials
key: connection
# Terraform/OpenTofu state object storage. MUST be present — without it the
# Terraform::StateUploader has no object store and every TF-state API call 403s
# ("Object Storage is not enabled for Terraform::StateUploader"). State files already
# live in foxhunt-gitlab-artifacts (6b/86/<sha256(project_id)>/<state>/<ver>.tfstate).
terraformState:
enabled: true
bucket: foxhunt-gitlab-artifacts
connection:
secret: gitlab-s3-credentials
key: connection
gitlab_kas:
enabled: false

View File

@@ -18,6 +18,9 @@ spec:
- podSelector:
matchLabels:
app.kubernetes.io/name: broker-gateway
- podSelector:
matchLabels:
app: multistrat-rebalancer
ports:
- protocol: TCP
port: 4002

View File

@@ -0,0 +1,123 @@
# Multi-strat rebalancer — weekly CronJob running the adaptive book on IBKR (paper) via the
# in-cluster ib-gateway. Production deployment of scripts/surfer/multistrat_bot.py.
#
# Code is delivered via ConfigMap (no image build needed — lean). Create/refresh it with:
# kubectl create configmap multistrat-bot-code -n foxhunt \
# --from-file=scripts/surfer/multistrat_bot.py \
# --from-file=scripts/surfer/multistrat_paper.py \
# --dry-run=client -o yaml | kubectl apply -f -
#
# SAFETY: MULTISTRAT_EXECUTE defaults to "false" (cluster dry-run). After validating a few dry-run
# CronJob logs, flip to "true" to place paper orders. Account is paper (ib-gateway TRADING_MODE=paper);
# the bot additionally refuses any non-DU (live) account unless MULTISTRAT_ALLOW_LIVE_CONFIRMED is set.
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: multistrat-state
namespace: foxhunt
labels:
app: multistrat-rebalancer
spec:
accessModes: ["ReadWriteOnce"] # sequential CronJob runs (Forbid concurrency) — RWO is fine
resources:
requests:
storage: 1Gi
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: multistrat-rebalancer
namespace: foxhunt
spec:
podSelector:
matchLabels:
app: multistrat-rebalancer
policyTypes: [Egress]
egress:
- to: [] # DNS
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}
- to: # in-cluster ib-gateway (4002 API + 4004 socat bridge for pod-to-pod)
- podSelector:
matchLabels:
app: ib-gateway
ports:
- {protocol: TCP, port: 4002}
- {protocol: TCP, port: 4004}
- to: # internet 443: PyPI (pip) + Yahoo (prices), no internal ranges
- ipBlock:
cidr: 0.0.0.0/0
except: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
ports:
- {protocol: TCP, port: 443}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: multistrat-rebalancer
namespace: foxhunt
labels:
app: multistrat-rebalancer
app.kubernetes.io/part-of: foxhunt
spec:
schedule: "35 14 * * 1-5" # weekdays 14:35 UTC (~1h after US open) — daily risk re-eval; hysteresis prevents churn
concurrencyPolicy: Forbid
startingDeadlineSeconds: 3600
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 10
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 600
template:
metadata:
labels:
app: multistrat-rebalancer
spec:
restartPolicy: Never
nodeSelector:
k8s.scaleway.com/pool-name: platform
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: rebalancer
image: python:3.12-slim
command: ["/bin/sh", "-c"]
args:
- >-
pip install --quiet --no-cache-dir ib_async==2.1.0 numpy &&
python /app/multistrat_bot.py run
env:
- {name: IB_HOST, value: "ib-gateway"}
- {name: IB_PORT, value: "4004"} # socat bridge: relays via localhost so IB Gateway trusts it (4002 = pod-IP, untrusted -> handshake timeout)
- {name: IB_CLIENT_ID, value: "11"}
- {name: MULTISTRAT_MAXLEV, value: "1.0"}
- {name: MULTISTRAT_HYST, value: "0.03"}
- {name: MULTISTRAT_REBALANCE_DAYS, value: "1"} # daily eval (weekday cron); hysteresis (3%) gates actual trades
- {name: MULTISTRAT_DD_HALT, value: "0.20"}
- {name: MULTISTRAT_MAX_ORDER, value: "0.30"}
- {name: MULTISTRAT_EXECUTE, value: "true"} # ARMED: places paper orders (account DU* paper-guarded; live needs MULTISTRAT_ALLOW_LIVE_CONFIRMED)
- {name: MULTISTRAT_STATE, value: "/data/state.json"}
- {name: MULTISTRAT_LOG, value: "/data/bot.log"}
- {name: PIP_DISABLE_PIP_VERSION_CHECK, value: "1"}
- {name: PYTHONUNBUFFERED, value: "1"}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests: {memory: "256Mi", cpu: "100m"}
limits: {memory: "512Mi", cpu: "500m"}
volumeMounts:
- {mountPath: /app, name: code, readOnly: true}
- {mountPath: /data, name: state}
volumes:
- name: code
configMap:
name: multistrat-bot-code
- name: state
persistentVolumeClaim:
claimName: multistrat-state

View File

@@ -14,9 +14,12 @@ inputs = {
platform_type = "DEV1-L"
platform_max_size = 3
# CPU compile pool (POP2-HC-32C-64G — 32 vCPU, 64GB RAM, high clock)
# CI build pool — right-sized for the Python fxhnt cockpit/deploy builds (kaniko + pip, ~2-6 vCPU).
# Was POP2-HC-32C-64G (32 vCPU/64GB) for the now-dormant Rust cargo compile + precompute_features;
# downsized 2026-06-21 to POP2-4C-16G (in stock in fr-par-2; the 32C type hit a zone stock-out that
# jammed the autoscaler — "Resource POP2-HC-32C-64G is out of stock"). Far cheaper, scale-to-zero.
enable_ci_compile_cpu_pool = true
ci_compile_cpu_type = "POP2-HC-32C-64G"
ci_compile_cpu_type = "POP2-4C-16G"
ci_compile_cpu_max_size = 4
# High-memory CPU pool (POP2-HM-32C-256G — 32 vCPU, 256GB RAM).

View File

@@ -13,4 +13,5 @@ dependency "kapsule" {
inputs = {
private_network_id = dependency.kapsule.outputs.private_network_id
gateway_type = "VPC-GW-S"
bastion_enabled = true # matches the live gateway (carried over from the old kapsule gw config)
}

View File

@@ -3,31 +3,11 @@ resource "scaleway_vpc_private_network" "foxhunt" {
region = var.region
}
# VPC Public Gateway — provides NAT (masquerade) for private nodes
# and DHCP with default route propagation to prevent DNS deadlock.
# See incident_dns_deadlock.md for why this is critical.
resource "scaleway_vpc_public_gateway" "foxhunt" {
name = "${var.cluster_name}-gw"
type = "VPC-GW-S"
zone = "${var.region}-2"
bastion_enabled = true
bastion_port = 61000
}
resource "scaleway_vpc_public_gateway_dhcp" "foxhunt" {
subnet = "172.16.0.0/22"
push_default_route = true
push_dns_server = true
zone = "${var.region}-2"
}
resource "scaleway_vpc_gateway_network" "foxhunt" {
gateway_id = scaleway_vpc_public_gateway.foxhunt.id
private_network_id = scaleway_vpc_private_network.foxhunt.id
dhcp_id = scaleway_vpc_public_gateway_dhcp.foxhunt.id
enable_masquerade = true
zone = "${var.region}-2"
}
# NAT gateway (foxhunt-gw) is owned by the dedicated `public-gateway` module (IPAM mode +
# dedicated IP), which depends on this module's private_network_id output. The legacy
# gateway/dhcp/gateway_network resources that used to live here were a pre-refactor duplicate
# (never in this module's state) and were removed 2026-06-21 to stop `terragrunt apply` here
# from trying to create a second gateway. See reference_ci_pool_and_tfstate / incident_dns_deadlock.
resource "scaleway_k8s_cluster" "foxhunt" {
name = var.cluster_name

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Install GPU PyTorch for the surfer experiments (local RTX 3050 Ti, driver 580 → CUDA 12.x).
#
# RECOMMENDED (no sudo — installs to ~/.local, matching your existing numpy/scipy/databento):
# bash scripts/install_torch_gpu.sh
#
# System-wide (only if you really want it):
# sudo bash scripts/install_torch_gpu.sh
#
# Pick a different CUDA wheel tag if cu124 ever 404s (cu126 / cu128 also work on driver 580):
# bash scripts/install_torch_gpu.sh cu126
set -euo pipefail
CUDA_TAG="${1:-cu124}"
TORCH_INDEX="https://download.pytorch.org/whl/${CUDA_TAG}"
if [ "$(id -u)" -eq 0 ]; then
echo "[install] running as ROOT → system-wide site-packages (--break-system-packages)"
FLAGS="--break-system-packages"
else
echo "[install] running as USER → ~/.local (matches existing packages; no sudo needed)"
FLAGS="--user --break-system-packages"
fi
echo "[install] torch from ${TORCH_INDEX}"
python3 -m pip install ${FLAGS} torch --index-url "${TORCH_INDEX}"
echo "[verify] importing torch + checking CUDA on the local GPU"
python3 - <<'PY'
import torch
print(" torch:", torch.__version__)
print(" cuda available:", torch.cuda.is_available())
if torch.cuda.is_available():
print(" device:", torch.cuda.get_device_name(0),
"| capability:", torch.cuda.get_device_capability(0))
x = torch.randn(1_000_000, device="cuda")
print(" GPU tensor op OK, sum =", float(x.sum()))
else:
raise SystemExit("CUDA NOT available to torch — check driver / try a different CUDA_TAG (cu126/cu128)")
print(" ✅ GPU PyTorch ready")
PY

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""Frontier (a) — cross-exchange funding dispersion: edge distinct from momentum?
Fetch funding from Bybit + OKX (free) to pair with Binance funding + prices (crypto_pit).
Build cross-sectional signals among multi-venue coins: multi-venue-avg funding (robust carry),
cross-venue dispersion (positioning stress), Binance-premium. Validate each + correlation to
the full crypto momentum book + marginal-alpha regression. Realistic: Reff = return - Binance
funding (traded venue), death-excl, 10bp. Honest prior: low (cross-venue spreads arbitraged).
"""
import json
import math
import os
import sys
import time
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
from surfer_poc import compute_weights, CFG # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_MS = 86_400_000
XF = "data/surfer/xfund"
COINS = ["BTC", "ETH", "SOL", "XRP", "DOGE", "ADA", "AVAX", "LINK", "LTC", "DOT",
"NEAR", "ATOM", "FIL", "ETC", "XLM", "UNI", "AAVE", "BNB"]
def _get(u):
try:
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
except Exception:
return None
def bybit(coin):
cache = f"{XF}/bybit_{coin}.json"
if os.path.exists(cache):
return {int(k): v for k, v in json.load(open(cache)).items()}
out, end = {}, int(time.time() * 1000)
for _ in range(40):
r = _get(f"https://api.bybit.com/v5/market/funding/history?category=linear&symbol={coin}USDT&endTime={end}&limit=200")
lst = (r or {}).get("result", {}).get("list", [])
if not lst:
break
for x in lst:
t = int(x["fundingRateTimestamp"]); out.setdefault(t // DAY_MS, 0.0)
out[t // DAY_MS] += float(x["fundingRate"])
end = min(int(x["fundingRateTimestamp"]) for x in lst) - 1
if len(lst) < 200:
break
time.sleep(0.06)
os.makedirs(XF, exist_ok=True); json.dump({str(k): v for k, v in out.items()}, open(cache, "w"))
return out
def okx(coin):
cache = f"{XF}/okx_{coin}.json"
if os.path.exists(cache):
return {int(k): v for k, v in json.load(open(cache)).items()}
out, before = {}, ""
for _ in range(60):
u = f"https://www.okx.com/api/v5/public/funding-rate-history?instId={coin}-USDT-SWAP&limit=100"
if before:
u += f"&after={before}"
r = _get(u); data = (r or {}).get("data", [])
if not data:
break
for x in data:
t = int(x["fundingTime"]); out.setdefault(t // DAY_MS, 0.0)
out[t // DAY_MS] += float(x["fundingRate"])
before = min(int(x["fundingTime"]) for x in data)
if len(data) < 100:
break
time.sleep(0.06)
os.makedirs(XF, exist_ok=True); json.dump({str(k): v for k, v in out.items()}, open(cache, "w"))
return out
def main():
syms, days, close, qv, fund = pit_sweep.load()
idx = {s: j for j, s in enumerate(syms)}
coins = [c for c in COINS if c + "USDT" in idx]
print(f"fetching Bybit+OKX funding for {len(coins)} coins...")
by = {c: bybit(c) for c in coins}
ok = {c: okx(c) for c in coins}
nby = sum(1 for c in coins if len(by[c]) > 100); nok = sum(1 for c in coins if len(ok[c]) > 100)
print(f" Bybit covered {nby}/{len(coins)}, OKX covered {nok}/{len(coins)}")
N = len(coins); T = len(days)
di = {int(days[t]): t for t in range(T)}
bn_f = np.full((T, N), np.nan); by_f = np.full((T, N), np.nan); ok_f = np.full((T, N), np.nan)
cl = np.full((T, N), np.nan)
for j, c in enumerate(coins):
col = idx[c + "USDT"]
cl[:, j] = close[:, col]; bn_f[:, j] = fund[:, col]
for d, v in by[c].items():
if d in di:
by_f[di[d], j] = v
for d, v in ok[c].items():
if d in di:
ok_f[di[d], j] = v
R = np.zeros((T, N)); R[1:] = np.log(cl)[1:] - np.log(cl)[:-1]; R = np.where(np.isfinite(R), R, 0.0)
bnf = np.where(np.isfinite(bn_f), bn_f, 0.0)
Reff = R - bnf # trade on Binance -> pay Binance funding
# cross-venue stack
stack = np.stack([bn_f, by_f, ok_f]) # [3,T,N]
avg_f = np.nanmean(stack, axis=0) # multi-venue avg funding
disp_f = np.nanstd(stack, axis=0) # cross-venue dispersion
prem = bn_f - np.nanmean(np.stack([by_f, ok_f]), axis=0) # Binance premium vs others
def roll_mean(A, L):
out = np.full_like(A, np.nan)
for t in range(L, len(A)):
out[t] = np.nanmean(A[t - L:t], axis=0)
return out
sigs = {
"carry_BINANCE_only": -roll_mean(np.where(np.isfinite(bn_f), bn_f, np.nan), 7), # control: is it just major-coin carry?
"carry_multivenue": -roll_mean(avg_f, 7), # long low avg funding (robust carry)
"venue_dispersion": -roll_mean(disp_f, 7), # low disagreement? test
"venue_dispersion+": roll_mean(disp_f, 7), # high disagreement
"binance_premium": -roll_mean(prem, 7), # fade Binance-crowded
}
valid = np.isfinite(cl) & np.isfinite(avg_f)
year = (1970 + days / 365.25).astype(int)
def book(sig):
s = sig.copy(); s[~valid] = np.nan
return pnl_w(xs_weights(s), Reff, cost_bp=10)
# full-universe momentum book for correlation
cw, _ = compute_weights(close, qv, days, CFG)
cR = np.zeros_like(close); cR[1:] = np.log(close)[1:] - np.log(close)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0)
cf = np.where(np.isfinite(fund), fund, 0.0)
mom = np.sum(cw[:-1] * (cR - cf)[1:], axis=1)
mom_by = {int(days[1:][i]): mom[i] for i in range(len(mom))}
print(f"\n===== CROSS-EXCHANGE FUNDING — {len(coins)} multi-venue coins, death-excl, 10bp, deflate N=55 =====")
print(f"{'signal':>18} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5} {'corr_mom':>8} {'marg_t':>7}")
pdays = days[1:]
for nm, sg in sigs.items():
p = book(sg); v = validate(p, days, 55)
pb = {int(pdays[i]): p[i] for i in range(len(p))}
common = sorted(set(pb) & set(mom_by))
a = np.array([mom_by[d] for d in common]); b = np.array([pb[d] for d in common])
m = np.isfinite(a) & np.isfinite(b); a, b = a[m], b[m]
corr = float(np.corrcoef(a, b)[0, 1]) if len(a) > 100 else float("nan")
beta1 = np.cov(a, b)[0, 1] / (np.var(a) + 1e-12); res = b - beta1 * a
mt = float(res.mean() / (res.std() / math.sqrt(len(res)) + 1e-12))
print(f"{nm:>18} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f} {corr:>+8.2f} {mt:>+7.2f}")
print("\nVERDICT: any signal with full+OOS+CPCVmed>0, DSR>0.5, low |corr_mom|, AND marg_t>2 = real distinct edge.")
print("(marg_t = t-stat of marginal alpha vs momentum book). Honest prior: cross-venue spreads arbitraged -> expect fail.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Cross-venue crypto funding-arb scanner + paper-forward (price-neutral, no spot leg needed).
Funding differs across venues. For a coin on >=2 liquid venues: SHORT the highest-funding venue
perp + LONG the lowest-funding venue perp (same coin) -> price-neutral (both perps, opposite),
collect the funding DIFFERENCE (max-min). No spot leg -> solves the single-venue hedgeability block.
Funding intervals differ (Binance/Bybit 8h, Hyperliquid 1h) -> normalize to DAILY before comparing.
No agents, no key, no capital. Venues: Binance, Bybit, Hyperliquid (all bulk-fetch).
python3 cross_venue_funding.py scan live top cross-venue spreads
python3 cross_venue_funding.py run book daily carry on the top-K book, persist (cron)
python3 cross_venue_funding.py status cumulative paper track record
(alias: paper == run)
"""
import datetime
import json
import math
import os
import sys
import urllib.request
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
STATE = os.path.join(_REPO, "data/surfer/crossvenue_state.json")
LIQ = 10e6 # >$10M/day on BOTH legs (clean, fungible)
MAXF = 0.005 # exclude legs with |daily funding| > 50bp/day = distress/artifact (un-tradeable)
TOPK = 10 # book the top-K spreads
COST_RT = 0.0010 # ~10bp round-trip (2 perp legs, maker)
HURDLE = 0.0005 # 5bp/day spread to show in scan
ENTRY = 0.0010 # hysteresis: only ENTER a new pair above 10bp/day
EXIT = 0.0005 # hysteresis: HOLD a pair until its spread decays below 5bp/day (cuts turnover)
def get(url, post=None):
data = json.dumps(post).encode() if post else None
h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
if post:
h["Content-Type"] = "application/json"
for a in range(3):
try:
return json.loads(urllib.request.urlopen(urllib.request.Request(url, data=data, headers=h), timeout=25).read())
except Exception:
if a == 2:
raise
return None
def base(sym):
for q in ("USDT", "USDC"):
if sym.endswith(q):
return sym[:-len(q)]
return sym
def binance(): # {coin: (daily_funding, daily_vol)}
fund = {x["symbol"]: float(x["lastFundingRate"]) for x in get("https://fapi.binance.com/fapi/v1/premiumIndex")}
try:
itv = {x["symbol"]: float(x.get("fundingIntervalHours", 8)) for x in get("https://fapi.binance.com/fapi/v1/fundingInfo")}
except Exception:
itv = {}
vol = {x["symbol"]: float(x["quoteVolume"]) for x in get("https://fapi.binance.com/fapi/v1/ticker/24hr")}
return {base(s): (f * (24.0 / itv.get(s, 8)), vol.get(s, 0.0)) for s, f in fund.items() if s.endswith("USDT")}
def bybit():
r = get("https://api.bybit.com/v5/market/tickers?category=linear")["result"]["list"]
return {base(x["symbol"]): (float(x["fundingRate"]) * 3, float(x.get("turnover24h", 0.0)))
for x in r if x["symbol"].endswith("USDT") and x.get("fundingRate")}
def hyperliquid():
r = get("https://api.hyperliquid.xyz/info", post={"type": "metaAndAssetCtxs"})
meta, ctxs = r[0], r[1]
out = {}
for u, c in zip(meta["universe"], ctxs):
if c.get("funding") is not None:
out[u["name"]] = (float(c["funding"]) * 24, float(c.get("dayNtlVlm", 0.0)))
return out
def spreads():
V = {}
for nm, fn in [("Binance", binance), ("Bybit", bybit), ("HL", hyperliquid)]:
try:
V[nm] = fn()
except Exception as e:
print(f" ({nm} fetch failed: {str(e)[:40]})")
coins = set().union(*[set(v) for v in V.values()])
rows = []
for c in coins:
pts = {nm: V[nm][c] for nm in V if c in V[nm] and V[nm][c][1] > LIQ and abs(V[nm][c][0]) <= MAXF}
if len(pts) < 2:
continue
f = {nm: pts[nm][0] for nm in pts}
hi = max(f, key=f.get); lo = min(f, key=f.get)
rows.append({"coin": c, "spread": f[hi] - f[lo], "short": hi, "long": lo, "f": f})
rows.sort(key=lambda r: -r["spread"])
return rows, list(V)
def load_state():
if os.path.exists(STATE):
return json.load(open(STATE))
return {"positions": {}, "cum_gross": 0.0, "cum_net": 0.0, "days": 0, "last_run_date": ""}
def cmd_scan():
rows, venues = spreads()
print(f"cross-venue funding scan {datetime.date.today()} (venues: {', '.join(venues)}; liquid both legs >${LIQ/1e6:.0f}M)")
n_ok = sum(1 for r in rows if r["spread"] > HURDLE)
print(f" {len(rows)} coins on >=2 venues | {n_ok} with spread > {HURDLE*1e4:.0f}bp/day")
print(f" {'coin':>8} {'spread/day':>11} {'ann%':>7} short -> long (daily funding)")
for r in rows[:15]:
leg = " ".join(f"{k}{1e4*v:+.1f}" for k, v in sorted(r["f"].items(), key=lambda kv: -kv[1]))
print(f" {r['coin']:>8} {1e4*r['spread']:>9.1f}bp {100*r['spread']*365:>6.0f}% short {r['short']}->long {r['long']} [{leg}]")
def cmd_run():
st = load_state()
today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
if st.get("last_run_date") == today:
print(f"already booked {today} (day {st['days']}); skipping"); return
rows, _ = spreads()
cur = {r["coin"]: r["spread"] for r in rows}
prev = st["positions"]
realized = sum(w * cur.get(c, 0.0) for c, w in prev.items()) # carry on yesterday's book at today's spreads
# HYSTERESIS (the deployable, low-turnover version): hold winners until they decay, only enter strong fresh
held = [c for c in prev if cur.get(c, 0.0) > EXIT]
for r in sorted([r for r in rows if r["spread"] > ENTRY], key=lambda r: -r["spread"]):
if len(held) >= TOPK:
break
if r["coin"] not in held:
held.append(r["coin"])
qual = [r for r in rows if r["coin"] in held]
newpos = {c: 1.0 / len(held) for c in held} if held else {}
turn = sum(abs(newpos.get(c, 0) - prev.get(c, 0)) for c in set(newpos) | set(prev))
net = realized - turn * (COST_RT / 2)
st.update(positions=newpos, days=st["days"] + 1, last_run_date=today)
st["cum_gross"] += realized; st["cum_net"] += net
os.makedirs(os.path.dirname(STATE), exist_ok=True); json.dump(st, open(STATE, "w"))
top = " ".join(f"{r['coin']}:{1e4*r['spread']:.0f}bp({r['short'][:2]}>{r['long'][:2]})" for r in qual[:5])
print(f"{today} day={st['days']} | qualifying={len(qual)} | realized24h gross={100*realized:+.3f}% net={100*net:+.3f}% "
f"| cum net={100*st['cum_net']:+.2f}% | top: {top}")
def cmd_status():
st = load_state()
print(f"cross-venue funding paper — day {st['days']} (through {st.get('last_run_date') or 'n/a'}), "
f"{len(st['positions'])} pairs, cum gross {100*st['cum_gross']:+.2f}% net {100*st['cum_net']:+.2f}%")
for c, w in sorted(st["positions"].items(), key=lambda kv: -kv[1]):
print(f" {c:>8} w={w:.3f}")
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "scan"
{"scan": cmd_scan, "run": cmd_run, "paper": cmd_run, "status": cmd_status}.get(cmd, cmd_scan)()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Cascade reversion — do crypto coins BOUNCE after an extreme forced-flow down-move?
Liquidation cascades are forced deleveraging that overshoots; the harvestable consequence is the
mean-reversion bounce. Binance killed the historical liquidation feed (live websocket only), so we
test the PROXY already on disk: extreme negative trailing-H return = a cascade. Pre-registered:
formation: trailing-H log return per coin (cross-sectional)
signal: LONG the bottom-q fraction (biggest losers) — long-only on crashed names
hold: next H hours, NON-OVERLAPPING rebalance
cost: 5 bp / leg on actual weight turnover
two builds: raw_long (equal-weight losers, has crypto beta)
hedged (long losers short equal-weight UNIVERSE basket; beta-neutral, short leg
is the diversified basket NOT a single coin → dodges the short-melt-up ruin
that killed generic XS reversal)
KILL: net annualized SR < 0.5 OR not positive in most years (chrono) → cascade reversion closed.
LIMITATION: npz panel has close only (no volume) → cannot condition on the volume spike that
confirms a true liquidation cascade; the extreme-return tail is the proxy. Flagged, not hidden.
"""
import math
import os
import numpy as np
OUT = "data/surfer/crypto1h"
COST_BP = 5.0
SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "DOGEUSDT", "ADAUSDT", "AVAXUSDT",
"LINKUSDT", "LTCUSDT", "DOTUSDT", "TRXUSDT", "BCHUSDT", "ETCUSDT", "FILUSDT", "ATOMUSDT"]
HOUR_MS = 3_600_000
HOLDS = [1, 2, 4, 8, 12, 24] # formation == hold, hours
Q = 0.20 # bottom quintile = biggest losers
def build_panel():
series = {}
for s in SYMS:
cache = f"{OUT}/{s}.npz"
if not os.path.exists(cache):
continue
d = np.load(cache)
if len(d["ts"]) > 24 * 60:
series[s] = (d["ts"], d["close"])
allts = sorted(set().union(*[set((ts // HOUR_MS).tolist()) for ts, _ in series.values()]))
tindex = {t: i for i, t in enumerate(allts)}
syms = sorted(series)
P = np.full((len(allts), len(syms)), np.nan)
for j, s in enumerate(syms):
ts, c = series[s]
for t, px in zip(ts // HOUR_MS, c):
P[tindex[int(t)], j] = px
yrs = np.array([1970 + int(t) // int(365.25 * 24) for t in allts])
return P, syms, yrs
def ann(rets, per_year_factor):
rets = np.asarray(rets)
if len(rets) < 20 or rets.std() == 0:
return float("nan"), float("nan")
sr = rets.mean() / rets.std()
t = rets.mean() / (rets.std() / math.sqrt(len(rets)))
return sr * per_year_factor, t
def run():
P, syms, yrs = build_panel()
T, N = P.shape
logP = np.log(P)
print(f"\n===== CRYPTO CASCADE REVERSION (long the crashed names, net {COST_BP}bp/leg) =====")
print(f"panel: {T} hours x {N} coins years {int(yrs.min())}-{int(yrs.max())} q={Q} (bottom quintile)")
print("LONG bottom-q losers; hedged = long losers - short equal-wt universe basket\n")
hdr = f"{'hold':>5} {'periods':>8} {'build':>8} {'net_SR':>8} {'t(net)':>7} {'hit%':>6} {'per-year net-SR (chrono)':>30}"
print(hdr); print("-" * len(hdr))
for H in HOLDS:
pf = math.sqrt(24 * 365 / H)
idx = np.arange(H, T - H, H) # need t-H for formation, t+H for fwd
raw, hed, yr_raw, yr_hed = [], [], [], []
w_raw_prev = np.zeros(N); w_hed_prev = np.zeros(N)
for t in idx:
past = logP[t] - logP[t - H]
fwd = logP[t + H] - logP[t]
ok = np.isfinite(past) & np.isfinite(fwd)
n_ok = int(ok.sum())
if n_ok < 6:
continue
okidx = np.where(ok)[0]
k = max(1, int(round(Q * n_ok)))
losers = okidx[np.argsort(past[okidx])[:k]] # most-negative trailing return
# raw long
w_raw = np.zeros(N); w_raw[losers] = 1.0 / k
# hedged: long losers - short equal-weight universe basket
w_hed = np.zeros(N); w_hed[losers] += 1.0 / k; w_hed[okidx] -= 1.0 / n_ok
r_raw = float(np.nansum(w_raw * fwd)) - np.abs(w_raw - w_raw_prev).sum() * COST_BP / 1e4
r_hed = float(np.nansum(w_hed * fwd)) - np.abs(w_hed - w_hed_prev).sum() * COST_BP / 1e4
raw.append(r_raw); hed.append(r_hed)
yr_raw.append(int(yrs[t])); yr_hed.append(int(yrs[t]))
w_raw_prev = w_raw; w_hed_prev = w_hed
if len(raw) < 20:
print(f"{H:>5} (too few periods)"); continue
for name, series, yrl in (("raw_long", raw, yr_raw), ("hedged", hed, yr_hed)):
sr, t = ann(series, pf)
hit = 100.0 * np.mean(np.asarray(series) > 0)
py = {}
for r, y in zip(series, yrl):
py.setdefault(y, []).append(r)
pystr = " ".join(f"{y}:{(np.mean(v)/(np.std(v)+1e-12)):+.2f}"
for y, v in sorted(py.items()) if len(v) >= 10)
tag = f"{H}h" if name == "raw_long" else ""
print(f"{tag:>5} {len(series):>8} {name:>8} {sr:>+8.2f} {t:>+7.2f} {hit:>6.1f} {pystr}")
print("-" * len(hdr))
print("PASS: hedged net SR > 0.5, t>=2, positive in most years. Else cascade reversion closed.")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Delta-neutral crypto funding HARVEST (cash-and-carry) — the one retail path to higher Sharpe.
When perp funding > 0 (longs pay shorts), go short-perp + long-spot (DELTA-NEUTRAL: price cancels)
and collect the funding rate as carry. No price prediction. Causal: position[t] from funding[t-1]
(funding is persistent), collect funding[t]. Net of realistic round-trip cost. Stress: worst months
(crashes flip funding negative). Variants: broad vs majors, threshold, cost sensitivity.
Treats `fund` as per-day funding (conservative; if per-8h the real APR/Sharpe is higher).
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
def metrics(r):
r = np.asarray(r); r = r[np.isfinite(r)]
if len(r) < 50 or r.std() == 0:
return (float("nan"),) * 4
ann = r.mean() * 365; vol = r.std() * math.sqrt(365)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann, vol, ann / vol, dd
def harvest(fund, liq, thr, c_round, year, label, topk=None):
T, N = fund.shape
elig = liq & np.isfinite(fund)
sig = np.zeros((T, N))
sig[1:] = np.where(elig[1:] & (fund[:-1] > thr), 1.0, 0.0) # causal: position from yesterday's funding
if topk: # restrict to top-K best-funded eligible
for t in range(1, T):
idx = np.where(sig[t] > 0)[0]
if len(idx) > topk:
keep = idx[np.argsort(-fund[t - 1, idx])[:topk]]
m = np.zeros(N); m[keep] = 1.0; sig[t] = m
w = sig / np.maximum(sig.sum(1, keepdims=True), 1) # equal-weight positioned
f = np.nan_to_num(fund)
gross = np.sum(w[:-1] * f[1:], axis=1)
turn = np.sum(np.abs(w[1:] - w[:-1]), axis=1) # fraction of book traded
net = gross - turn * (c_round / 2) # one-way cost each side
ann, vol, sr, dd = metrics(net)
npos = sig.sum(1); avgpos = npos[npos > 0].mean()
print(f"{label:>26} {100*ann:>6.1f} {100*vol:>6.1f} {sr:>+7.2f} {100*dd:>+7.1f} {avgpos:>6.0f} {100*turn.mean():>6.1f}")
return net
def main():
syms, days, close, qv, fund = pit_sweep.load()
T, N = fund.shape
year = (1970 + days / 365.25).astype(int)
qv30 = np.full_like(qv, np.nan)
for t in range(30, T):
qv30[t] = np.nanmean(qv[t - 30:t], axis=0)
liq = np.isfinite(qv30) & (qv30 > 5e6) # >$5M/day quote volume
majors = np.array([s in ("BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT", "ADAUSDT",
"DOGEUSDT", "AVAXUSDT", "LINKUSDT", "MATICUSDT") for s in syms])
liq_maj = liq & majors[None, :]
print(f"\n===== CRYPTO FUNDING HARVEST (delta-neutral, {N} coins, {T} days) =====")
print(f"{'variant':>26} {'APR%':>6} {'vol%':>6} {'Sharpe':>7} {'maxDD%':>7} {'#pos':>6} {'turn%':>6}")
base = harvest(fund, liq, 0.0, 0.0010, year, "broad, thr=0, 10bp")
harvest(fund, liq, 0.0003, 0.0010, year, "broad, thr=3bp, 10bp")
harvest(fund, liq, 0.0, 0.0010, year, "broad top-20, 10bp", topk=20)
harvest(fund, liq_maj, 0.0, 0.0010, year, "majors only, 10bp")
print(" -- cost sensitivity (broad, thr=0) --")
harvest(fund, liq, 0.0, 0.0005, year, "broad, 5bp")
harvest(fund, liq, 0.0, 0.0020, year, "broad, 20bp")
harvest(fund, liq, 0.0, 0.0040, year, "broad, 40bp (harsh)")
# per-year + worst-month stress on the base case
print("\n per-year Sharpe (broad, thr=0, 10bp):")
print(" " + " ".join(f"{y}:{metrics(base[year[1:]==y])[2]:+.1f}" for y in range(2019, 2027) if (year[1:] == y).sum() > 60))
# monthly buckets
mo = (days[1:] / 30.4).astype(int)
worst = sorted(set(mo), key=lambda m: base[mo == m].sum())[:5]
print(" worst 5 ~monthly buckets (crash stress — does delta-neutral hold?):")
for m in worst:
seg = base[mo == m]
d0 = int(days[1:][mo == m][0])
import datetime
print(f" ~{datetime.date.fromordinal(d0+719163)}: {100*seg.sum():+.1f}% over {len(seg)}d")
print("\nVERDICT: if net Sharpe >> 1 AND survives crash months (delta-neutral so price-neutral) = the genuine")
print("retail higher-Sharpe edge. Watch: turnover cost (funding flips), and whether tail months go deeply negative.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Decay/liveness: is the crypto funding carry still ALIVE now, or competed to death?
The harvest was +12 Sharpe 2020-24 but negative 2022, 2025, 2026. Decisive question: is that
(a) STRUCTURAL COMPRESSION (funding yield shrinking year-over-year = arbitraged away = dead) or
(b) REGIME (funding flips negative in deleverage, recovers = dormant)? And does a regime filter
(only harvest reliably-positive funding, else sit in cash) rescue the recent period?
Diagnostics: (1) funding-yield decay table by year; (2) quarterly net-harvest liveness timeline;
(3) regime-filtered vs unfiltered in the recent window.
"""
import datetime
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
def sharpe(r):
r = r[np.isfinite(r)]
return float("nan") if len(r) < 20 or r.std() == 0 else r.mean() / r.std() * math.sqrt(365)
def harvest(fund, elig, hurdle, c_round, trail=None):
T, N = fund.shape
cond = fund > hurdle
if trail: # regime filter: trailing-mean funding > hurdle
tf = np.full_like(fund, np.nan)
for t in range(trail, T):
tf[t] = np.nanmean(fund[t - trail:t], axis=0)
cond = tf > hurdle
sig = np.zeros((T, N))
sig[1:] = np.where(elig[1:] & cond[:-1], 1.0, 0.0) # causal
w = sig / np.maximum(sig.sum(1, keepdims=True), 1)
f = np.nan_to_num(fund)
gross = np.sum(w[:-1] * f[1:], axis=1)
turn = np.sum(np.abs(w[1:] - w[:-1]), axis=1)
net = gross - turn * (c_round / 2)
inmkt = sig.sum(1)[1:] > 0 # days actually positioned
return net, inmkt
def main():
syms, days, close, qv, fund = pit_sweep.load()
T, N = fund.shape
year = (1970 + days / 365.25).astype(int)
qv30 = np.full_like(qv, np.nan)
for t in range(30, T):
qv30[t] = np.nanmean(qv[t - 30:t], axis=0)
elig = np.isfinite(qv30) & (qv30 > 5e6) & np.isfinite(fund)
print("\n===== (1) FUNDING-YIELD DECAY by year (compression vs regime) =====")
print(f"{'year':>6} {'#coins':>7} {'frac_pos':>9} {'mean_all(bp)':>13} {'mean_pos(bp)':>13}")
for y in range(2019, 2027):
m = year == y
e = elig[m]
fy = fund[m][e]
if len(fy) < 100:
continue
ncoin = int(e.sum(1).mean()) if e.ndim == 2 else int(e.mean() * N)
fpos = float((fy > 0).mean())
print(f"{y:>6} {ncoin:>7} {fpos:>9.2f} {1e4*fy.mean():>13.2f} {1e4*fy[fy > 0].mean():>13.2f}")
print("\n===== (2) QUARTERLY net-harvest liveness (broad, thr=0, 10bp) =====")
net, _ = harvest(fund, elig, 0.0, 0.0010)
q = ((days[1:] - days[1]) / 91.3).astype(int)
dd = days[1:]
print(f"{'quarter':>10} {'netAPR%':>8} {'Sharpe':>7}")
for qq in sorted(set(q)):
seg = net[q == qq]
d0 = datetime.date.fromordinal(int(dd[q == qq][0]) + 719163)
if d0.year >= 2024: # focus recent
print(f"{str(d0):>10} {100 * seg.sum() * (365 / max(len(seg), 1)) / 1:>8.1f} {sharpe(seg):>7.1f}")
print("\n===== (3) REGIME FILTER rescue test (recent: 2025-2026) =====")
recent = year[1:] >= 2025
variants = [("unfiltered thr=0 10bp", harvest(fund, elig, 0.0, 0.0010)[0]),
("filtered tf14>3bp 10bp", harvest(fund, elig, 0.0003, 0.0010, trail=14)[0]),
("filtered tf14>5bp 10bp", harvest(fund, elig, 0.0005, 0.0010, trail=14)[0]),
("filtered tf30>5bp 10bp", harvest(fund, elig, 0.0005, 0.0010, trail=30)[0])]
print(f"{'variant':>24} {'2025-26 APR%':>13} {'2025-26 Sharpe':>15} {'full Sharpe':>12}")
for nm, r in variants:
rr = r[recent]
apr = 100 * np.nansum(rr) * 365 / max(np.isfinite(rr).sum(), 1)
print(f"{nm:>24} {apr:>13.1f} {sharpe(rr):>15.1f} {sharpe(r):>12.1f}")
print("\nVERDICT: if mean_pos(bp) stable across years but frac_pos dipped 2025-26 = REGIME (dormant, recoverable).")
print("If mean_pos shrinks monotonically = COMPRESSION (competed to death). If a filter turns 2025-26 positive")
print("= alive-but-needs-regime-timing. If even filtered 2025-26 is negative/zero = the edge is GONE now.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Clean OOS validation of the funding-harvest regime filter.
Protocol (no peeking): choose the regime-filter params on IN-SAMPLE 2019-2024 by IS Sharpe, then
apply that EXACT filter BLIND to OUT-OF-SAMPLE 2025-2026. If the IS-best filter still works OOS,
the edge is validated, not curve-fit. Also reports the full grid's OOS so we can see if it's
robust across reasonable filters (real) or only the lucky one (suspicious). Strategy structure
fixed; only the regime filter (trailing window, hurdle) is the tuned degree of freedom.
Raw Sharpe is funding-only (vol understated ~2-3x); realistic ~ raw / 2.5.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
def sharpe(r):
r = r[np.isfinite(r)]
return float("nan") if len(r) < 20 or r.std() == 0 else r.mean() / r.std() * math.sqrt(365)
def apr(r):
r = r[np.isfinite(r)]
return 100 * r.sum() * 365 / max(len(r), 1)
def main():
syms, days, close, qv, fund = pit_sweep.load()
T, N = fund.shape
year = (1970 + days / 365.25).astype(int)
qv30 = np.full_like(qv, np.nan)
for t in range(30, T):
qv30[t] = np.nanmean(qv[t - 30:t], axis=0)
elig = np.isfinite(qv30) & (qv30 > 5e6) & np.isfinite(fund)
f = np.nan_to_num(fund)
# precompute trailing means
trailmean = {}
for tr in (14, 30):
tm = np.full_like(fund, np.nan)
for t in range(tr, T):
tm[t] = np.nanmean(fund[t - tr:t], axis=0)
trailmean[tr] = tm
def harvest(trail, hurdle, c=0.0010):
cond = (fund > hurdle) if trail == 0 else (trailmean[trail] > hurdle)
sig = np.zeros((T, N))
sig[1:] = np.where(elig[1:] & cond[:-1], 1.0, 0.0) # causal
w = sig / np.maximum(sig.sum(1, keepdims=True), 1)
gross = np.sum(w[:-1] * f[1:], axis=1)
turn = np.sum(np.abs(w[1:] - w[:-1]), axis=1)
net = gross - turn * (c / 2)
inmkt = (sig.sum(1)[1:] > 0)
return net, inmkt
yr = year[1:]
IS = yr <= 2024
OOS = yr >= 2025
grid = [(tr, h) for tr in (0, 14, 30) for h in (0.0, 0.0002, 0.0003, 0.0005)]
rows = []
for tr, h in grid:
net, inmkt = harvest(tr, h)
rows.append(dict(tr=tr, h=h, net=net, inmkt=inmkt,
is_sr=sharpe(net[IS]), oos_sr=sharpe(net[OOS]),
oos_apr=apr(net[OOS]), oos_inmkt=float(inmkt[OOS].mean())))
print(f"\n===== CLEAN OOS VALIDATION — IS 2019-2024 / OOS 2025-2026 =====")
print(f"{'filter':>18} {'IS Sharpe':>10} {'OOS Sharpe':>11} {'OOS APR%':>9} {'OOS in-mkt%':>11}")
for r in rows:
nm = "naive thr=0" if (r["tr"] == 0 and r["h"] == 0) else (f"inst>{r['h']*1e4:.0f}bp" if r["tr"] == 0 else f"tf{r['tr']}>{r['h']*1e4:.0f}bp")
print(f"{nm:>18} {r['is_sr']:>+10.1f} {r['oos_sr']:>+11.1f} {r['oos_apr']:>+9.1f} {100*r['oos_inmkt']:>10.0f}")
best = max(rows, key=lambda r: r["is_sr"])
bnm = f"tf{best['tr']}>{best['h']*1e4:.0f}bp" if best["tr"] else f"inst>{best['h']*1e4:.0f}bp"
print(f"\n IS-BEST filter (chosen blind to OOS): {bnm} -> IS Sharpe {best['is_sr']:+.1f}")
print(f" ITS OOS RESULT: Sharpe {best['oos_sr']:+.1f} APR {best['oos_apr']:+.1f}% in-market {100*best['oos_inmkt']:.0f}% of days")
print(f" (realistic Sharpe after basis vol ~ raw / 2.5 = {best['oos_sr']/2.5:+.1f})")
# robustness: how many filters with IS Sharpe>2 also have OOS Sharpe>1
good = [r for r in rows if r["is_sr"] > 2]
robust = [r for r in good if r["oos_sr"] > 1]
print(f"\n robustness: of {len(good)} filters with IS Sharpe>2, {len(robust)} also have OOS Sharpe>1")
print("\nVERDICT: IS-best filter OOS Sharpe>1 (realistic) + broad robustness = VALIDATED, deployable.")
print("If IS-best collapses OOS or only 1 filter works = curve-fit, not real.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""fundcli — local CLI for the crypto funding-harvest strategy (Phase-1 paper-forward).
No agents, no cloud. A self-contained local tool over Binance's public API (no key, no capital).
Delta-neutral funding harvest: hold long-spot/short-perp on CRYPTO-NATIVE coins whose trailing-30d
mean daily funding > 5bp; collect funding as market-neutral carry. Validated OOS (realistic
Sharpe ~2-3.6); this tool tracks the no-capital forward record.
python3 crypto_funding_paper.py snapshot live liveness check (qualifying coins; no state change)
python3 crypto_funding_paper.py run daily step: book funding on prior positions, log, persist
python3 crypto_funding_paper.py status current book + cumulative track record
python3 crypto_funding_paper.py gate Phase-1 -> Phase-2 assessment vs backtest
python3 crypto_funding_paper.py orders [USD] Phase-2: current book -> exact delta-neutral orders
python3 crypto_funding_paper.py log [N] last N run-log lines (default 25)
(alias: 'paper' == 'run', for the cron)
"""
import datetime
import json
import os
import sys
import time
import urllib.request
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
STATE = os.path.join(_REPO, "data/surfer/funding_paper_state.json")
LOG = os.path.join(_REPO, "data/surfer/funding_paper_runs.log")
FAPI = "https://fapi.binance.com"
HURDLE = 0.0005 # 5 bp/day trailing-30d mean funding (validated filter)
EXIT_HURDLE = 0.0003 # hysteresis: keep held coins until they fall below 3 bp/day
LIQ_USD = 5e6 # >$5M/day quote volume
COST_RT = 0.0010 # 10 bp round-trip (net-of-cost awareness)
INTERVALS_PER_DAY = 3 # Binance funding settles every 8h
BACKTEST_APR = (15, 22) # validated APR band on deployed capital
def get(url, tries=4):
for a in range(tries):
try:
req = urllib.request.Request(url, headers={"User-Agent": "curl/8"})
return json.loads(urllib.request.urlopen(req, timeout=30).read())
except Exception:
if a == tries - 1:
raise
time.sleep(2 * (a + 1))
def crypto_native():
info = get(f"{FAPI}/fapi/v1/exchangeInfo")
return {s["symbol"] for s in info["symbols"] if s.get("underlyingType") == "COIN"}
def spot_symbols():
"""Symbols with a Binance SPOT market — required to build the delta-neutral hedge.
Perp-only coins can't be cash-and-carry harvested (no spot leg); their high funding survives
PRECISELY because the arb is impossible, so they must be excluded."""
return {x["symbol"] for x in get("https://api.binance.com/api/v3/ticker/price")}
def universe():
"""Liquid, crypto-native, HEDGEABLE (spot+perp) USDT perps — the only ones that can be
delta-neutral harvested."""
native = crypto_native()
spot = spot_symbols()
t = get(f"{FAPI}/fapi/v1/ticker/24hr")
return {x["symbol"]: float(x["quoteVolume"]) for x in t
if x["symbol"].endswith("USDT") and x["symbol"] in native and x["symbol"] in spot
and float(x["quoteVolume"]) > LIQ_USD}
def funding_hist(sym, limit=90):
h = get(f"{FAPI}/fapi/v1/fundingRate?symbol={sym}&limit={limit}")
return [float(x["fundingRate"]) for x in h]
def scan(prev=None):
"""Fetch universe + funding; return (liq, tf30, last24) for union of universe and prev positions."""
liq = universe()
need = set(liq) | set(prev or {})
tf30, last24 = {}, {}
for i, sym in enumerate(sorted(need)):
try:
h = funding_hist(sym)
except Exception:
continue
if len(h) < 30:
continue
tf30[sym] = (sum(h) / len(h)) * INTERVALS_PER_DAY
last24[sym] = sum(h[-INTERVALS_PER_DAY:])
if i % 50 == 49:
time.sleep(0.5)
return liq, tf30, last24
def qualify(liq, tf30, prev):
q = {}
for c in liq:
t = tf30.get(c)
if t is not None and (t > HURDLE or (c in prev and t > EXIT_HURDLE)):
q[c] = t
return q
def regime(n):
return "ALIVE (healthy)" if n >= 15 else ("THIN / deleverage (mostly cash, by design)" if n >= 5 else "COMPRESSED (edge thin/decaying)")
def load_state():
if os.path.exists(STATE):
return json.load(open(STATE))
return {"positions": {}, "cum_gross": 0.0, "cum_net": 0.0, "days": 0}
# ---- commands ----
def cmd_snapshot():
liq, tf30, _ = scan()
q = qualify(liq, tf30, {})
top = sorted(q.items(), key=lambda kv: -kv[1])
med = sorted(q.values())[len(q) // 2] if q else 0.0
print(f"funding snapshot {datetime.date.today()} (crypto-native, live)")
print(f" liquid universe: {len(liq)} perps (>${LIQ_USD/1e6:.0f}M/day)")
print(f" qualifying (tf30 > {HURDLE*1e4:.0f}bp/day): {len(q)} regime: {regime(len(q))}")
print(f" median qualifying funding: {med*1e4:.1f} bp/day")
for c, t in top[:15]:
print(f" {c:>16} {t*1e4:5.1f} bp/day")
def cmd_run():
st = load_state()
today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
if st.get("last_run_date") == today: # idempotent: one booking per UTC day
print(f"already booked {today} (day {st['days']}); skipping")
return
prev = st["positions"]
liq, tf30, last24 = scan(prev)
realized = sum(w * last24.get(c, 0.0) for c, w in prev.items())
q = qualify(liq, tf30, prev)
n = len(q)
newpos = {c: 1.0 / n for c in q} if n else {}
turnover = sum(abs(newpos.get(c, 0) - prev.get(c, 0)) for c in set(newpos) | set(prev))
net = realized - turnover * (COST_RT / 2)
st.update(positions=newpos, days=st["days"] + 1, last_run_date=today)
st["cum_gross"] += realized
st["cum_net"] += net
os.makedirs(os.path.dirname(STATE), exist_ok=True)
json.dump(st, open(STATE, "w"))
top = " ".join(f"{c}:{t*1e4:.0f}bp" for c, t in sorted(q.items(), key=lambda kv: -kv[1])[:5])
line = (f"{datetime.date.today()} day={st['days']} | qualifying={n} liquid={len(liq)} | "
f"realized24h gross={100*realized:+.3f}% net={100*net:+.3f}% turn={turnover:.2f} | "
f"cum gross={100*st['cum_gross']:+.2f}% net={100*st['cum_net']:+.2f}% | top: {top}")
print(line)
def cmd_status():
st = load_state()
print(f"funding-harvest paper state: day {st['days']}, {len(st['positions'])} positions, "
f"cum gross {100*st['cum_gross']:+.2f}% cum net {100*st['cum_net']:+.2f}%")
for c, w in sorted(st["positions"].items(), key=lambda kv: -kv[1]):
print(f" {c:>16} w={w:.3f}")
def cmd_gate():
st = load_state()
d = st["days"]
if d == 0:
print("no data yet — run 'run' (or wait for cron) to start the track record."); return
apr = st["cum_net"] * 365 / d * 100
print(f"=== Phase-1 gate assessment (day {d}, {d/7:.1f} weeks) ===")
print(f" cumulative net: {100*st['cum_net']:+.2f}% annualized: {apr:+.1f}% APR")
print(f" backtest target band: {BACKTEST_APR[0]}-{BACKTEST_APR[1]}% APR (gross carry, deployed capital)")
if d < 28:
rec = f"KEEP ACCUMULATING — need >=4 weeks ({28-d} days to go) before the gate is meaningful."
elif apr >= 10:
rec = "PROCEED-candidate -> Phase 2 (micro-live $1-3k, ONE tier-1 venue, low leverage). Carry holding forward."
elif apr > 0:
rec = "BORDERLINE -> extend paper-forward to 8 weeks; carry positive but below backtest band (thin regime)."
else:
rec = "DO NOT GO LIVE -> carry flat/negative forward; extend or stop. No capital risked."
print(f" recommendation: {rec}")
print(" caveats: GROSS carry only (realistic Sharpe ~ raw/2.5 after basis vol); counterparty/exchange")
print(" tail is the real -100% risk (un-modeled); current qualifiers may be small/niche coins.")
def cmd_orders(capital):
"""Phase-2 bridge: turn the current paper book into exact delta-neutral orders for `capital`,
and flag coins that are perp-only (no spot leg = can't hedge cleanly on Binance)."""
st = load_state()
book = st.get("positions", {})
if not book:
print("no current book — run 'snapshot'/'run' first."); return
try:
spot_px = {x["symbol"]: float(x["price"]) for x in get("https://api.binance.com/api/v3/ticker/price")}
except Exception:
spot_px = {}
perp_px = {x["symbol"]: float(x["price"]) for x in get(f"{FAPI}/fapi/v1/ticker/price")}
LEV = 2.0 # conservative perp leverage (avoid liquidation)
n = len(book)
X = capital / (n * (1 + 1 / LEV)) # notional per leg per coin
print(f"=== Phase-2 delta-neutral orders for ${capital:.0f} across {n} coins (perp {LEV:.0f}x) ===")
print(f" per coin: ~${X:.0f} notional/leg, ~${X/LEV:.0f} perp margin, ~${X*(1+1/LEV):.0f} capital")
print(f"{'coin':>14} {'hedge':>6} {'SPOT buy (units @ px)':>26} {'PERP short (units @ px)':>26}")
ok = 0
for c in sorted(book):
if c in spot_px and c in perp_px:
print(f"{c:>14} {'YES':>6} {X/spot_px[c]:>14.4f} @ {spot_px[c]:<9.5g} {X/perp_px[c]:>14.4f} @ {perp_px[c]:<9.5g}")
ok += 1
else:
why = "no-spot" if c not in spot_px else "no-perp"
print(f"{c:>14} {'NO':>6} ({why}) cannot delta-neutral hedge on Binance — skip / alt-venue")
print(f"\n {ok}/{n} coins hedgeable on Binance (spot+perp both exist).")
print(" Place SPOT + PERP legs together to stay delta-neutral. Keep perp leverage low; never let the")
print(" perp leg liquidate while holding spot. API keys: ENV ONLY, never commit. Place manually for")
print(" micro-live to validate fills before any automation. DEPLOY ONLY AFTER the Phase-1 gate passes.")
def cmd_log(n=25):
if not os.path.exists(LOG):
print(f"(no log yet at {LOG} — cron writes it nightly; 'run' appends when redirected)"); return
lines = open(LOG).read().splitlines()
print("\n".join(lines[-n:]))
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "snapshot"
if cmd in ("run", "paper"):
cmd_run()
elif cmd == "snapshot":
cmd_snapshot()
elif cmd == "status":
cmd_status()
elif cmd == "gate":
cmd_gate()
elif cmd == "orders":
cmd_orders(float(sys.argv[2]) if len(sys.argv) > 2 else 2000.0)
elif cmd == "log":
cmd_log(int(sys.argv[2]) if len(sys.argv) > 2 else 25)
else:
print(__doc__)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""MFT crypto pulse — cross-sectional momentum at HOUR holds, net of turnover cost, hardened-lite.
The proven crypto edge is DAILY cross-sectional momentum (Sharpe ~1.4). micro_gate showed MINUTE-horizon
(per-coin) is dead. This tests the gap: does cross-sectional momentum work at HOUR holds (1h..48h)? If a
pulse survives 5bp/leg turnover cost with stable per-year sign, it justifies a full multi-year hardened
build; if not, intraday/MFT is closed and the edge is purely daily.
Hourly Binance klines (free, timestamped) for liquid perps → aligned panel. Pre-registered: lookback=hold=H,
weights = dollar-neutral cross-sectional z-score of trailing-H return (gross 1), rebalance every H hours
(NON-OVERLAPPING), forward-H return, cost = turnover · 5bp. Report gross/net annualized Sharpe + t + per-year.
"""
import json
import math
import os
import time
import urllib.request
import numpy as np
OUT = "data/surfer/crypto1h"
COST_BP = 5.0
HOLDS = [1, 2, 4, 8, 12, 24, 48]
SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "DOGEUSDT", "ADAUSDT", "AVAXUSDT",
"LINKUSDT", "LTCUSDT", "DOTUSDT", "TRXUSDT", "BCHUSDT", "ETCUSDT", "FILUSDT", "ATOMUSDT"]
HOUR_MS = 3_600_000
def _get(u):
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
def fetch(sym):
cache = f"{OUT}/{sym}.npz"
if os.path.exists(cache):
d = np.load(cache); return d["ts"], d["close"]
end = int(time.time() * 1000); start = end - 5 * 365 * 86_400_000 # up to ~5y
rows, cur = [], start
for _ in range(400):
k = _get(f"https://fapi.binance.com/fapi/v1/klines?symbol={sym}&interval=1h&startTime={cur}&limit=1500")
if not k:
break
rows += k
cur = k[-1][0] + 1
if len(k) < 1500 or cur >= end:
break
time.sleep(0.05)
if not rows:
return np.array([]), np.array([])
d = {int(r[0]): float(r[4]) for r in rows}
ts = np.array(sorted(d)); close = np.array([d[t] for t in ts])
os.makedirs(OUT, exist_ok=True)
np.savez(cache, ts=ts, close=close)
return ts, close
def build_panel():
series = {}
for s in SYMS:
ts, c = fetch(s)
if len(ts) > 24 * 60: # need >~2 months
series[s] = (ts, c)
# align on the union of hourly timestamps (floored to the hour)
allts = sorted(set().union(*[set((ts // HOUR_MS).tolist()) for ts, _ in series.values()]))
tindex = {t: i for i, t in enumerate(allts)}
syms = sorted(series)
P = np.full((len(allts), len(syms)), np.nan)
for j, s in enumerate(syms):
ts, c = series[s]
for t, px in zip(ts // HOUR_MS, c):
P[tindex[int(t)], j] = px
yrs = np.array([1970 + int(t) // (365.25 * 24) for t in allts])
return P, syms, yrs
def ann_sharpe(x):
x = x[np.isfinite(x)]
if len(x) < 20 or x.std() == 0:
return float("nan"), float("nan"), 0
return float(x.mean() / x.std()), x.mean() / (x.std() / math.sqrt(len(x))), len(x)
def run():
P, syms, yrs = build_panel()
T, N = P.shape
logP = np.log(P)
print(f"\n========== MFT CRYPTO CROSS-SECTIONAL MOMENTUM (hour holds, net {COST_BP}bp/turnover) ==========")
print(f"panel: {T} hours × {N} coins ({syms[0]}..{syms[-1]}) years {int(yrs.min())}-{int(yrs.max())}")
print("dollar-neutral xs z-score momentum, lookback=hold=H, non-overlapping rebalance, cost=turnover·5bp\n")
print(f"{'hold':>6} {'periods':>8} {'gross_SR':>9} {'net_SR':>8} {'t(net)':>7} {'pos%':>6} {'per-year net-SR (chrono)':>26}")
print("-" * 92)
for H in HOLDS:
idx = np.arange(0, T - H, H) # non-overlapping rebalance points
rets, yr_of = [], []
w_prev = np.zeros(N)
per_year = {}
for t in idx:
past = logP[t] - logP[t - H] if t - H >= 0 else np.full(N, np.nan)
fwd = logP[t + H] - logP[t]
ok = np.isfinite(past) & np.isfinite(fwd)
if ok.sum() < 6:
continue
z = np.zeros(N)
pv = past[ok]; zz = (pv - pv.mean()) / (pv.std() + 1e-12)
z[ok] = zz
g = np.abs(z).sum()
w = z / g if g > 0 else z # dollar-neutral, gross 1
turn = np.abs(w - w_prev).sum()
r = float(np.nansum(w * fwd)) - turn * COST_BP / 1e4
rets.append(r); yr_of.append(int(yrs[t]))
per_year.setdefault(int(yrs[t]), []).append(r)
w_prev = w
rets = np.array(rets)
if len(rets) < 20:
print(f"{H:>6} (too few periods)"); continue
gross_sr, _, _ = ann_sharpe(rets + 0) # gross approximated below
# recompute gross (no cost) quickly
per_yr_str = " ".join(f"{y}:{(np.mean(v)/ (np.std(v)+1e-12)):+.2f}" for y, v in sorted(per_year.items()) if len(v) >= 10)
per_period = math.sqrt(24 * 365 / H) # annualization factor for H-hour periods
nsr, nt, n = ann_sharpe(rets)
# gross series
# (recompute gross by adding back mean turnover cost is approximate; instead report net only + per-year)
print(f"{H:>6}h {len(rets):>8} {'':>9} {nsr*per_period:>+8.2f} {nt:>+7.2f} {100*np.mean(rets>0):>6.1f} {per_yr_str}")
print("-" * 92)
print("PASS: net annualized SR > 0 with t≥2 AND positive in most years. Else MFT-crypto closed → daily only.")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Stablecoin peg-reversion existence test — the cleanest crypto-FX dislocation.
Stablecoins are supposed to be worth $1; mint/redeem arbitrage (forced flow) pulls a deviation back
to parity. This tests whether that reversion is harvestable NET OF COST on free Binance spot data, and
— critically — how bad the DEATH-SPIRAL tail is (some "depegs" never revert: UST->0, BUSD wind-down).
Pre-registered: pair STABLE/USDT, deviation d=close-1. Reversion bet when |d|>thresh: position =
-sign(d) (rich->short, cheap->long), hold H days, pnl_net = -sign(d)*(close[t+H]/close[t]-1) - cost_rt.
Report per-pair + pooled: trade count, mean net pnl (bp), hit%, WORST trade (bp, the death-spiral tail),
and a daily-strategy Sharpe. Cost levels bracket Binance stablecoin fees {0,2,10}bp round-trip.
KILL: pooled net mean<=0 OR edge entirely from one terminal name OR worst-trade tail dwarfs mean edge.
LIMITATION: overlapping forward windows (first-look existence test, not a hardened backtest); daily
granularity (intraday depeg spikes under-sampled). Flagged, not hidden.
"""
import json
import math
import os
import time
import urllib.request
import numpy as np
OUT = "data/surfer/stablecoin"
PAIRS = ["USDCUSDT", "FDUSDUSDT", "TUSDUSDT", "DAIUSDT", "BUSDUSDT", "USDPUSDT"]
DAY_MS = 86_400_000
THRESHOLDS_BP = [10, 25, 50]
HOLDS = [1, 2, 5]
COSTS_BP = [0.0, 2.0, 10.0]
def _get(u):
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
def fetch(sym):
cache = f"{OUT}/{sym}.npz"
if os.path.exists(cache):
d = np.load(cache); return d["ts"], d["close"]
start = 1_546_300_800_000 # 2019-01-01
end = int(time.time() * 1000)
rows, cur = [], start
for _ in range(400):
k = _get(f"https://api.binance.com/api/v3/klines?symbol={sym}&interval=1d&startTime={cur}&limit=1000")
if not k:
break
rows += k
cur = k[-1][0] + DAY_MS
if len(k) < 1000 or cur >= end:
break
time.sleep(0.05)
if not rows:
return np.array([]), np.array([])
d = {int(r[0]): float(r[4]) for r in rows}
ts = np.array(sorted(d)); close = np.array([d[t] for t in ts])
os.makedirs(OUT, exist_ok=True)
np.savez(cache, ts=ts, close=close)
return ts, close
def reversion(close, thresh_bp, H, cost_bp):
"""Event reversion bet: enter when |d|>thresh, pnl=-sign(d)*(fwd_ret)-cost_rt. Returns net-pnl array (bp)."""
d = close - 1.0
th = thresh_bp / 1e4
pnls = []
for t in range(len(close) - H):
if abs(d[t]) > th and close[t] > 0:
fwd = close[t + H] / close[t] - 1.0
pnl = -np.sign(d[t]) * fwd - cost_bp / 1e4
pnls.append(pnl * 1e4) # in bp
return np.array(pnls)
def run():
series = {}
for s in PAIRS:
ts, c = fetch(s)
if len(ts) > 60:
series[s] = c
print("\n========== STABLECOIN PEG-REVERSION (crypto-FX dislocation, free Binance spot daily) ==========")
print(f"pairs: {', '.join(f'{s}({len(c)}d)' for s, c in series.items())}\n")
# 1) dislocation frequency per pair
print("--- dislocation magnitude (|close-1|) per pair ---")
print(f"{'pair':>9} {'days':>5} {'med_bp':>7} {'p95_bp':>7} {'max_bp':>8} {'>10bp%':>7} {'>50bp%':>7}")
for s, c in series.items():
d = np.abs(c - 1.0) * 1e4
print(f"{s:>9} {len(c):>5} {np.median(d):>7.1f} {np.percentile(d,95):>7.1f} {d.max():>8.0f} "
f"{100*np.mean(d>10):>6.1f}% {100*np.mean(d>50):>6.1f}%")
# 2) reversion edge, pooled across pairs, by (thresh, H, cost)
print("\n--- reversion edge (pooled all pairs); pnl in bp/trade, net of round-trip cost ---")
print(f"{'thr_bp':>6} {'H':>2} {'cost':>5} {'trades':>7} {'mean_bp':>8} {'hit%':>6} {'worst_bp':>9} {'daily_SR':>9}")
for th in THRESHOLDS_BP:
for H in HOLDS:
for cost in COSTS_BP:
allp = np.concatenate([reversion(c, th, H, cost) for c in series.values()]) if series else np.array([])
if len(allp) < 10:
continue
sr = (allp.mean() / allp.std() * math.sqrt(365 / H)) if allp.std() > 0 else float("nan")
print(f"{th:>6} {H:>2} {cost:>4.0f}b {len(allp):>7} {allp.mean():>+8.1f} "
f"{100*np.mean(allp>0):>5.1f}% {allp.min():>+9.0f} {sr:>+9.2f}")
print(" " + "-" * 60)
# 3) per-pair edge at a fixed mid setting (thr=25bp, H=2, cost=2bp) — is it one terminal name?
print("\n--- per-pair edge @ thr=25bp H=2 cost=2bp (is the edge concentrated/terminal?) ---")
print(f"{'pair':>9} {'trades':>7} {'mean_bp':>8} {'hit%':>6} {'worst_bp':>9}")
for s, c in series.items():
p = reversion(c, 25, 2, 2.0)
if len(p) >= 5:
print(f"{s:>9} {len(p):>7} {p.mean():>+8.1f} {100*np.mean(p>0):>5.1f}% {p.min():>+9.0f}")
else:
print(f"{s:>9} {len(p):>7} (too few events)")
print("\nKILL if: pooled mean<=0, OR edge is one terminal name, OR |worst_bp| dwarfs mean (death-spiral tail).")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Stablecoin peg-reversion — HARDENED. Make-or-break test of the crypto-FX dislocation edge.
The spike (crypto_stablecoin_dislocation.py) found a real reversion edge (SR 3-4) but was blind to:
(1) survivorship — all sampled stables reverted; the true tail is buying a cheap stable that goes to
ZERO (UST -75%->delist). (2) overlap inflation. (3) stress slippage. (4) long/short asymmetry.
This hardens all four:
* UST included (the real death-spiral). FRAX excluded (garbage prints). USTC excluded (post-death).
* NON-OVERLAPPING event walk with explicit exit (revert / max_hold / stop-loss).
* deviation-SCALED slippage: cost_rt = base_bp + slip_k*|deviation| (big depeg = thin book).
* LONG-cheap (unbounded -100% tail) vs SHORT-rich (bounded) decomposed separately.
* collapse filters compared: raw / stop-loss / skip-falling-knife(+stop).
* per-year sign + BTC-stress correlation proxy (does it lose when crypto crashes?).
KILL: long-side net<=0 after UST+filters, OR edge only pre-cost, OR loses concentrated in BTC crashes.
"""
import math
import os
import numpy as np
OUT = "data/surfer/stablecoin"
HEALTHY = ["USDCUSDT", "FDUSDUSDT", "TUSDUSDT", "BUSDUSDT", "USDPUSDT", "SUSDUSDT", "DAIUSDT"]
TERMINAL = ["USTUSDT"]
DAY_MS = 86_400_000
THRESH_BP = 50
EXIT_BP = 10
MAX_HOLD = 5
BASE_BP = 2.0
SLIP_K = 0.15 # slippage = 15% of the entry deviation (stress thinness)
STOP = 0.03 # 3% stop-loss
def fetch(sym):
import json, time, urllib.request
cache = f"{OUT}/{sym}.npz"
if os.path.exists(cache):
d = np.load(cache); return d["ts"], d["close"]
g = lambda u: json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
start, end = 1_546_300_800_000, int(time.time() * 1000)
rows, cur = [], start
for _ in range(400):
k = g(f"https://api.binance.com/api/v3/klines?symbol={sym}&interval=1d&startTime={cur}&limit=1000")
if not k:
break
rows += k; cur = k[-1][0] + DAY_MS
if len(k) < 1000 or cur >= end:
break
time.sleep(0.05)
if not rows:
return np.array([]), np.array([])
d = {int(r[0]): float(r[4]) for r in rows}
ts = np.array(sorted(d)); close = np.array([d[t] for t in ts])
os.makedirs(OUT, exist_ok=True); np.savez(cache, ts=ts, close=close)
return ts, close
def walk(ts, close, mode):
"""Non-overlapping reversion trades. mode: 'raw'|'stop'|'skip'. Returns list of (dir, year, net_bp, entry_dev_bp)."""
n = len(close); d = close - 1.0; th = THRESH_BP / 1e4; ex = EXIT_BP / 1e4
trades = []; i = 1
while i < n - 1:
if abs(d[i]) <= th:
i += 1; continue
direction = -1.0 if d[i] > 0 else 1.0 # rich->short(-1), cheap->long(+1)
# skip-falling-knife: only buy a cheap stable if it is NOT still dropping (bounce confirmation)
if mode == "skip" and direction > 0 and close[i] < close[i - 1]:
i += 1; continue
entry = close[i]; j = i + 1; stop = STOP if mode in ("stop", "skip") else 9.9
while j < n and (j - i) <= MAX_HOLD:
pnl = direction * (close[j] / entry - 1.0)
if pnl <= -stop or abs(close[j] - 1.0) < ex:
break
j += 1
j = min(j, n - 1)
gross = direction * (close[j] / entry - 1.0)
slip = (BASE_BP + SLIP_K * abs(d[i]) * 1e4) / 1e4
yr = 1970 + int(ts[i]) // (365 * DAY_MS)
trades.append((direction, yr, (gross - slip) * 1e4, abs(d[i]) * 1e4))
i = j + 1
return trades
def stats(pnls):
p = np.array(pnls)
if len(p) < 3:
return f"n={len(p)} (too few)"
sr = p.mean() / p.std() * math.sqrt(365 / ((MAX_HOLD + 1) / 2)) if p.std() > 0 else float("nan")
return f"n={len(p):>4} mean={p.mean():>+7.1f}bp hit={100*np.mean(p>0):>4.1f}% worst={p.min():>+7.0f}bp SR~{sr:>+5.2f}"
def run():
data = {}
for s in HEALTHY + TERMINAL:
ts, c = fetch(s)
if len(ts) > 30:
data[s] = (ts, c)
print("\n===== STABLECOIN PEG-REVERSION — HARDENED (non-overlap, UST-in, slip-scaled, long/short split) =====")
print(f"pairs: {', '.join(data)} thr={THRESH_BP}bp hold<={MAX_HOLD}d exit<{EXIT_BP}bp slip={BASE_BP}+{SLIP_K}*|dev| stop={STOP*100:.0f}%\n")
for mode in ("raw", "stop", "skip"):
allt = []
for s in data:
allt += [(s, *t) for t in walk(*data[s], mode)]
longs = [t[3] for t in allt if t[1] > 0]
shorts = [t[3] for t in allt if t[1] < 0]
ust = [t[3] for t in allt if t[0] == "USTUSDT"]
print(f"[{mode:>4}] ALL : {stats([t[3] for t in allt])}")
print(f" LONG(cheap, -100% tail): {stats(longs)}")
print(f" SHORT(rich, bounded) : {stats(shorts)}")
print(f" UST trades only : {stats(ust)}")
print()
# per-year (SHORT-only, skip mode = the candidate deployable core)
print("--- per-year SHORT-rich net mean bp (the bounded-risk core), skip mode ---")
by_yr = {}
for s in data:
for t in walk(*data[s], "skip"):
if t[0] < 0:
by_yr.setdefault(t[1], []).append(t[2])
print(" ".join(f"{y}:{np.mean(v):+.0f}({len(v)})" for y, v in sorted(by_yr.items()) if v))
# BTC-stress proxy: does the LONG side lose during BTC crashes?
bts, btc = fetch("BTCUSDT")
if len(btc) > 100:
bret = {int(bts[k]) // DAY_MS: (btc[k] / btc[k - 1] - 1.0) for k in range(1, len(btc))}
crash, calm = [], []
for s in data:
ts, c = data[s]
for dirn, yr, net, dev in walk(ts, c, "skip"):
pass
# simpler: tag each long trade entry day with BTC 1d return
for s in data:
ts, c = data[s]; d = c - 1.0; th = THRESH_BP / 1e4
for i in range(1, len(c) - 1):
if abs(d[i]) > th and d[i] < 0: # long-cheap entries
br = bret.get(int(ts[i]) // DAY_MS, 0.0)
(crash if br < -0.05 else calm).append(1)
print(f"\nLONG-cheap entries on BTC-crash days (>-5%): {len(crash)} vs calm: {len(calm)} "
f"({100*len(crash)/max(1,len(crash)+len(calm)):.0f}% in crashes = tail-correlation flag)")
print("\nREAD: deployable core = SHORT-rich (bounded). LONG-cheap viable only if filters tame the UST tail.")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Stablecoin peg-reversion — INTRADAY EXECUTION validation (the make-or-break-on-fills test).
Daily test found short-rich reversion ~+25bp/SR4. This checks it survives REALISTIC intraday execution:
* 1h bars, REALISTIC fills: enter at NEXT bar's OPEN (reaction lag — you can't transact at the peak),
exit at the reverting bar's close; pay round-trip spread (calibrated from live book: ~1bp/side).
* deviation HALF-LIFE: of rich events at hour t, what % still rich at t+1/+2/+4/+8 (do you have time?).
* short-rich (validated, bounded) focus; long-cheap with skip-falling-knife for contrast.
Universe = live-tradeable liquid stables only (USDC/FDUSD/TUSD/USDP; DAI/BUSD books are empty).
KILL: edge gone after next-open fills + spread, OR deviations vanish within 1h (no reaction window).
"""
import math
import os
import numpy as np
OUT = "data/surfer/stable1h"
PAIRS = ["USDCUSDT", "FDUSDUSDT", "TUSDUSDT", "USDPUSDT"]
HOUR_MS = 3_600_000
THRESHES = [25, 50]
EXIT_BP = 10
MAX_HOLD_H = 48
SPREAD_BP = 1.0 # per side, from live book; round-trip = 2*SPREAD_BP
def fetch(sym):
import json, time, urllib.request
cache = f"{OUT}/{sym}.npz"
if os.path.exists(cache):
d = np.load(cache); return d["ts"], d["open"], d["close"]
g = lambda u: json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
start, end = int(time.time() * 1000) - 3 * 365 * 86_400_000, int(time.time() * 1000)
rows, cur = [], start
for _ in range(800):
k = g(f"https://api.binance.com/api/v3/klines?symbol={sym}&interval=1h&startTime={cur}&limit=1000")
if not k:
break
rows += k; cur = k[-1][0] + HOUR_MS
if len(k) < 1000 or cur >= end:
break
time.sleep(0.04)
if not rows:
return np.array([]), np.array([]), np.array([])
o = {int(r[0]): (float(r[1]), float(r[4])) for r in rows}
ts = np.array(sorted(o)); op = np.array([o[t][0] for t in ts]); cl = np.array([o[t][1] for t in ts])
os.makedirs(OUT, exist_ok=True); np.savez(cache, ts=ts, open=op, close=cl)
return ts, op, cl
def walk(op, cl, thresh, side, skip=False):
"""Non-overlapping. side='short'(rich) or 'long'(cheap). enter next-bar OPEN, exit revert close, pay spread."""
n = len(cl); d = cl - 1.0; th = thresh / 1e4; ex = EXIT_BP / 1e4; rt = 2 * SPREAD_BP / 1e4
out = []; i = 1
while i < n - 2:
rich = d[i] > th; cheap = d[i] < -th
take = (side == "short" and rich) or (side == "long" and cheap)
if not take:
i += 1; continue
if skip and side == "long" and cl[i] < cl[i - 1]: # don't catch a still-falling knife
i += 1; continue
direction = -1.0 if rich else 1.0
entry = op[i + 1] # realistic: fill at next bar open
j = i + 2
while j < n and (j - (i + 1)) <= MAX_HOLD_H:
if abs(cl[j] - 1.0) < ex:
break
j += 1
j = min(j, n - 1)
gross = direction * (cl[j] / entry - 1.0)
out.append((gross - rt) * 1e4)
i = j + 1
return np.array(out)
def half_life(cl, thresh):
d = cl - 1.0; th = thresh / 1e4
ev = np.where(np.abs(d) > th)[0]; ev = ev[ev < len(cl) - 9]
if len(ev) == 0:
return None
return {h: 100 * np.mean(np.abs(d[ev + h]) > th) for h in (1, 2, 4, 8)}
def st(p):
if len(p) < 3:
return f"n={len(p)} (few)"
sr = p.mean() / p.std() * math.sqrt(365 * 24 / (MAX_HOLD_H / 2)) if p.std() > 0 else float("nan")
return f"n={len(p):>4} mean={p.mean():>+7.1f}bp hit={100*np.mean(p>0):>4.1f}% worst={p.min():>+7.0f}bp SR~{sr:>+5.2f}"
def run():
data = {}
for s in PAIRS:
ts, op, cl = fetch(s)
if len(ts) > 500:
data[s] = (op, cl, ts)
print("\n===== STABLECOIN INTRADAY EXECUTION (1h, next-open fills, spread 2bp rt) =====")
print(f"pairs: {', '.join(f'{s}({len(v[1])}h)' for s,v in data.items())}\n")
print("--- deviation HALF-LIFE: % of events still beyond threshold after H hours (reaction window) ---")
for s, (op, cl, ts) in data.items():
for th in THRESHES:
hl = half_life(cl, th)
if hl:
print(f" {s:>9} thr{th}: t+1={hl[1]:.0f}% t+2={hl[2]:.0f}% t+4={hl[4]:.0f}% t+8={hl[8]:.0f}%")
print()
for th in THRESHES:
sh = np.concatenate([walk(d[0], d[1], th, "short") for d in data.values()]) if data else np.array([])
lo = np.concatenate([walk(d[0], d[1], th, "long", skip=True) for d in data.values()]) if data else np.array([])
print(f"thr={th}bp SHORT-rich : {st(sh)}")
print(f" LONG-cheap*: {st(lo)} (*skip-falling-knife)")
# per-pair short
for s, (op, cl, ts) in data.items():
print(f" {s:>9} short: {st(walk(op, cl, th, 'short'))}")
print()
print("READ: edge survives if SHORT-rich stays +ve net of next-open+spread AND half-life>~2h (time to act).")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Deflated signal sweep — Batch 2 (crypto perps): broaden + robustness-check.
Cross-sectional, market-neutral, unit-gross signals on major Binance USDT perps. Funding
baked into the return (Reff = log-return daily funding; long pays positive funding). ~10bp
taker cost on turnover. Deflated Sharpe deflated by the CUMULATIVE search (Batch1 17 + these).
For any DEVELOP-grade signal (CPCVmed>0 & IS>0 & OOS>0): also report 2× cost + per-year Sharpe
(regime robustness). Survivorship caveat: v1 = currently-liquid majors over history.
"""
import glob
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
PRIOR_TRIALS = 17 # Batch 1 (futures factor zoo)
DEV = "cuda" if torch.cuda.is_available() else "cpu"
MIN_FUNDCOV = float(os.environ.get("MIN_FUNDCOV", "0.0")) # filter coins by funding coverage
MIN_DAYS = int(os.environ.get("MIN_DAYS", "0")) # ...and minimum history
def load_crypto():
syms, data = [], {}
for p in sorted(glob.glob("data/surfer/crypto/*.npz")):
d = np.load(p); s = p.split("/")[-1][:-4]
fund = d["funding"].astype(float)
if len(d["day"]) < MIN_DAYS or np.mean(fund != 0) < MIN_FUNDCOV:
continue
data[s] = (d["day"].astype(np.int64), d["close"].astype(float), fund)
syms.append(s)
syms = sorted(syms)
days = np.array(sorted(set().union(*[set(data[s][0].tolist()) for s in syms])))
di = {d: i for i, d in enumerate(days.tolist())}
T, N = len(days), len(syms)
close = np.full((T, N), np.nan); fund = np.full((T, N), np.nan)
for j, s in enumerate(syms):
dd, cc, ff = data[s]
for k in range(len(dd)):
r = di[int(dd[k])]; close[r, j] = cc[k]; fund[r, j] = ff[k]
return syms, days, close, fund
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def zc(x):
mu = np.nanmean(x, axis=1, keepdims=True); sd = np.nanstd(x, axis=1, keepdims=True)
return np.nan_to_num((x - mu) / np.where(sd > 0, sd, 1))
def main():
syms, days, close, fund = load_crypto()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
Reff = R - fund
vol30 = np.full_like(lc, np.nan)
for t in range(30, T):
vol30[t] = np.nanstd(R[t - 30:t], axis=0)
def tmf(K): # trailing mean funding over K days
out = np.full_like(fund, np.nan)
for t in range(K, T):
out[t] = np.nanmean(fund[t - K:t], axis=0)
return out
f7, f3, f14 = tmf(7), tmf(3), tmf(14)
W = {} # name -> weights[T,N]
for K in [3, 7, 14, 30]:
W[f"XS_carry_{K}"] = xs_weights(-tmf(K))
for L in [7, 30, 90]:
W[f"XS_mom_{L}"] = xs_weights(trailing(lc, L))
W["XS_rev_3"] = xs_weights(-trailing(lc, 3))
W["XS_fundmom"] = xs_weights(f3 - f14) # rising funding (positioning building)
W["XS_lowvol"] = xs_weights(-vol30)
W["XS_carry+mom30"] = xs_weights(zc(-f7) + zc(trailing(lc, 30)))
W["XS_carry+rev3"] = xs_weights(zc(-f7) + zc(-trailing(lc, 3)))
W["XS_carry+lowvol"] = xs_weights(zc(-f7) + zc(-vol30))
NT = PRIOR_TRIALS + len(W)
year = (1970 + days / 365.25).astype(int)
rows = []
pnls = {}
for nm, w in W.items():
pnl = pnl_w(w, Reff, cost_bp=10)
pnls[nm] = (w, pnl)
rows.append((nm, validate(pnl, days, NT)))
rows.sort(key=lambda r: -(r[1]["oos"] if not math.isnan(r[1]["oos"]) else -9))
print(f"\n===== CRYPTO SWEEP (broadened) — {N} perps, {T}d ({days.min()}..{days.max()}), fundcov={np.mean(fund!=0):.2f} =====")
print(f"N_trials(cumulative deflation) = {NT}")
print(f"{'signal':>16} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'5%':>6} {'DSR':>5}")
for nm, v in rows:
print(f"{nm:>16} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['p5']:>+6.2f} {v['dsr']:>5.2f}")
print("\n----- ROBUSTNESS for develop-grade signals (CPCVmed>0 & IS>0 & OOS>0) -----")
yrs = sorted(set(year[1:].tolist()))
print(f"{'signal':>16} {'full10bp':>9} {'full20bp':>9} | per-year Sharpe: " + " ".join(f"{y}" for y in yrs))
for nm, v in rows:
if v["med"] > 0 and v["is_"] > 0 and v["oos"] > 0:
w, pnl = pnls[nm]
pnl2 = pnl_w(w, Reff, cost_bp=20)
s10 = sharpe_t(torch.tensor(pnl, device=DEV, dtype=torch.float64))
s20 = sharpe_t(torch.tensor(pnl2, device=DEV, dtype=torch.float64))
py = []
for y in yrs:
m = year[1:] == y
py.append(sharpe_t(torch.tensor(pnl[m], device=DEV, dtype=torch.float64)) if m.sum() > 30 else float("nan"))
print(f"{nm:>16} {s10:>+9.2f} {s20:>+9.2f} | " + " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py))
surv = [nm for nm, v in rows if v["dsr"] > 0.95 and v["oos"] > 0 and v["med"] > 0]
print(f"\nDEPLOY survivors (DSR>0.95 & OOS>0 & CPCVmed>0): {surv if surv else 'NONE'}")
print("Develop-grade = robust real edge worth building; deploy-grade = DSR>0.95 (harsh, deflated by all trials).")
print("Caveat: survivorship (current majors); confirm point-in-time next.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Crypto TS-trend sizing — reconfirm the validated return engine + find the deployable CAGR.
The TSMOM crypto sleeve is VALIDATED (memory: Sharpe ~1.23, CAGR 75.7% @ 61% vol, corr~0 to funding).
The 76% CAGR is just 61%-vol sizing, not a free lunch. This re-runs the SAME pre-registered config
(LONG/FLAT, lookbacks 20/60/120, on crypto_pit daily close, liquidity floor on qvol) and sweeps a
vol-target overlay to answer the #2 question: at a SANE vol target, what is the deployable CAGR and
does the Sharpe hold (it should — vol-target is a rescale, but per-period sizing adds timing).
NOT a search: the 20/60/120 long/flat config is replicated verbatim from the validated harness.
"""
import glob
import math
import numpy as np
PIT = "data/surfer/crypto_pit"
LOOKBACKS = [20, 60, 120]
COST_BP = 5.0
ADV_FLOOR = 5e6 # $5M trailing quote-volume liquidity floor
VOL_TARGETS = [0.10, 0.15, 0.20]
LEV_CAP = 3.0
PPY = 365 # crypto trades every day
def build():
days = set()
raw = {}
for f in sorted(glob.glob(f"{PIT}/*.npz")):
d = np.load(f)
if len(d["day"]) < 130:
continue
sym = f.split("/")[-1][:-4]
raw[sym] = {int(dd): (c, q) for dd, c, q in zip(d["day"], d["close"], d["qvol"])}
days.update(int(x) for x in d["day"])
days = sorted(days)
di = {d: i for i, d in enumerate(days)}
syms = sorted(raw)
C = np.full((len(days), len(syms)), np.nan)
V = np.full((len(days), len(syms)), np.nan)
for j, s in enumerate(syms):
for dd, (c, q) in raw[s].items():
C[di[dd], j] = c; V[di[dd], j] = q
return np.array(days), syms, C, V
def maxdd(equity):
peak = np.maximum.accumulate(equity)
return float((equity / peak - 1).min())
def run():
days, syms, C, V = build()
T, N = C.shape
logC = np.log(C)
ret = np.full((T, N), np.nan)
ret[1:] = C[1:] / C[:-1] - 1.0
# long/flat signal = mean over lookbacks of 1[trailing-L log return > 0]
sig = np.zeros((T, N))
cnt = np.zeros((T, N))
for L in LOOKBACKS:
tr = np.full((T, N), np.nan)
tr[L:] = logC[L:] - logC[:-L]
on = np.where(np.isfinite(tr), (tr > 0).astype(float), np.nan)
m = np.isfinite(on)
sig[m] += on[m]; cnt[m] += 1
sig = np.where(cnt > 0, sig / np.maximum(cnt, 1), 0.0) # 0..1 long/flat conviction
print(f"\n===== CRYPTO TS-TREND SIZING (long/flat {LOOKBACKS}, ${ADV_FLOOR/1e6:.0f}M ADV floor, {COST_BP}bp/leg) =====")
print(f"panel: {T} days x {N} coins (survivorship-free incl. dead)\n")
# base book (gross 1, daily rebalance, equal-weight by conviction among liquid+on coins)
w_prev = np.zeros(N)
book = np.zeros(T)
for t in range(1, T):
eligible = np.isfinite(ret[t]) & np.isfinite(V[t - 1]) & (V[t - 1] > ADV_FLOOR)
s = sig[t - 1] * eligible
g = s.sum()
w = s / g if g > 0 else np.zeros(N)
turn = np.abs(w - w_prev).sum()
book[t] = float(np.nansum(w * ret[t])) - turn * COST_BP / 1e4
w_prev = w
valid = np.arange(T) > max(LOOKBACKS)
base = book[valid]
base_sr = base.mean() / base.std() * math.sqrt(PPY) if base.std() > 0 else float("nan")
base_vol = base.std() * math.sqrt(PPY)
base_cagr = float(np.prod(1 + base) ** (PPY / len(base)) - 1)
print(f"BASE (un-vol-targeted): Sharpe {base_sr:+.2f} vol {base_vol*100:.0f}% CAGR {base_cagr*100:+.1f}% maxDD {maxdd(np.cumprod(1+base))*100:.1f}%")
print(f" (memory reference: Sharpe ~1.23, CAGR ~76% @ ~61% vol — replicating)\n")
print(f"{'vol_tgt':>8} {'Sharpe':>7} {'CAGR':>7} {'real_vol':>9} {'maxDD':>7} {'avg_lev':>8} {'per-year SR':>30}")
print("-" * 86)
# vol-target overlay on the base book (trailing 30d realized, lagged, leverage-capped)
rv = np.full(T, np.nan)
for t in range(31, T):
w = book[t - 30:t]
rv[t] = w.std() * math.sqrt(PPY)
yrs = 1970 + days // 365
for vt in VOL_TARGETS:
lev = np.where(np.isfinite(rv) & (rv > 0), np.clip(vt / rv, 0, LEV_CAP), 0.0)
tgt = book * np.concatenate([[0], lev[:-1]]) # lag leverage by 1 day
s = tgt[valid]
sr = s.mean() / s.std() * math.sqrt(PPY) if s.std() > 0 else float("nan")
vol = s.std() * math.sqrt(PPY)
cagr = float(np.prod(1 + s) ** (PPY / len(s)) - 1)
dd = maxdd(np.cumprod(1 + s))
avglev = float(np.mean(lev[valid]))
yv = {}
for r, y in zip(s, yrs[valid]):
yv.setdefault(int(y), []).append(r)
ystr = " ".join(f"{y}:{(np.mean(v)/(np.std(v)+1e-12)*math.sqrt(PPY)):+.1f}"
for y, v in sorted(yv.items()) if len(v) >= 30)
print(f"{vt*100:>6.0f}% {sr:>+7.2f} {cagr*100:>+6.1f}% {vol*100:>8.0f}% {dd*100:>+6.1f}% {avglev:>8.2f} {ystr}")
print("-" * 86)
print("Deployable read: pick the vol target whose maxDD you can stomach; Sharpe should ~hold across targets.")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""First energy gate: is battery arbitrage a real, persistent, engine-suited edge?
Free DE-LU day-ahead hourly prices (Fraunhofer Energy-Charts, no key). For a 4h/1MW battery
(1 cycle/day, 85% round-trip): daily arbitrage value under PERFECT FORESIGHT (upper bound) vs a
NAIVE fixed-hours heuristic (charge night / discharge evening). The gap = the value forecasting/RL
could add. Annualize vs ~e50-80k/yr/MW needed to justify a battery. Trend = is it being competed away?
"""
import datetime
import json
import math
import os
import time
import urllib.request
import numpy as np
CACHE = "data/surfer/energy"
ETA = 0.85 # round-trip efficiency
HRS = 4 # 4h battery, 1 MW -> 4 MWh / cycle
ed = math.sqrt(ETA)
def get(u):
for attempt in range(6):
try:
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=60))
except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(8 * (attempt + 1)); continue
raise
raise RuntimeError("rate-limited after retries")
def fetch():
os.makedirs(CACHE, exist_ok=True)
ts, px = [], []
for yr in range(2019, 2026):
f = f"{CACHE}/de_{yr}.json"
if os.path.exists(f):
d = json.load(open(f))
else:
d = get(f"https://api.energy-charts.info/price?bzn=DE-LU&start={yr}-01-01&end={yr}-12-31")
json.dump({"unix_seconds": d.get("unix_seconds", []), "price": d.get("price", [])}, open(f, "w"))
time.sleep(5) # space requests (avoid 429)
ts += d["unix_seconds"]; px += d["price"]
return np.array(ts), np.array(px, dtype=float)
def main():
ts, px = fetch()
ok = np.isfinite(px)
ts, px = ts[ok], px[ok]
day = np.array([datetime.datetime.utcfromtimestamp(int(t)).date().toordinal() for t in ts])
udays = np.unique(day)
rows, dord = [], []
for d in udays:
h = px[day == d]
if len(h) == 24: # drop DST-transition days (23/25h)
rows.append(h); dord.append(d)
P = np.array(rows); D = np.array(dord) # [days, 24]
yr = np.array([datetime.date.fromordinal(int(x)).year for x in D])
print(f"DE day-ahead: {len(P)} clean days ({datetime.date.fromordinal(int(D[0]))}..{datetime.date.fromordinal(int(D[-1]))})")
# perfect-foresight 4h arbitrage per day (1 MW): discharge top-4 hrs, charge bottom-4 hrs
srt = np.sort(P, axis=1)
fore = srt[:, -HRS:].sum(1) * ed - srt[:, :HRS].sum(1) / ed # EUR/day per MW
# naive fixed-hours heuristic: charge 02-05h, discharge 18-21h (typical shape)
naive = P[:, 18:22].sum(1) * ed - P[:, 2:6].sum(1) / ed
spread = P.max(1) - P.min(1)
def stats(name, v):
ann = np.mean(v) * 365
print(f" {name:>16}: e{np.mean(v):6.1f}/day/MW -> e{ann/1000:6.1f}k/yr/MW (median e{np.median(v):.1f}/day)")
print(f"\n===== BATTERY ARBITRAGE ({HRS}h/1MW, {int(ETA*100)}% round-trip) =====")
print(f"daily price spread: mean e{spread.mean():.1f}/MWh median e{np.median(spread):.1f} (negative-price days: {100*np.mean(P.min(1)<0):.0f}%)")
stats("perfect-foresight", fore)
stats("naive fixed-hours", naive)
print(f" forecasting/RL gap (foresight-naive): e{(fore.mean()-naive.mean())*365/1000:.1f}k/yr/MW "
f"= {100*(fore.mean()-naive.mean())/fore.mean():.0f}% of the value")
print(f"\n viability: a 4h/1MW battery ~e400-600k capex; needs ~e50-80k/yr/MW arbitrage over 10y.")
print(f" per-year perfect-foresight arb value (e k/yr/MW):")
for y in range(2019, 2026):
m = yr == y
if m.sum() > 100:
print(f" {y}: e{np.mean(fore[m])*365/1000:5.1f}k (naive e{np.mean(naive[m])*365/1000:.1f}k)")
print("\nVERDICT: foresight value >> e50-80k threshold = battery arb economically real; big foresight-naive gap")
print("= forecasting/RL adds real value (the engine's role); persistent across years = not yet competed away.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Realistic-capture gate: how much of perfect-foresight battery value does a real day-ahead
forecast + dispatch capture? (The make-or-break for the energy pivot.)
Day-ahead model: commit tomorrow's charge/discharge schedule from a FORECAST of tomorrow's 24
prices, realize P&L against ACTUAL prices. Ladder of forecasters: persistence -> last-week ->
seasonal climatology -> walk-forward ML (gradient boosting). Report each one's realized capture
% of perfect foresight, EUR k/yr/MW, per-year. Leak-free (ML trained only on past, predict forward).
"""
import datetime
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from energy_battery_gate import fetch # cached DE prices # noqa: E402
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
ETA = 0.85
HRS = 4
ed = math.sqrt(ETA)
def dispatch_value(yhat_day, actual_day):
"""Schedule from forecast (charge HRS cheapest, discharge HRS priciest), realize on ACTUAL."""
ch = np.argsort(yhat_day)[:HRS]
dis = np.argsort(yhat_day)[-HRS:]
return actual_day[dis].sum() * ed - actual_day[ch].sum() / ed
def main():
ts, px = fetch()
ok = np.isfinite(px); ts, px = ts[ok], px[ok]
day = np.array([datetime.datetime.utcfromtimestamp(int(t)).date().toordinal() for t in ts])
rows, dord = [], []
for d in np.unique(day):
h = px[day == d]
if len(h) == 24:
rows.append(h); dord.append(d)
P = np.array(rows); D = np.array(dord); ND = len(P)
dow = np.array([datetime.date.fromordinal(int(x)).weekday() for x in D])
month = np.array([datetime.date.fromordinal(int(x)).month for x in D])
yr = np.array([datetime.date.fromordinal(int(x)).year for x in D])
foresight = np.array([np.sort(P[d])[-HRS:].sum() * ed - np.sort(P[d])[:HRS].sum() / ed for d in range(ND)])
# ---- forecasters (each -> yhat[ND,24]; only valid from d>=7) ----
yh = {}
yh["persistence"] = np.roll(P, 1, axis=0)
yh["last_week"] = np.roll(P, 7, axis=0)
clim = np.full_like(P, np.nan) # trailing 28d climatology by weekend/weekday
for d in range(14, ND):
wk = dow[d] >= 5
past = [k for k in range(max(0, d - 28), d) if (dow[k] >= 5) == wk]
if past:
clim[d] = P[past].mean(0)
yh["climatology"] = clim
# ---- ML forecaster (walk-forward, leak-free) ----
roll7 = np.full_like(P, np.nan)
for d in range(7, ND):
roll7[d] = P[d - 7:d].mean(0)
# feature builder per (day d, hour h), causal
def feats(d):
y1, y7, y2 = P[d - 1], P[d - 7], P[d - 2]
base = np.array([dow[d], month[d], 1.0 if dow[d] >= 5 else 0.0,
y1.mean(), y1.max() - y1.min(), y7.mean(), roll7[d].mean()])
X = np.zeros((24, 7 + 4))
for h in range(24):
X[h] = np.concatenate([[h], base[:1], base[1:], [y1[h], y7[h], y2[h], roll7[d, h]]])[:11]
return X
ml = np.full_like(P, np.nan)
INIT, STEP = 365, 90
Xcache = {d: feats(d) for d in range(7, ND)}
for s in range(INIT, ND, STEP):
e = min(s + STEP, ND)
Xtr = np.vstack([Xcache[d] for d in range(7, s)])
ytr = np.concatenate([P[d] for d in range(7, s)])
gb = HistGradientBoostingRegressor(max_depth=4, max_iter=200, learning_rate=0.05,
min_samples_leaf=50).fit(Xtr, ytr)
for d in range(s, e):
ml[d] = gb.predict(Xcache[d])
yh["ML walk-fwd"] = ml
print(f"\n===== REALISTIC-CAPTURE GATE (DE 4h/1MW day-ahead, {ND} days) =====")
print(f"perfect-foresight: e{foresight.mean()*365/1000:.1f}k/yr/MW")
print(f"{'forecaster':>14} {'capture%':>8} {'EURk/yr/MW':>11} | per-year capture%")
valid = np.arange(INIT, ND) # compare all on the ML-valid window
for nm, yhat in yh.items():
vv = np.array([dispatch_value(yhat[d], P[d]) for d in valid])
fv = foresight[valid]
cap = vv.sum() / fv.sum()
py = " ".join(f"{y}:{100*np.array([dispatch_value(yhat[d],P[d]) for d in valid[yr[valid]==y]]).sum()/foresight[valid[yr[valid]==y]].sum():.0f}%"
for y in range(2020, 2026) if (yr[valid] == y).sum() > 100)
print(f"{nm:>14} {100*cap:>7.0f}% {vv.mean()*365/1000:>10.1f} | {py}")
print("\nVERDICT: ML capture >> simple baselines AND >=60% of foresight (~e55-80k/yr/MW) = engine earns its keep.")
print("If even ML captures little, or simple climatology already gets most, the engine adds little here.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Harder gate: does the engine beat the climatology heuristic when given the REAL price drivers
(weather -> renewables)? And specifically on the high-volatility days where the value concentrates?
Adds free Open-Meteo weather (wind@100m, solar radiation, temp) for 3 German points to the
day-ahead forecaster. Compares: climatology (the 83% champ) vs ML-price-only (76%) vs
ML-with-weather. Capture % overall AND on the top-20% highest-spread days (where wind-drought
spikes live and fundamentals should matter). Leak-free walk-forward. Weather@day-d is the
forecast you'd have at decision time (day-ahead weather forecasts are ~90%+ accurate).
"""
import datetime
import json
import math
import os
import sys
import time
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from energy_battery_gate import fetch # cached prices # noqa: E402
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
ETA = 0.85; HRS = 4; ed = math.sqrt(ETA)
WCACHE = "data/surfer/energy/weather"
PTS = {"north": (53.55, 9.99), "central": (50.1, 8.68), "south": (48.14, 11.58)}
def wget(u):
for a in range(6):
try:
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=90))
except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(10 * (a + 1)); continue
raise
raise RuntimeError("rate-limited")
def weather():
os.makedirs(WCACHE, exist_ok=True)
acc = {}
for nm, (la, lo) in PTS.items():
f = f"{WCACHE}/{nm}.json"
if os.path.exists(f):
d = json.load(open(f))
else:
u = (f"https://archive-api.open-meteo.com/v1/archive?latitude={la}&longitude={lo}"
f"&start_date=2019-01-01&end_date=2025-09-29"
f"&hourly=wind_speed_100m,shortwave_radiation,temperature_2m&timezone=UTC")
d = wget(u)["hourly"]; json.dump(d, open(f, "w")); time.sleep(3)
acc[nm] = d
return acc
def dispatch(yhat, actual):
ch = np.argsort(yhat)[:HRS]; dis = np.argsort(yhat)[-HRS:]
return actual[dis].sum() * ed - actual[ch].sum() / ed
def main():
ts, px = fetch()
ok = np.isfinite(px); ts, px = ts[ok], px[ok]
pday = np.array([datetime.datetime.utcfromtimestamp(int(t)).date().toordinal() for t in ts])
phour = np.array([datetime.datetime.utcfromtimestamp(int(t)).hour for t in ts])
# weather aligned to (ordinal_day, hour), averaged over 3 points
W = weather()
wind = {}; sol = {}; tmp = {}
for nm, d in W.items():
for i, tstr in enumerate(d["time"]):
dt = datetime.datetime.strptime(tstr, "%Y-%m-%dT%H:%M")
k = (dt.date().toordinal(), dt.hour)
wind.setdefault(k, []).append(d["wind_speed_100m"][i] or 0.0)
sol.setdefault(k, []).append(d["shortwave_radiation"][i] or 0.0)
tmp.setdefault(k, []).append(d["temperature_2m"][i] or 0.0)
rows, dord = [], []
for dd in np.unique(pday):
h = px[pday == dd]
if len(h) == 24 and all((dd, hr) in wind for hr in range(24)):
rows.append(h); dord.append(dd)
P = np.array(rows); D = np.array(dord); ND = len(P)
Wd = np.array([[np.mean(wind[(d, h)]) for h in range(24)] for d in D])
Sd = np.array([[np.mean(sol[(d, h)]) for h in range(24)] for d in D])
Td = np.array([[np.mean(tmp[(d, h)]) for h in range(24)] for d in D])
dow = np.array([datetime.date.fromordinal(int(x)).weekday() for x in D])
month = np.array([datetime.date.fromordinal(int(x)).month for x in D])
yr = np.array([datetime.date.fromordinal(int(x)).year for x in D])
fore = np.array([np.sort(P[d])[-HRS:].sum() * ed - np.sort(P[d])[:HRS].sum() / ed for d in range(ND)])
spread = P.max(1) - P.min(1)
print(f"DE prices+weather aligned: {ND} days")
roll7 = np.full_like(P, np.nan)
for d in range(7, ND):
roll7[d] = P[d - 7:d].mean(0)
def feats(d, with_w):
y1, y7 = P[d - 1], P[d - 7]
X = np.zeros((24, 12 if with_w else 8))
for h in range(24):
f = [h, dow[d], month[d], y1[h], y7[h], y1.mean(), y7.mean(), roll7[d, h]]
if with_w:
f += [Wd[d, h], Sd[d, h], Td[d, h], Wd[d].mean()]
X[h] = f
return X
INIT, STEP = 365, 90
yhat = {"price_only": np.full_like(P, np.nan), "with_weather": np.full_like(P, np.nan)}
for with_w, key in [(False, "price_only"), (True, "with_weather")]:
Xc = {d: feats(d, with_w) for d in range(7, ND)}
for s in range(INIT, ND, STEP):
e = min(s + STEP, ND)
Xtr = np.vstack([Xc[d] for d in range(7, s)]); ytr = np.concatenate([P[d] for d in range(7, s)])
gb = HistGradientBoostingRegressor(max_depth=4, max_iter=200, learning_rate=0.05, min_samples_leaf=50).fit(Xtr, ytr)
for d in range(s, e):
yhat[key][d] = gb.predict(Xc[d])
# climatology champ
clim = np.full_like(P, np.nan)
for d in range(14, ND):
wk = dow[d] >= 5
past = [k for k in range(max(0, d - 28), d) if (dow[k] >= 5) == wk]
if past:
clim[d] = P[past].mean(0)
yhat["climatology"] = clim
valid = np.arange(INIT, ND)
hi = valid[spread[valid] >= np.quantile(spread[valid], 0.80)] # top-20% volatile days
print(f"\n===== HARDER CAPTURE GATE (fundamentals + volatile-day focus), {len(valid)} days =====")
print(f"perfect-foresight e{fore.mean()*365/1000:.1f}k/yr/MW | top-20%-vol days hold {100*fore[hi].sum()/fore[valid].sum():.0f}% of value")
print(f"{'forecaster':>14} {'capture%':>8} {'EURk/yr':>8} {'capture% on HI-VOL days':>24}")
for nm in ["climatology", "price_only", "with_weather"]:
yh = yhat[nm]
vv = np.array([dispatch(yh[d], P[d]) for d in valid]); cap = vv.sum() / fore[valid].sum()
vh = np.array([dispatch(yh[d], P[d]) for d in hi]); caph = vh.sum() / fore[hi].sum()
print(f"{nm:>14} {100*cap:>7.0f}% {vv.mean()*365/1000:>7.1f} {100*caph:>23.0f}%")
print("\nVERDICT: with_weather > climatology (esp on HI-VOL days) = engine+fundamentals earns its keep.")
print("If climatology still wins, even fundamentals don't beat the heuristic for day-ahead -> engine's home is elsewhere (intraday/multi-market).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Intraday gate: on CAISO real-time prices (less seasonal, forecast-error-driven), does the
ENGINE finally beat the heuristics — unlike day-ahead, where climatology won?
Tests: (1) is RT more volatile than DA (more battery value)? (2) RT-price forecastability:
baseline "RT=DA" vs climatology vs ML(walk-fwd, features incl. DA price + DART lags) ->
capture % of perfect-foresight RT battery value. If ML >> baselines on RT, the engine earns
its keep in the less-seasonal market. If nothing beats "RT=DA", intraday deviations are noise.
"""
import datetime
import json
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
ETA = 0.85; HRS = 4; ed = math.sqrt(ETA)
OUT = "data/surfer/caiso"
def load():
dam = json.load(open(f"{OUT}/dam.json")); rtm = json.load(open(f"{OUT}/rtm.json"))
keys = sorted(set(dam) & set(rtm)) # "YYYY-MM-DDHHH"
# group into days with full 24h
byday = {}
for k in keys:
d, h = k[:10], int(k[-2:])
byday.setdefault(d, {})[h] = (dam[k], rtm[k])
rows, dord = [], []
for d in sorted(byday):
if len(byday[d]) == 24:
da = [byday[d][h][0] for h in range(1, 25)]
rt = [byday[d][h][1] for h in range(1, 25)]
rows.append((da, rt)); dord.append(d)
DA = np.array([r[0] for r in rows]); RT = np.array([r[1] for r in rows])
D = [datetime.date.fromisoformat(x) for x in dord]
return DA, RT, D
def dispatch(yhat, actual):
ch = np.argsort(yhat)[:HRS]; dis = np.argsort(yhat)[-HRS:]
return actual[dis].sum() * ed - actual[ch].sum() / ed
def main():
DA, RT, D = load()
ND = len(DA)
dow = np.array([d.weekday() for d in D]); month = np.array([d.month for d in D])
sprDA = DA.max(1) - DA.min(1); sprRT = RT.max(1) - RT.min(1)
foreDA = np.array([np.sort(DA[d])[-HRS:].sum() * ed - np.sort(DA[d])[:HRS].sum() / ed for d in range(ND)])
foreRT = np.array([np.sort(RT[d])[-HRS:].sum() * ed - np.sort(RT[d])[:HRS].sum() / ed for d in range(ND)])
print(f"CAISO NP15 {ND} days ({D[0]}..{D[-1]})")
print(f"daily spread: DA mean ${sprDA.mean():.0f}/MWh RT mean ${sprRT.mean():.0f}/MWh (RT/DA = {sprRT.mean()/sprDA.mean():.1f}x)")
print(f"perfect-foresight battery: DA ${foreDA.mean()*365/1000:.0f}k/yr/MW RT ${foreRT.mean()*365/1000:.0f}k/yr/MW")
# RT forecasters
roll7 = np.full_like(RT, np.nan)
for d in range(7, ND):
roll7[d] = RT[d - 7:d].mean(0)
dart = RT - DA
def feats(d):
X = np.zeros((24, 9))
for h in range(24):
X[h] = [h, dow[d], month[d], DA[d, h], RT[d - 1, h], RT[d - 7, h],
dart[d - 1, h], DA[d].mean(), roll7[d, h]]
return X
ml = np.full_like(RT, np.nan); INIT, STEP = 60, 30
Xc = {d: feats(d) for d in range(7, ND)}
for s in range(INIT, ND, STEP):
e = min(s + STEP, ND)
Xtr = np.vstack([Xc[d] for d in range(7, s)]); ytr = np.concatenate([RT[d] for d in range(7, s)])
gb = HistGradientBoostingRegressor(max_depth=4, max_iter=200, learning_rate=0.05, min_samples_leaf=40).fit(Xtr, ytr)
for d in range(s, e):
ml[d] = gb.predict(Xc[d])
clim = np.full_like(RT, np.nan)
for d in range(14, ND):
wk = dow[d] >= 5
past = [k for k in range(max(0, d - 28), d) if (dow[k] >= 5) == wk]
if past:
clim[d] = RT[past].mean(0)
valid = np.arange(INIT, ND)
fc = {"baseline RT=DA": DA, "climatology": clim, "ML walk-fwd": ml}
print(f"\n===== INTRADAY (RT) CAPTURE GATE — {len(valid)} days, perfect-foresight RT ${foreRT[valid].mean()*365/1000:.0f}k/yr/MW =====")
print(f"{'RT forecaster':>16} {'capture%':>8} {'EURk/yr/MW':>11}")
for nm, yh in fc.items():
vv = np.array([dispatch(yh[d], RT[d]) for d in valid]); cap = vv.sum() / foreRT[valid].sum()
print(f"{nm:>16} {100*cap:>7.0f}% {vv.mean()*365/1000:>10.0f}")
print("\nVERDICT: ML capture >> 'RT=DA' baseline AND > climatology = the engine FINALLY earns its keep")
print("(less-seasonal RT is where forecasting/RL beats heuristics). If ML ~ baselines, intraday is noise too.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""THE CHALLENGE: does the liquid high-volatility equity cohort fit our edge structure?
The equity analog of crypto's reflexive cohort: LIQUID (top by dollar-vol -> low cost, avoids the
small-cap wall) AND HIGH realized vol (reflexive, retail-driven, less fundamentally-anchored ->
where momentum can persist, unlike efficient large-caps). The one slice where reflexivity and
liquidity OVERLAP. Test cross-sectional momentum / reversal / low-vol AND long-only momentum
(no borrow) within this cohort, gross + net of realistic (liquid ~10bp) cost, OOS, per-year.
Reuses the already-downloaded DBEQ data (free).
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll, trailing # noqa: E402
from signal_sweep import xs_weights, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DV_FLOOR = 1e7 # $10M/day: liquid -> low cost (avoid the small-cap wall)
VOL_PCTILE = 0.60 # keep names above 60th pctile realized vol (the reflexive cohort)
COST_BP = 10.0 # liquid names: ~10bp round-trip (illiquidity wall avoided by construction)
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
# cohort: liquid AND high-vol (reflexive)
univ = np.zeros((T, N), bool)
for t in range(T):
liq = (dv30[t] > DV_FLOOR) & np.isfinite(close[t]) & np.isfinite(vol63[t])
e = np.where(liq)[0]
if len(e) > 50:
thr = np.quantile(vol63[t, e], VOL_PCTILE)
univ[t, e[vol63[t, e] >= thr]] = True
print(f"loaded {N} insts {T}d; cohort/day ~{int(univ.sum(1).mean())} (liquid>${DV_FLOOR/1e6:.0f}M & high-vol)")
def held_weekly(w, K=5):
a = 2.0 / (5 + 1)
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
wh = w.copy(); last = 0
for t in range(T):
if t % K == 0:
last = t
wh[t] = wh[last] = w[last]
return wh
def pnl_ls(sig, net=True): # long-short market-neutral
s = sig.copy(); s[~univ] = np.nan
w = held_weekly(xs_weights(s))
g = np.sum(w[:-1] * R[1:], axis=1)
if not net:
return g
return g - np.sum(np.abs(w[1:] - w[:-1]), axis=1) * COST_BP / 1e4
def pnl_long(sig, q=0.10, net=True): # long-only top-decile (no borrow)
s = sig.copy(); s[~univ] = np.nan
w = np.zeros((T, N))
for t in range(T):
e = np.where(np.isfinite(s[t]) & univ[t])[0]
if len(e) > 20:
k = max(int(q * len(e)), 5)
top = e[np.argsort(-s[t, e])[:k]]; w[t, top] = 1.0 / k
w = held_weekly(w)
g = np.sum(w[:-1] * R[1:], axis=1)
if net:
g -= np.sum(np.abs(w[1:] - w[:-1]), axis=1) * COST_BP / 1e4
return g
# cohort equal-weight benchmark (the beta of the cohort)
ewb = np.array([R[t][univ[t - 1]].mean() if t > 0 and univ[t - 1].any() else 0.0 for t in range(T)])[1:]
T_ = lambda x: torch.tensor(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
sigs = {"mom_63_skip5": trailing(lc, 63, skip=5), "mom_126_skip5": trailing(lc, 126, skip=5),
"reversal_5": -trailing(lc, 5), "lowvol_63": -vol63}
print(f"\n===== LIQUID HIGH-VOL EQUITY COHORT — cross-sectional (net {COST_BP}bp) =====")
print(f"cohort equal-weight (beta) Sharpe: {sharpe_t(T_(ewb)):+.2f}")
print(f"{'factor':>16} {'L/S gross':>9} {'L/S NET':>8} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5} {'LongOnly NET':>12} | per-year(L/S net)")
for nm, sg in sigs.items():
g = pnl_ls(sg, net=False); p = pnl_ls(sg, net=True); lo = pnl_long(sg, net=True)
v = validate(p, days, 20)
py = " ".join(f"{y}:{sharpe_t(T_(p[year[1:]==y])):+.1f}" for y in range(2023, 2027) if (year[1:] == y).sum() > 40)
print(f"{nm:>16} {sharpe_t(T_(g)):>+9.2f} {v['full']:>+8.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f} {sharpe_t(T_(lo)):>+12.2f} | {py}")
print("\nVERDICT: a factor with L/S NET full+OOS+CPCVmed>0 & DSR>0.5 in the reflexive-liquid cohort = the fit.")
print("If momentum still negative even here, the cohort doesn't rescue it (efficiency/regime, not cost).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Small/mid-cap US equity cross-sectional factor gate (DBEQ daily, realistic small-cap costs).
The less-efficient corner: exclude mega-caps (too efficient) and illiquid micro-caps
(untradeable); keep the small/mid liquid band where your small capital is an advantage.
Test XS momentum (12-1 style), short-term reversal, low-vol, residual momentum — GROSS and
NET of ILLIQUIDITY-SCALED cost (small-cap spreads ~30-150bp, the honest killer). Weekly
rebalance + smoothing. Point-in-time universe (incl delisted) -> survivorship-aware.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import xs_weights, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_NS = 86_400 * 10**9
OUT = "data/surfer/dbeq_ohlcv1d.dbn"
EXCLUDE_TOP = 50 # drop mega-caps (efficient)
DV_FLOOR = 2e6 # $2M/day min (tradeable)
TOPK = 600 # small/mid liquid band size
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def trailing(lc, L, skip=0):
out = np.full_like(lc, np.nan)
if skip:
out[L + skip:] = lc[L:-skip] - lc[:-(L + skip)]
else:
out[L:] = lc[L:] - lc[:-L]
return out
def load():
import databento as db
a = db.DBNStore.from_file(OUT).to_ndarray()
iid = a["instrument_id"]; ts = a["ts_event"].astype(np.int64)
close = a["close"].astype(np.float64) / 1e9
vol = a["volume"].astype(np.float64)
day = ts // DAY_NS
days = np.unique(day); insts = np.unique(iid)
dix = np.searchsorted(days, day); iix = np.searchsorted(insts, iid)
T, N = len(days), len(insts)
C = np.full((T, N), np.nan); DVOL = np.full((T, N), np.nan)
C[dix, iix] = np.where(close > 0, close, np.nan)
DVOL[dix, iix] = close * vol
return insts, days, C, DVOL
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
print(f"loaded {N} instruments, {T} days ({days.min()}..{days.max()})")
# point-in-time small/mid-cap universe: drop top mega-caps, require liquidity floor, take band
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > DV_FLOOR) & np.isfinite(close[t]))[0]
if len(elig) > EXCLUDE_TOP + 20:
order = elig[np.argsort(-dv30[t, elig])]
band = order[EXCLUDE_TOP:EXCLUDE_TOP + TOPK] # skip mega-caps, take next TOPK
univ[t, band] = True
# illiquidity-scaled round-trip cost (bp): small-caps 30-150bp, mid 5-30bp
rt_cost = np.clip(60.0 / np.sqrt(np.maximum(dv30, 1.0) / 1e6), 5.0, 150.0) / 1e4
def held_weekly(wt, K=5):
wh = wt.copy()
last = 0
for t in range(T):
if t % K == 0:
last = t
wh[t] = wt[last]
return wh
def pnl(sig, net=True):
s = sig.copy(); s[~univ] = np.nan
w = xs_weights(s)
a = 2.0 / (5 + 1) # smooth span 5
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
w = held_weekly(w)
gross = np.sum(w[:-1] * R[1:], axis=1)
if not net:
return gross
turn = np.abs(w[1:] - w[:-1])
cost = np.sum(turn * rt_cost[1:], axis=1)
return gross - cost
sigs = {
"mom_63_skip5": trailing(lc, 63, skip=5),
"mom_126_skip5": trailing(lc, 126, skip=5),
"reversal_5": -trailing(lc, 5),
"lowvol_63": -vol63,
}
print(f"\n===== SMALL/MID-CAP EQUITY FACTOR GATE (band {EXCLUDE_TOP}-{EXCLUDE_TOP+TOPK}, ${DV_FLOOR/1e6:.0f}M floor) =====")
print(f"universe/day ~{int(univ.sum(1).mean())}, weekly rebal+smooth, deflate N=20")
print(f"{'factor':>16} {'gross':>6} {'NET':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5} | per-year")
T_ = lambda x: torch.tensor(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
for nm, sg in sigs.items():
g = pnl(sg, net=False); p = pnl(sg, net=True)
gsr = sharpe_t(T_(g)); v = validate(p, days, 20)
py = " ".join(f"{y}:{sharpe_t(T_(p[year[1:]==y])):+.1f}" for y in range(2023, 2027) if (year[1:] == y).sum() > 40)
print(f"{nm:>16} {gsr:>+6.2f} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f} | {py}")
print("\nVERDICT: a factor with NET full+OOS+CPCVmed>0 & DSR>0.5 survives small-cap costs = real.")
print("gross>>NET means the edge is eaten by illiquidity cost (the usual small-cap fate).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Decompose the equity low-vol +0.74 in detail: alpha vs beta, which leg, turnover, concentration.
Questions: (1) Is it cross-sectional ALPHA or a structural short-beta directional bet?
(2) Which leg carries it — long low-vol (deployable, no borrow) or short high-vol (expensive)?
(3) Turnover (is the cost charge fair?). (4) P&L concentration over time. (5) Beta-neutral residual.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll # noqa: E402
from signal_sweep import validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DV_FLOOR, VOL_PCT, COST = 1e7, 0.60, 10.0
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
univ = np.zeros((T, N), bool)
for t in range(T):
e = np.where((dv30[t] > DV_FLOOR) & np.isfinite(close[t]) & np.isfinite(vol63[t]))[0]
if len(e) > 50:
univ[t, e[vol63[t, e] >= np.quantile(vol63[t, e], VOL_PCT)]] = True
def held(w, K=5):
a = 2.0 / 6
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
wh = w.copy(); last = 0
for t in range(T):
if t % K == 0:
last = t
wh[t] = w[last]
return wh
def leg(which, q=0.10):
"""EW long basket of a decile: 'low'=lowest-vol, 'high'=highest-vol, 'all'=whole cohort."""
w = np.zeros((T, N))
for t in range(T):
e = np.where(univ[t])[0]
if len(e) < 20:
continue
if which == "all":
w[t, e] = 1.0 / len(e)
else:
order = e[np.argsort(vol63[t, e])] # ascending vol
k = max(int(q * len(e)), 5)
sel = order[:k] if which == "low" else order[-k:]
w[t, sel] = 1.0 / k
w = held(w)
pnl = np.sum(w[:-1] * R[1:], axis=1) - np.sum(np.abs(w[1:] - w[:-1]), axis=1) * COST / 1e4
turn = float(np.mean(np.sum(np.abs(w[1:] - w[:-1]), axis=1)))
return pnl, turn
lo, lo_turn = leg("low")
hi, hi_turn = leg("high")
mkt, _ = leg("all")
ls = lo - hi # dollar-neutral L/S (gross of the extra cost already in legs)
sr = sharpe_t
print(f"\n===== EQUITY LOW-VOL +0.74 DECOMPOSITION (cohort liquid>${DV_FLOOR/1e6:.0f}M & vol>{int(VOL_PCT*100)}pct) =====")
print(f"cohort EW (beta/market) Sharpe {sr(T_(mkt)):+.2f}")
print(f"LONG leg (low-vol decile) Sharpe {sr(T_(lo)):+.2f} alpha-vs-cohort {sr(T_(lo-mkt)):+.2f} turn/period {lo_turn:.2f}")
print(f"HIGH-vol decile Sharpe {sr(T_(hi)):+.2f} (short it -> +{sr(T_(mkt-hi)):+.2f} short-alpha-vs-cohort)")
print(f"L/S (low - high) Sharpe {sr(T_(ls)):+.2f}")
# beta decomposition of L/S vs cohort market
a, b = np.asarray(mkt), np.asarray(ls)
m = np.isfinite(a) & np.isfinite(b); x, yv = a[m], b[m]
beta = float(np.cov(x, yv)[0, 1] / (np.var(x) + 1e-12))
resid = yv - beta * x
print(f"\nL/S beta to cohort-market = {beta:+.2f} (negative = structural short-beta tilt)")
print(f"L/S beta-NEUTRAL residual alpha Sharpe = {sr(T_(resid)):+.2f} "
f"(this is the TRUE cross-sectional alpha; if ~0 it was just short-beta)")
# long-leg deployable check (no borrow)
vlo = validate(lo - mkt, days, 30) # long-leg market-neutralized (long decile vs cohort)
print(f"\nLONG-leg alpha (deployable, no borrow): full {vlo['full']:+.2f} OOS {vlo['oos']:+.2f} CPCVmed {vlo['med']:+.2f} DSR {vlo['dsr']:.2f}")
# per-year + concentration
print("per-year L/S: " + " ".join(f"{y}:{sr(T_(ls[year[1:]==y])):+.2f}" for y in range(2023,2027) if (year[1:]==y).sum()>40))
p = np.nan_to_num(ls); tot = p.sum(); top = np.sort(p)[-10:].sum()
print(f"P&L concentration: top-10 days = {100*top/ (tot+1e-12):.0f}% of total (high => episodic/fragile)")
print(f"turnover: low-leg {lo_turn:.2f}/period, high-leg {hi_turn:.2f}/period (low => slow signal, cost charge fair)")
print("\nREAD: if beta-neutral residual ~0 => short-beta bet (fragile); if long-leg alpha DSR>0.5 => deployable long-only edge.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Full gauntlet on the equity low-vol/lottery-aversion lead (liquid high-vol cohort).
The make-or-break for the first non-crypto edge: (1) name-bootstrap (robust to WHICH names? —
the test that killed carry), (2) cohort-param robustness (did $10M/60pct manufacture it?),
(3) realistic short-borrow + long-only (is the edge trapped in the un-cheap short?),
(4) honest deflation + per-year, (5) correlation to the crypto momentum book (diversifier?).
Free — DBEQ on disk.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll # noqa: E402
from signal_sweep import xs_weights, validate, sharpe_t # noqa: E402
import pit_sweep # noqa: E402
from surfer_poc import compute_weights, CFG # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
T_ = lambda x: torch.tensor(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
def cohort(dv_floor, vol_pct):
u = np.zeros((T, N), bool)
for t in range(T):
liq = (dv30[t] > dv_floor) & np.isfinite(close[t]) & np.isfinite(vol63[t])
e = np.where(liq)[0]
if len(e) > 50:
u[t, e[vol63[t, e] >= np.quantile(vol63[t, e], vol_pct)]] = True
return u
def smooth_weekly(w, K=5):
a = 2.0 / 6
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
wh = w.copy(); last = 0
for t in range(T):
if t % K == 0:
last = t
wh[t] = w[last]
return wh
def lowvol_pnl(univ, cols=None, long_only=False, borrow_ann=0.0):
sig = (-vol63).copy(); sig[~univ] = np.nan
if cols is not None:
mask = np.zeros(N, bool); mask[cols] = True; sig[:, ~mask] = np.nan
if long_only:
w = np.zeros((T, N))
for t in range(T):
e = np.where(np.isfinite(sig[t]))[0]
if len(e) > 20:
k = max(int(0.10 * len(e)), 5); w[t, e[np.argsort(-sig[t, e])[:k]]] = 1.0 / k
else:
w = xs_weights(sig)
w = smooth_weekly(w)
g = np.sum(w[:-1] * R[1:], axis=1)
g -= np.sum(np.abs(w[1:] - w[:-1]), axis=1) * 10.0 / 1e4 # 10bp trade cost
if borrow_ann > 0: # borrow on short notional
short_notional = np.sum(np.clip(-w[:-1], 0, None), axis=1)
g -= short_notional * borrow_ann / 252.0
return g
base = cohort(1e7, 0.60)
base_pnl = lowvol_pnl(base)
v = validate(base_pnl, days, 30)
print(f"\n===== EQUITY LOW-VOL GAUNTLET (liquid high-vol cohort, deflate N=30) =====")
print(f"BASE L/S: full {v['full']:+.2f} OOS {v['oos']:+.2f} CPCVmed {v['med']:+.2f} DSR {v['dsr']:.2f}")
# (1) name-bootstrap
cohort_cols = np.where(base.any(0))[0]
rng = np.random.default_rng(5); sh = []
for _ in range(200):
c = rng.choice(cohort_cols, size=max(len(cohort_cols) // 2, 20), replace=False)
s = sharpe_t(T_(lowvol_pnl(base, cols=c)))
if not math.isnan(s):
sh.append(s)
sh = np.array(sh)
print(f"(1) name-bootstrap(200): frac>0 {np.mean(sh>0):.2f} median {np.median(sh):+.2f} 5th {np.percentile(sh,5):+.2f}")
# (2) cohort-param robustness
print("(2) cohort-param grid (L/S full Sharpe):")
for fl in [5e6, 1e7, 2e7, 5e7]:
row = []
for vp in [0.50, 0.60, 0.70]:
row.append(sharpe_t(T_(lowvol_pnl(cohort(fl, vp)))))
print(f" ${fl/1e6:>3.0f}M floor: " + " ".join(f"vp{int(vp*100)}:{r:+.2f}" for vp, r in zip([0.50,0.60,0.70], row)))
# (3) short-borrow + long-only
print("(3) short-borrow & long-only (net):")
for ba in [0.0, 0.10, 0.30]:
print(f" L/S borrow {int(ba*100)}%/yr: {sharpe_t(T_(lowvol_pnl(base, borrow_ann=ba))):+.2f}")
lo = lowvol_pnl(base, long_only=True)
vlo = validate(lo, days, 30)
print(f" LONG-ONLY (no borrow): full {vlo['full']:+.2f} OOS {vlo['oos']:+.2f} CPCVmed {vlo['med']:+.2f} DSR {vlo['dsr']:.2f}")
# (4) per-year (base L/S)
print("(4) per-year (base L/S): " + " ".join(f"{y}:{sharpe_t(T_(base_pnl[year[1:]==y])):+.2f}" for y in range(2023,2027) if (year[1:]==y).sum()>40))
# (5) correlation to crypto momentum book
syms, cdays, cc, cqv, cf = pit_sweep.load()
cw, _ = compute_weights(cc, cqv, cdays, CFG)
cR = np.zeros_like(cc); cR[1:] = np.log(cc)[1:] - np.log(cc)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0)
cff = np.where(np.isfinite(cf), cf, 0.0)
cmom = np.sum(cw[:-1] * (cR - cff)[1:], axis=1)
cmd = {int(cdays[1:][i]): cmom[i] for i in range(len(cmom))}
emd = {int(days[1:][i]): base_pnl[i] for i in range(len(base_pnl))}
common = sorted(set(cmd) & set(emd))
a = np.array([cmd[d] for d in common]); b = np.array([emd[d] for d in common])
m = np.isfinite(a) & np.isfinite(b)
corr = float(np.corrcoef(a[m], b[m])[0, 1]) if m.sum() > 50 else float("nan")
print(f"(5) corr to crypto-momentum book: {corr:+.2f} (low => diversifier in a preferred market)")
print("\nVERDICT: bootstrap frac>0~1 + grid broadly + long-only positive + DSR>0.5 + low crypto-corr = real 2nd sleeve.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Equity-index VRP on XSP (mini-SPX) options, 2013-2026 — the one real-prior frontier.
For each day: find the ~30d ATM straddle (strike where call~=put = the forward), back out
implied vol from the straddle price, compare to forward realized vol. VRP = IV - RV; a
short-vol seller harvests it. 13y spans 2018/2020/2022 tail events. Tests raw VRP Sharpe,
per-year (incl. crashes), tail/skew, AND the tail-managed version (don't sell when IV rising
— the gate that fixed crypto VRP). XSP = cash-settled European -> no early-exercise distortion.
"""
import glob
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_NS = 86_400 * 10**9
def load_xsp():
import databento as db
rows = []
for p in sorted(glob.glob("data/surfer/xsp/*.dbn")):
try:
df = db.DBNStore.from_file(p).to_df().reset_index()
except Exception:
continue
if df.empty or "symbol" not in df.columns:
continue
df = df[df["close"] > 0]
rows.append(df[["ts_event", "symbol", "close"]])
import pandas as pd
d = pd.concat(rows, ignore_index=True)
s = d["symbol"].astype(str).str.replace(" ", "", regex=False) # "XSP240119C00450000"
d["right"] = s.str[-9]
d["strike"] = s.str[-8:].astype(float) / 1000.0
d["expiry"] = s.str[-15:-9] # YYMMDD
d["day"] = (d["ts_event"].astype("int64") // DAY_NS)
return d
def to_epoch_day(yymmdd):
import datetime
y = 2000 + int(yymmdd[:2]); mo = int(yymmdd[2:4]); da = int(yymmdd[4:6])
return (datetime.date(y, mo, da) - datetime.date(1970, 1, 1)).days
def main():
d = load_xsp()
print(f"loaded {len(d)} XSP option-days, {d['day'].nunique()} trading days")
d["exp_day"] = d["expiry"].map(to_epoch_day)
d["ttm"] = d["exp_day"] - d["day"]
d = d[(d["ttm"] >= 20) & (d["ttm"] <= 45)] # ~30d window
iv_by_day = {}
S_by_day = {}
for day, g in d.groupby("day"):
# pick the expiry closest to 30d
exp = g.iloc[(g["ttm"] - 30).abs().argsort()].iloc[0]["exp_day"]
ge = g[g["exp_day"] == exp]
calls = ge[ge["right"] == "C"].groupby("strike")["close"].mean() # dedup AM/PM series
puts = ge[ge["right"] == "P"].groupby("strike")["close"].mean()
common = calls.index.intersection(puts.index)
if len(common) < 3:
continue
diff = (calls[common] - puts[common]).abs()
katm = diff.idxmin() # ATM: where C~=P
S = float(katm + calls[katm] - puts[katm]) # parity forward
straddle = float(calls[katm] + puts[katm])
T = float(ge["ttm"].iloc[0]) / 365.0
if S <= 0 or T <= 0:
continue
iv = straddle / (0.8 * S * math.sqrt(T)) # ATM straddle -> implied vol
iv_by_day[int(day)] = iv; S_by_day[int(day)] = S
days = np.array(sorted(S_by_day))
S = np.array([S_by_day[x] for x in days]); IV = np.array([iv_by_day[x] for x in days])
r = np.zeros(len(days)); r[1:] = np.log(S[1:] / S[:-1])
# forward 21-day realized vol (annualized) — what the seller faces
H = 21
rv_fwd = np.full(len(days), np.nan)
for t in range(len(days) - H):
rv_fwd[t] = np.std(r[t + 1:t + 1 + H]) * math.sqrt(252)
vrp = IV - rv_fwd # premium (positive = seller wins)
# short-vol daily P&L proxy: collect implied variance, pay realized squared return
iv_lag = np.concatenate([[np.nan], IV[:-1]])
svol = (iv_lag ** 2) / 252.0 - r ** 2
year = np.array([1970 + x / 365.25 for x in days]).astype(int)
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
print(f"\n===== EQUITY-INDEX VRP (XSP, {days.min()}..{days.max()}, {len(days)} days) =====")
print(f"mean implied vol {np.nanmean(IV):.1%} mean fwd-realized {np.nanmean(rv_fwd):.1%} "
f"mean VRP {np.nanmean(vrp):.1%} (positive => premium exists)")
print(f"VRP frac>0 (IV>RV): {np.nanmean(vrp[np.isfinite(vrp)]>0):.2f}")
print(f"\nshort-vol daily P&L: Sharpe {sharpe_t(T_(svol)):+.2f} skew {float(((svol[np.isfinite(svol)]-np.nanmean(svol))**3).mean()/np.nanstd(svol)**3):+.2f} worst-day {np.nanmin(svol)/np.nanstd(svol):+.1f}σ")
print("per-year short-vol: " + " ".join(f"{y}:{sharpe_t(T_(svol[year==y])):+.1f}" for y in range(2013, 2027) if (year == y).sum() > 60))
# tail-managed: don't sell when implied vol rising over 5d (the crypto-VRP gate)
rising = np.zeros(len(days))
for t in range(6, len(days)):
rising[t] = 0.0 if IV[t - 1] > IV[t - 6] else 1.0
svol_tm = rising * svol
print(f"\ntail-managed (don't sell into rising IV): Sharpe {sharpe_t(T_(svol_tm)):+.2f} "
f"skew {float(((svol_tm[np.isfinite(svol_tm)]-np.nanmean(svol_tm))**3).mean()/np.nanstd(svol_tm)**3):+.2f} worst {np.nanmin(svol_tm)/np.nanstd(svol_tm):+.1f}σ")
print("per-year tail-managed: " + " ".join(f"{y}:{sharpe_t(T_(svol_tm[year==y])):+.1f}" for y in range(2013, 2027) if (year == y).sum() > 60))
print("\nVERDICT: VRP frac>0 high + short-vol Sharpe>0 + tail-managed improves skew/recent = real, harvestable equity VRP.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Equity-index VRP, CORRECTED: clean SPY underlying (not noisy parity) + XSP option-implied IV.
Fixes the bug where parity-derived underlying inflated realized vol (spurious -VRP). SPY ~= XSP
(~SPX/10), so SPY is the clean underlying for ATM reference + realized vol. Overlap 2023-2026.
VRP = implied (XSP ATM straddle) - realized (clean SPY). Short-vol Sharpe, per-year, tail, tail-managed.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_vrp import load_xsp, to_epoch_day # noqa: E402
from signal_sweep import sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_NS = 86_400 * 10**9
def load_spy():
import databento as db
a = db.DBNStore.from_file("data/surfer/spy_ohlcv1d.dbn").to_ndarray()
day = a["ts_event"].astype(np.int64) // DAY_NS
close = a["close"].astype(np.float64) / 1e9
return {int(day[i]): float(close[i]) for i in range(len(day)) if close[i] > 0}
def main():
spy = load_spy()
print(f"SPY clean: {len(spy)} days ({min(spy)}..{max(spy)}), level {spy[min(spy)]:.0f}->{spy[max(spy)]:.0f}")
d = load_xsp()
d["exp_day"] = d["expiry"].map(to_epoch_day)
d["ttm"] = d["exp_day"] - d["day"]
d = d[(d["ttm"] >= 20) & (d["ttm"] <= 45)]
d = d[d["day"].isin(spy.keys())] # overlap with clean SPY
iv = {}
for day, g in d.groupby("day"):
S = spy[int(day)]
exp = g.iloc[(g["ttm"] - 30).abs().argsort()].iloc[0]["exp_day"]
ge = g[g["exp_day"] == exp]
calls = ge[ge["right"] == "C"].groupby("strike")["close"].mean()
puts = ge[ge["right"] == "P"].groupby("strike")["close"].mean()
common = calls.index.intersection(puts.index)
if len(common) < 3:
continue
katm = common[np.abs(np.array(common) - S).argmin()] # ATM = strike nearest clean SPY level
straddle = float(calls[katm] + puts[katm])
T = float(ge["ttm"].iloc[0]) / 365.0
if straddle <= 0 or T <= 0:
continue
iv[int(day)] = straddle / (0.8 * S * math.sqrt(T))
days = np.array(sorted(set(iv) & set(spy)))
S = np.array([spy[x] for x in days]); IV = np.array([iv[x] for x in days])
r = np.zeros(len(days)); r[1:] = np.log(S[1:] / S[:-1])
H = 21
rv_fwd = np.full(len(days), np.nan)
for t in range(len(days) - H):
rv_fwd[t] = np.std(r[t + 1:t + 1 + H]) * math.sqrt(252)
vrp = IV - rv_fwd
iv_lag = np.concatenate([[np.nan], IV[:-1]])
svol = (iv_lag ** 2) / 252.0 - r ** 2
year = np.array([1970 + x / 365.25 for x in days]).astype(int)
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
fin = np.isfinite(vrp)
print(f"\n===== EQUITY-INDEX VRP (CORRECTED, clean SPY) — {len(days)} days =====")
print(f"mean implied {np.nanmean(IV):.1%} mean fwd-realized(clean) {np.nanmean(rv_fwd):.1%} "
f"mean VRP {np.nanmean(vrp[fin]):+.1%} frac>0 {np.mean(vrp[fin]>0):.2f}")
sk = float(((svol[np.isfinite(svol)] - np.nanmean(svol)) ** 3).mean() / np.nanstd(svol) ** 3)
print(f"short-vol daily P&L: Sharpe {sharpe_t(T_(svol)):+.2f} skew {sk:+.2f} worst {np.nanmin(svol)/np.nanstd(svol):+.1f}σ")
print("per-year: " + " ".join(f"{y}:{sharpe_t(T_(svol[year==y])):+.1f}" for y in range(2023, 2027) if (year == y).sum() > 40))
rising = np.zeros(len(days))
for t in range(6, len(days)):
rising[t] = 0.0 if IV[t - 1] > IV[t - 6] else 1.0
tm = rising * svol
print(f"tail-managed (don't sell into rising IV): Sharpe {sharpe_t(T_(tm)):+.2f} worst {np.nanmin(tm)/np.nanstd(tm):+.1f}σ")
print("per-year TM: " + " ".join(f"{y}:{sharpe_t(T_(tm[year==y])):+.1f}" for y in range(2023, 2027) if (year == y).sum() > 40))
print("\nVERDICT: mean VRP>0 + frac>0~0.8 confirms premium exists; short-vol Sharpe (esp tail-managed) = harvestable.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Equity-index VRP, robust internal-parity version (self-consistent, no external underlying).
Per day: implied forward F = MEDIAN over the 5 near-ATM strikes of (K + C - P) [put-call parity,
median-smoothed to kill per-strike noise]. Use F consistently for ATM, implied vol, AND the
realized-vol series. Internally consistent -> avoids both the single-strike-parity noise and the
SPY!=XSP divergence that produced spurious negative VRP. XSP 2013-2026.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_vrp import load_xsp, to_epoch_day # noqa: E402
from signal_sweep import sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def main():
d = load_xsp()
d["exp_day"] = d["expiry"].map(to_epoch_day)
d["ttm"] = d["exp_day"] - d["day"]
d = d[(d["ttm"] >= 20) & (d["ttm"] <= 45)]
F, IVm = {}, {}
for day, g in d.groupby("day"):
exp = g.iloc[(g["ttm"] - 30).abs().argsort()].iloc[0]["exp_day"]
ge = g[g["exp_day"] == exp]
calls = ge[ge["right"] == "C"].groupby("strike")["close"].mean()
puts = ge[ge["right"] == "P"].groupby("strike")["close"].mean()
common = np.array(sorted(set(calls.index) & set(puts.index)))
if len(common) < 5:
continue
rough = common[np.abs(calls[common].values - puts[common].values).argmin()] # min|C-P| ~ ATM
near = common[np.argsort(np.abs(common - rough))[:5]] # 5 nearest strikes
f = float(np.median([k + float(calls[k]) - float(puts[k]) for k in near])) # parity forward (median)
katm = common[np.abs(common - f).argmin()]
straddle = float(calls[katm] + puts[katm])
T = float(ge["ttm"].iloc[0]) / 365.0
if f <= 0 or straddle <= 0 or T <= 0:
continue
F[int(day)] = f; IVm[int(day)] = straddle / (0.8 * f * math.sqrt(T))
days = np.array(sorted(F))
S = np.array([F[x] for x in days]); IV = np.array([IVm[x] for x in days])
r = np.zeros(len(days)); r[1:] = np.log(S[1:] / S[:-1])
r = np.clip(r, -0.25, 0.25) # guard residual parity glitches
H = 21
rv = np.full(len(days), np.nan)
for t in range(len(days) - H):
rv[t] = np.std(r[t + 1:t + 1 + H]) * math.sqrt(252)
vrp = IV - rv; fin = np.isfinite(vrp)
iv_lag = np.concatenate([[np.nan], IV[:-1]])
svol = (iv_lag ** 2) / 252.0 - r ** 2
year = np.array([1970 + x / 365.25 for x in days]).astype(int)
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
print(f"\n===== EQUITY-INDEX VRP (robust internal parity) — {len(days)} days ({days.min()}..{days.max()}) =====")
print(f"implied-forward level {S[0]:.0f}->{S[-1]:.0f} (should track SPX/10 ~ 400->650)")
print(f"mean implied {np.nanmean(IV):.1%} mean fwd-realized {np.nanmean(rv):.1%} "
f"mean VRP {np.nanmean(vrp[fin]):+.1%} frac>0 {np.mean(vrp[fin]>0):.2f}")
sk = float(((svol[np.isfinite(svol)] - np.nanmean(svol)) ** 3).mean() / np.nanstd(svol) ** 3)
print(f"short-vol daily P&L: Sharpe {sharpe_t(T_(svol)):+.2f} skew {sk:+.2f} worst {np.nanmin(svol)/np.nanstd(svol):+.1f}σ")
print("per-year: " + " ".join(f"{y}:{sharpe_t(T_(svol[year==y])):+.1f}" for y in range(2013, 2027) if (year == y).sum() > 60))
rising = np.zeros(len(days))
for t in range(6, len(days)):
rising[t] = 0.0 if IV[t - 1] > IV[t - 6] else 1.0
tm = rising * svol
print(f"tail-managed: Sharpe {sharpe_t(T_(tm)):+.2f} worst {np.nanmin(tm)/np.nanstd(tm):+.1f}σ")
print("per-year TM: " + " ".join(f"{y}:{sharpe_t(T_(tm[year==y])):+.1f}" for y in range(2013, 2027) if (year == y).sum() > 60))
print("\nVERDICT: implied-forward tracking ~SPX/10 + mean RV ~13-16% + VRP>0 => measurement clean & premium real.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Fetch CAISO day-ahead (DAM) + real-time (RTM) hourly LMP for one hub (free OASIS, no key).
The RT-vs-DA spread is the forecast-error-driven, less-seasonal intraday opportunity. Monthly
chunks with backoff (OASIS rate-limits hard). Aggregates RT to hourly. Caches per chunk.
Saves data/surfer/caiso/{dam,rtm}.json as {hour_key: price}.
"""
import io
import json
import os
import time
import urllib.request
import zipfile
NODE = "TH_NP15_GEN-APND"
OUT = "data/surfer/caiso"
MONTHS = [(2024, m) for m in range(1, 13)]
def oasis(qn, market, y, m):
s = f"{y}{m:02d}01T08:00-0000"
ny, nm = (y + 1, 1) if m == 12 else (y, m + 1)
e = f"{ny}{nm:02d}01T08:00-0000"
u = (f"http://oasis.caiso.com/oasisapi/SingleZip?queryname={qn}&startdatetime={s}"
f"&enddatetime={e}&version=1&market_run_id={market}&node={NODE}&resultformat=6")
for a in range(5):
try:
raw = urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=90).read()
z = zipfile.ZipFile(io.BytesIO(raw))
txt = z.read(z.namelist()[0]).decode()
return txt
except Exception as ex:
time.sleep(12 * (a + 1))
return None
def parse_hourly(txt):
lines = txt.strip().split("\n")
hdr = lines[0].split(",")
if "LMP_TYPE" not in hdr:
return {} # error report / wrong format
iT = hdr.index("LMP_TYPE")
iV = hdr.index("MW") if "MW" in hdr else (hdr.index("PRC") if "PRC" in hdr else -1)
if iV < 0:
return {}
iD = hdr.index("OPR_DT"); iH = hdr.index("OPR_HR")
agg = {}
for ln in lines[1:]:
c = ln.split(",")
if len(c) <= max(iT, iV, iD, iH):
continue
if c[iT] != "LMP":
continue
k = f"{c[iD]}H{int(c[iH]):02d}"
agg.setdefault(k, []).append(float(c[iV]))
return {k: sum(v) / len(v) for k, v in agg.items()}
def fetch(market, qn):
os.makedirs(OUT, exist_ok=True)
out = {}
for y, m in MONTHS:
cf = f"{OUT}/{market}_{y}{m:02d}.json"
if os.path.exists(cf):
out.update(json.load(open(cf))); continue
txt = oasis(qn, market, y, m)
if not txt:
print(f" {market} {y}-{m:02d}: FAILED"); continue
h = parse_hourly(txt)
json.dump(h, open(cf, "w")); out.update(h)
print(f" {market} {y}-{m:02d}: {len(h)} hrs")
time.sleep(6)
return out
def main():
dam = fetch("DAM", "PRC_LMP"); rtm = fetch("RTPD", "PRC_RTPD_LMP") # 15-min RT pre-dispatch
json.dump(dam, open(f"{OUT}/dam.json", "w")); json.dump(rtm, open(f"{OUT}/rtm.json", "w"))
print(f"DONE: DAM {len(dam)} hrs, RTM {len(rtm)} hrs")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Fetch daily klines + funding history for major USDT perps (Binance, free, no key).
Caches per-symbol npz to data/surfer/crypto/ (gitignored): day, open, close, funding_daily
(sum of the 8h funding rates that day). Curated long-history majors → reduces (not eliminates)
survivorship; v1 caveat documented. Crypto is 24/7 → no roll / no overnight gap.
"""
import json
import os
import time
import urllib.request
import numpy as np
OUT = "data/surfer/crypto"
DAY_MS = 86_400_000
TOP_N = 80 # programmatic universe: top-N USDT perps by 24h quote-volume (removes hand-selection bias)
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "curl/8"})
return json.load(urllib.request.urlopen(req, timeout=30))
def universe(n):
info = get("https://fapi.binance.com/fapi/v1/exchangeInfo")
perps = {s["symbol"] for s in info["symbols"]
if s.get("contractType") == "PERPETUAL" and s.get("quoteAsset") == "USDT"
and s.get("status") == "TRADING"}
tick = get("https://fapi.binance.com/fapi/v1/ticker/24hr")
vol = {t["symbol"]: float(t["quoteVolume"]) for t in tick if t["symbol"] in perps}
return sorted(vol, key=lambda s: -vol[s])[:n]
SYMS = universe(TOP_N)
def klines(sym):
out = []
end = None
for _ in range(20):
u = f"https://fapi.binance.com/fapi/v1/klines?symbol={sym}&interval=1d&limit=1500"
if end:
u += f"&endTime={end}"
k = get(u)
if not k:
break
out = k + out
end = k[0][0] - 1
if len(k) < 1500:
break
time.sleep(0.15)
# dedup by openTime
d = {int(r[0]): (float(r[1]), float(r[4])) for r in out}
days = np.array(sorted(d))
op = np.array([d[t][0] for t in days]); cl = np.array([d[t][1] for t in days])
return days // DAY_MS, op, cl
def funding(sym, start_ms):
out = []
st = start_ms
for _ in range(80): # forward pagination (startTime works; endTime didn't)
u = f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={sym}&startTime={st}&limit=1000"
f = get(u)
if not f:
break
out += f
st = f[-1]["fundingTime"] + 1
if len(f) < 1000:
break
time.sleep(0.12)
daily = {}
for r in out:
daily.setdefault(int(r["fundingTime"]) // DAY_MS, 0.0)
daily[int(r["fundingTime"]) // DAY_MS] += float(r["fundingRate"])
return daily
def main():
os.makedirs(OUT, exist_ok=True)
ok = 0
for sym in SYMS:
outp = f"{OUT}/{sym}.npz"
if os.path.exists(outp):
print(f" {sym}: cached"); ok += 1; continue
try:
kd, op, cl = klines(sym)
if len(kd) < 400:
print(f" {sym}: too short ({len(kd)}d), skip"); continue
fmap = funding(sym, int(kd.min()) * DAY_MS)
fund = np.array([fmap.get(int(d), 0.0) for d in kd])
np.savez(outp, day=kd, open=op, close=cl, funding=fund)
print(f" {sym}: {len(kd)}d ({kd.min()}..{kd.max()}) fundcov={np.mean(fund!=0):.2f}")
ok += 1
time.sleep(0.2)
except Exception as e:
print(f" {sym}: FAIL {type(e).__name__} {str(e)[:80]}")
print(f"DONE: {ok}/{len(SYMS)} symbols -> {OUT}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Point-in-time crypto fetch: broad live universe + KNOWN-DEAD perps, WITH volume.
For a survivorship-free test: delisted Binance perps still return klines up to delisting,
so including them lets a date-by-date "top-N by trailing dollar-volume" universe contain
dead coins while they traded and drop them when they die. Saves day, close, qvol, funding
to data/surfer/crypto_pit/ (gitignored).
"""
import json
import os
import time
import urllib.request
import numpy as np
OUT = "data/surfer/crypto_pit"
DAY_MS = 86_400_000
TOP_N = 120
# famous delisted / dead Binance USDT perps (the survivorship risk); klines available post-delist
DEAD = ["LUNAUSDT", "ANCUSDT", "SRMUSDT", "HNTUSDT", "MATICUSDT", "FTTUSDT", "RAYUSDT",
"WAVESUSDT", "BNXUSDT", "SCUSDT", "OCEANUSDT", "AGIXUSDT", "CVCUSDT", "TLMUSDT",
"DENTUSDT", "KEYUSDT", "CTKUSDT", "TOMOUSDT", "AKROUSDT", "BLZUSDT", "COMBOUSDT",
"FTMUSDT", "DGBUSDT", "REEFUSDT", "SLPUSDT", "IDEXUSDT", "LINAUSDT", "NEBLUSDT",
"RADUSDT", "BTSUSDT", "STMXUSDT", "MDTUSDT", "AMBUSDT", "GTCUSDT", "DARUSDT"]
def get(url):
try:
return json.load(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "curl/8"}), timeout=30))
except Exception:
return None
def universe(n):
info = get("https://fapi.binance.com/fapi/v1/exchangeInfo")
perps = {s["symbol"] for s in info["symbols"]
if s.get("contractType") == "PERPETUAL" and s.get("quoteAsset") == "USDT" and s.get("status") == "TRADING"}
tick = get("https://fapi.binance.com/fapi/v1/ticker/24hr")
vol = {t["symbol"]: float(t["quoteVolume"]) for t in tick if t["symbol"] in perps}
return sorted(vol, key=lambda s: -vol[s])[:n]
def klines(sym):
out, end = [], None
for _ in range(20):
u = f"https://fapi.binance.com/fapi/v1/klines?symbol={sym}&interval=1d&limit=1500"
if end:
u += f"&endTime={end}"
k = get(u)
if not k:
break
out = k + out
end = k[0][0] - 1
if len(k) < 1500:
break
time.sleep(0.15)
d = {int(r[0]): (float(r[4]), float(r[7])) for r in out} # close, quote-volume
days = np.array(sorted(d))
cl = np.array([d[t][0] for t in days]); qv = np.array([d[t][1] for t in days])
return days // DAY_MS, cl, qv
def funding(sym, start_ms):
out, st = [], start_ms
for _ in range(80):
f = get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={sym}&startTime={st}&limit=1000")
if not f:
break
out += f
st = f[-1]["fundingTime"] + 1
if len(f) < 1000:
break
time.sleep(0.12)
daily = {}
for r in out:
daily.setdefault(int(r["fundingTime"]) // DAY_MS, 0.0)
daily[int(r["fundingTime"]) // DAY_MS] += float(r["fundingRate"])
return daily
def main():
os.makedirs(OUT, exist_ok=True)
syms = sorted(set(universe(TOP_N)) | set(DEAD))
ok = 0
for sym in syms:
outp = f"{OUT}/{sym}.npz"
if os.path.exists(outp):
ok += 1; continue
kd, cl, qv = klines(sym)
if len(kd) < 200:
print(f" {sym}: short/none ({len(kd)}d), skip"); continue
fmap = funding(sym, int(kd.min()) * DAY_MS)
fund = np.array([fmap.get(int(d), 0.0) for d in kd])
np.savez(outp, day=kd, close=cl, qvol=qv, funding=fund)
alive = "DEAD" if qv[-30:].mean() == 0 else "live"
print(f" {sym}: {len(kd)}d ({kd.min()}..{kd.max()}) {alive}")
ok += 1
time.sleep(0.2)
print(f"DONE: {ok}/{len(syms)} -> {OUT}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Download daily OHLCV for the surfer universe — continuous front-month, BUDGET-CAPPED.
Re-confirms get_cost (free, no download) and ABORTS if over the cap before spending.
Saves raw DBN to data/surfer/ (gitignored). Reads DATABENTO_API_KEY from env (never printed).
"""
import datetime
import os
import sys
import databento as db
CAP_USD = 40.00 # hard ceiling on cumulative get_cost; covered by $125 free credits ($0 cash)
PER_ROOT_CAP = 10.00 # per-root sanity cap
DS = "GLBX.MDP3"
SCHEMA = "ohlcv-1d"
START = "2010-06-06"
END = datetime.date.today().isoformat() # fetch through today (dynamic) — was hardcoded; needed for daily forward-track
ROOTS = ["ES", "NQ", "YM", "RTY", "ZN", "ZB", "ZF", "ZT", "6E", "6J", "6B", "6A",
"6C", "GC", "SI", "HG", "CL", "NG", "RB", "ZC", "ZS", "ZW"]
# parent symbology = all outright expiries per root (fast; continuous chain resolution 504s).
# We build the continuous series ourselves (volume-roll + ratio-adjust) from these.
STYPE = "parent"
OUT_DIR = "data/surfer"
def main():
key = os.environ.get("DATABENTO_API_KEY")
if not key:
print("DATABENTO_API_KEY not set"); return 2
client = db.Historical(key)
global END
try: # clamp END to the dataset's available end (GLBX lags ~1-3d)
rng = client.metadata.get_dataset_range(dataset=DS)
avail = (rng.get("end") or "")[:10]
if avail:
END = min(END, avail)
except Exception:
pass
os.makedirs(OUT_DIR, exist_ok=True)
print(f"fetching {SCHEMA} per-root ({len(ROOTS)} roots, {STYPE}) {START}..{END}; "
f"per-root get_cost gate (≤${PER_ROOT_CAP}), cumulative cap ${CAP_USD}")
import time
total_recs = 0
spent = 0.0
failed = []
for r in ROOTS:
sym = r + ".FUT"
out_r = f"{OUT_DIR}/{r}.dbn"
# per-root cost gate (free, no download)
try:
c_r = client.metadata.get_cost(dataset=DS, symbols=[sym], schema=SCHEMA,
start=START, end=END, stype_in=STYPE)
except Exception as e:
print(f" {r}: get_cost failed ({type(e).__name__}); skipping"); failed.append(r); continue
if c_r > PER_ROOT_CAP or spent + c_r > CAP_USD:
print(f" {r}: cost ${c_r:.4f} would breach cap (spent ${spent:.2f}) — SKIP"); failed.append(r); continue
ok = False
for attempt in range(1, 4):
try:
data = client.timeseries.get_range(dataset=DS, symbols=[sym], schema=SCHEMA,
start=START, end=END, stype_in=STYPE)
data.to_file(out_r)
n = sum(1 for _ in data)
total_recs += n; spent += c_r
print(f" {r}: ${c_r:.4f} {n:,} recs -> {out_r} (cum ${spent:.2f})")
ok = True
break
except Exception as e:
print(f" {r}: attempt {attempt} {type(e).__name__}; retry in 3s")
time.sleep(3)
if not ok:
failed.append(r)
print(f"DONE: {total_recs:,} records, get_cost-cum ${spent:.2f}, "
f"{len(ROOTS)-len(failed)}/{len(ROOTS)} roots ok"
+ (f"; FAILED/SKIPPED: {failed}" if failed else ""))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Fetch Nasdaq earnings calendar (date + symbol + surprise%) for all weekdays in the DBEQ range,
for a proper PEAD test. Free, no key. Cached per date; gentle pacing + backoff.
"""
import datetime
import json
import os
import time
import urllib.request
OUT = "data/surfer/earnings"
START = datetime.date(2023, 3, 28)
END = datetime.date(2026, 5, 31)
def get(u):
for a in range(4):
try:
req = urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0", "Accept": "application/json"})
return json.loads(urllib.request.urlopen(req, timeout=25).read())
except Exception:
if a == 3:
return None
time.sleep(4 * (a + 1))
return None
def main():
os.makedirs(OUT, exist_ok=True)
d = START
n = fail = 0
while d <= END:
if d.weekday() < 5:
f = f"{OUT}/{d}.json"
if not os.path.exists(f):
r = get(f"https://api.nasdaq.com/api/calendar/earnings?date={d}")
if r is None:
fail += 1
else:
rows = (r.get("data") or {}).get("rows") or []
json.dump([(x.get("symbol"), x.get("surprise")) for x in rows], open(f, "w"))
n += 1
time.sleep(1.3)
d += datetime.timedelta(days=1)
print(f"DONE: fetched {n} new days, {fail} failed; cache dir {OUT}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Fetch all US equities daily OHLCV (DBEQ.BASIC) for the small/mid-cap factor test.
Budget-capped, get_cost-gated. Saves DBN to data/surfer/dbeq_ohlcv1d.dbn.
"""
import os
import sys
import databento as db
CAP = 70.0
DS, SCH = "DBEQ.BASIC", "ohlcv-1d"
START, END = "2023-03-28", "2026-06-05"
OUT = "data/surfer/dbeq_ohlcv1d.dbn"
def main():
c = db.Historical(os.environ["DATABENTO_API_KEY"])
if os.path.exists(OUT):
print("already downloaded"); return 0
cost = c.metadata.get_cost(dataset=DS, symbols=["ALL_SYMBOLS"], schema=SCH, start=START, end=END)
print(f"get_cost=${cost:.2f} cap=${CAP:.2f}")
if cost > CAP:
print("ABORT over cap"); return 1
data = c.timeseries.get_range(dataset=DS, symbols=["ALL_SYMBOLS"], schema=SCH, start=START, end=END)
os.makedirs("data/surfer", exist_ok=True)
data.to_file(OUT)
print(f"saved {OUT}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Fetch full-history ES front-month OHLCV-1m (continuous .c.0), year-chunked, BUDGET-CAPPED.
get_cost-gated (aborts over cap, no download); year chunks with quarter fallback on 504.
Saves per-chunk DBN to data/surfer/es1m/ (gitignored). Key from env, never printed.
"""
import os
import sys
import time
import databento as db
CAP_USD = 25.00
DS = "GLBX.MDP3"
SCHEMA = "ohlcv-1m"
SYM = "ES.c.0"
STYPE = "continuous"
OUT_DIR = "data/surfer/es1m"
def fetch(client, start, end, out):
data = client.timeseries.get_range(dataset=DS, symbols=[SYM], schema=SCHEMA,
start=start, end=end, stype_in=STYPE)
data.to_file(out)
return sum(1 for _ in data)
def main():
key = os.environ.get("DATABENTO_API_KEY")
if not key:
print("DATABENTO_API_KEY not set"); return 2
client = db.Historical(key)
cost = client.metadata.get_cost(dataset=DS, symbols=[SYM], schema=SCHEMA,
start="2010-06-06", end="2026-06-05", stype_in=STYPE)
print(f"aggregate get_cost=${cost:.4f} cap=${CAP_USD:.2f}")
if cost > CAP_USD:
print("ABORT: over cap — nothing downloaded."); return 1
os.makedirs(OUT_DIR, exist_ok=True)
total = 0
for year in range(2010, 2027):
y0, y1 = f"{year}-01-01", f"{year+1}-01-01"
if year == 2010:
y0 = "2010-06-06"
if year == 2026:
y1 = "2026-06-05"
out = f"{OUT_DIR}/ES_{year}.dbn"
if os.path.exists(out):
print(f" {year}: exists, skip"); continue
ok = False
for attempt in range(1, 3):
try:
n = fetch(client, y0, y1, out); total += n
print(f" {year}: {n:,} recs -> {out}")
ok = True; break
except Exception as e:
print(f" {year}: attempt {attempt} {type(e).__name__}; retry")
time.sleep(3)
if not ok: # fall back to quarter chunks
print(f" {year}: year failed → quarter fallback")
for q, (m0, m1) in enumerate([("01-01", "04-01"), ("04-01", "07-01"),
("07-01", "10-01"), ("10-01", "12-31")], 1):
qs, qe = f"{year}-{m0}", f"{year}-{m1}"
if year == 2010 and q == 1:
qs = "2010-06-06"
if year == 2026 and q >= 3:
continue
qout = f"{OUT_DIR}/ES_{year}_q{q}.dbn"
if os.path.exists(qout):
continue
try:
n = fetch(client, qs, qe, qout); total += n
print(f" {year} q{q}: {n:,} -> {qout}")
except Exception as e:
print(f" {year} q{q}: FAILED {type(e).__name__}")
time.sleep(1)
print(f"DONE: {total:,} records into {OUT_DIR}/")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Expand the daily futures universe (16y ohlcv-1d, parent) for a diversified multi-asset test.
Budget-capped, get_cost-gated. Saves to data/surfer/<root>.dbn alongside the existing 22 roots.
"""
import os
import sys
import databento as db
CAP = 20.0
DS = "GLBX.MDP3"
START, END = "2010-06-06", "2026-06-05"
ADD = ["6S", "6N", "6M", "HO", "PL", "PA", "UB", "ZL", "ZM", "ZO", "KE", "ZR", "LE", "HE", "GF", "EMD", "NKD"]
def main():
c = db.Historical(os.environ["DATABENTO_API_KEY"])
todo = [r for r in ADD if not os.path.exists(f"data/surfer/{r}.dbn")]
cost = sum(c.metadata.get_cost(dataset=DS, symbols=[r + ".FUT"], schema="ohlcv-1d",
start=START, end=END, stype_in="parent") for r in todo)
print(f"to fetch: {todo}\naggregate get_cost=${cost:.2f} cap=${CAP:.2f}")
if cost > CAP:
print("ABORT over cap"); return 1
for r in todo:
data = c.timeseries.get_range(dataset=DS, symbols=[r + ".FUT"], schema="ohlcv-1d",
start=START, end=END, stype_in="parent")
data.to_file(f"data/surfer/{r}.dbn")
print(f" {r}: saved")
print(f"DONE: {len(todo)} roots")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Fetch XSP (mini-SPX) options daily OHLCV for the equity-index VRP test. HARD-CAPPED.
get_cost-gated: prints the exact cost and ABORTS if above CAP — no surprise charges.
Single dataset/schema/symbol/window. ohlcv-1d only (NOT definition).
"""
import os
import sys
import databento as db
CAP = 70.00 # hard cap — abort if cost exceeds this
DS, SCH, SYM = "OPRA.PILLAR", "ohlcv-1d", "XSP.OPT"
START, END = "2013-04-01", "2026-06-05"
OUT = "data/surfer/xsp_ohlcv1d.dbn"
def main():
c = db.Historical(os.environ["DATABENTO_API_KEY"])
if os.path.exists(OUT):
print(f"already downloaded: {OUT}"); return 0
cost = c.metadata.get_cost(dataset=DS, symbols=[SYM], schema=SCH, start=START, end=END, stype_in="parent")
gb = c.metadata.get_billable_size(dataset=DS, symbols=[SYM], schema=SCH, start=START, end=END, stype_in="parent") / 1e9
print(f"EXACT get_cost = ${cost:.2f} ({gb:.2f} GB) CAP = ${CAP:.2f}")
if cost > CAP:
print(f"ABORT: ${cost:.2f} exceeds cap ${CAP:.2f} — NOTHING downloaded.")
return 1
print(f"under cap by ${CAP - cost:.2f} -> downloading {SYM} {SCH} year-chunked (same total cost)")
os.makedirs("data/surfer/xsp", exist_ok=True)
for yr in range(2013, 2027):
ys = f"{yr}-01-01" if yr > 2013 else "2013-04-01"
ye = f"{yr+1}-01-01" if yr < 2026 else "2026-06-05"
out = f"data/surfer/xsp/xsp_{yr}.dbn"
if os.path.exists(out):
print(f" {yr}: exists"); continue
for attempt in range(3):
try:
d = c.timeseries.get_range(dataset=DS, symbols=[SYM], schema=SCH, start=ys, end=ye, stype_in="parent")
d.to_file(out)
print(f" {yr}: saved ({os.path.getsize(out)/1e6:.1f} MB)")
break
except Exception as e:
print(f" {yr}: attempt {attempt+1} {type(e).__name__}; retry")
print(f"DONE -> data/surfer/xsp/")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Fetch historical funding (Binance + Bybit) for liquid coins on both, to BACKTEST the cross-venue
funding arb instead of waiting for live paper-forward. Aggregates 8h settlements to daily per venue.
Cached. Saves data/surfer/xvenue/panel.json = {coin: {date: [binance_daily, bybit_daily]}}.
"""
import datetime
import json
import os
import time
import urllib.request
OUT = "data/surfer/xvenue"
NCOINS = 60
DAYS = 330
def get(u, post=None):
data = json.dumps(post).encode() if post else None
h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
for a in range(4):
try:
return json.loads(urllib.request.urlopen(urllib.request.Request(u, data=data, headers=h), timeout=25).read())
except Exception:
if a == 3:
return None
time.sleep(3 * (a + 1))
return None
def base(s):
return s[:-4] if s.endswith("USDT") else s
def day_of(ms):
return datetime.datetime.utcfromtimestamp(int(ms) / 1000).strftime("%Y-%m-%d")
def binance_hist(coin):
r = get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={coin}USDT&limit=1000")
d = {}
for x in (r or []):
d.setdefault(day_of(x["fundingTime"]), 0.0)
d[day_of(x["fundingTime"])] += float(x["fundingRate"])
return d
def bybit_hist(coin):
out, end = {}, None
for _ in range(6):
u = f"https://api.bybit.com/v5/market/funding/history?category=linear&symbol={coin}USDT&limit=200"
if end:
u += f"&endTime={end}"
r = get(u)
lst = ((r or {}).get("result") or {}).get("list") or []
if not lst:
break
for x in lst:
t = x["fundingRateTimestamp"]
out.setdefault(day_of(t), 0.0)
out[day_of(t)] += float(x["fundingRate"])
end = int(lst[-1]["fundingRateTimestamp"]) - 1
time.sleep(0.3)
return out
def main():
os.makedirs(OUT, exist_ok=True)
binv = {base(x["symbol"]): float(x["quoteVolume"]) for x in get("https://fapi.binance.com/fapi/v1/ticker/24hr") if x["symbol"].endswith("USDT")}
byr = get("https://api.bybit.com/v5/market/tickers?category=linear")["result"]["list"]
bybv = {base(x["symbol"]): float(x.get("turnover24h", 0)) for x in byr if x["symbol"].endswith("USDT")}
coins = sorted([c for c in binv if c in bybv and binv[c] > 10e6 and bybv[c] > 10e6], key=lambda c: -binv[c])[:NCOINS]
print(f"universe: {len(coins)} coins on both venues >$10M/day")
panel = {}
for i, c in enumerate(coins):
cf = f"{OUT}/{c}.json"
if os.path.exists(cf):
panel[c] = json.load(open(cf)); continue
bn = binance_hist(c); by = bybit_hist(c)
days = sorted(set(bn) & set(by))
panel[c] = {d: [bn[d], by[d]] for d in days}
json.dump(panel[c], open(cf, "w"))
if i % 10 == 0:
print(f" {i+1}/{len(coins)} {c}: {len(panel[c])} common days")
time.sleep(0.4)
json.dump(panel, open(f"{OUT}/panel.json", "w"))
print(f"DONE: {len(panel)} coins -> {OUT}/panel.json")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Deep multi-venue funding history (Binance + OKX + Hyperliquid, ~1yr) for a confident cross-venue
backtest. Binance & HL are deep (~330d); OKX ~3mo. Aggregate to daily per venue. Cache per coin.
Saves data/surfer/xvenue/panel2.json = {coin: {date: {bn, okx, hl}}}.
"""
import datetime
import json
import os
import time
import urllib.request
OUT = "data/surfer/xvenue2"
NCOINS = 35
DAYS = 330
def get(u, post=None):
data = json.dumps(post).encode() if post else None
h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
if post:
h["Content-Type"] = "application/json"
for a in range(4):
try:
return json.loads(urllib.request.urlopen(urllib.request.Request(u, data=data, headers=h), timeout=25).read())
except Exception:
if a == 3:
return None
time.sleep(3 * (a + 1))
return None
def base(s):
return s[:-4] if s.endswith("USDT") else s
def day_of(ms):
return datetime.datetime.utcfromtimestamp(int(ms) / 1000).strftime("%Y-%m-%d")
def binance_hist(coin, now_ms):
out, start = {}, now_ms - DAYS * 86400000
for _ in range(8): # paginate back full DAYS window
r = get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={coin}USDT&startTime={start}&limit=1000")
if not r:
break
for x in r:
k = day_of(x["fundingTime"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
last = int(r[-1]["fundingTime"])
if last <= start or len(r) < 2:
break
start = last + 1
return out
def okx_hist(coin):
out, after = {}, None
for _ in range(6):
u = f"https://www.okx.com/api/v5/public/funding-rate-history?instId={coin}-USDT-SWAP&limit=100"
if after:
u += f"&after={after}"
r = get(u); data = (r or {}).get("data") or []
if not data:
break
for x in data:
k = day_of(x["fundingTime"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
after = data[-1]["fundingTime"]; time.sleep(0.2)
return out
def hl_hist(coin, now_ms):
out, start = {}, now_ms - DAYS * 86400000
for _ in range(30):
r = get("https://api.hyperliquid.xyz/info", post={"type": "fundingHistory", "coin": coin, "startTime": start})
if not r:
break
for x in r:
k = day_of(x["time"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
last = int(r[-1]["time"])
if last <= start or len(r) < 2:
break
start = last + 1
return out
def main():
os.makedirs(OUT, exist_ok=True)
now_ms = int(time.time() * 1000)
binv = {base(x["symbol"]): float(x["quoteVolume"]) for x in get("https://fapi.binance.com/fapi/v1/ticker/24hr") if x["symbol"].endswith("USDT")}
hlmeta = get("https://api.hyperliquid.xyz/info", post={"type": "metaAndAssetCtxs"})
hlcoins = {u["name"] for u in hlmeta[0]["universe"]}
coins = sorted([c for c in binv if c in hlcoins and binv[c] > 10e6], key=lambda c: -binv[c])[:NCOINS]
print(f"universe: {len(coins)} coins (Binance>$10M & on Hyperliquid)")
panel = {}
for i, c in enumerate(coins):
cf = f"{OUT}/{c}.json"
if os.path.exists(cf):
panel[c] = json.load(open(cf)); continue
bn = binance_hist(c, now_ms); okx = okx_hist(c); hl = hl_hist(c, now_ms)
days = sorted(set(bn) | set(okx) | set(hl))
rec = {}
for d in days:
v = {}
if d in bn:
v["bn"] = bn[d]
if d in okx:
v["okx"] = okx[d]
if d in hl:
v["hl"] = hl[d]
if len(v) >= 2:
rec[d] = v
panel[c] = rec
json.dump(rec, open(cf, "w"))
print(f" {i+1}/{len(coins)} {c}: {len(rec)} days w/ >=2 venues (bn{len(bn)} okx{len(okx)} hl{len(hl)})")
time.sleep(0.3)
json.dump(panel, open(f"{OUT}/panel2.json", "w"))
print(f"DONE: {len(panel)} coins -> {OUT}/panel2.json")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Surfer Phase 0 — diversified-trend FLOOR + validation, on the GPU (PyTorch).
Loads per-root parent OHLCV-1d (data/surfer/*.dbn), builds ratio-adjusted continuous
front-month series (volume-roll), then runs the floor + the anti-overfit validation
ENTIRELY on the GPU (torch.cuda): TSMOM(1/3/12mo) → inverse-vol size → vol-target →
net-of-cost daily portfolio returns; CPCV(5th-pct OOS Sharpe), Deflated Sharpe, PBO,
IS↔OOS rank-consistency. Prints the PASS/FAIL verdict.
Data-loading/continuous-assembly is CPU (I/O staging, like mapped-pinned upload); ALL
strategy + statistics compute is on the GPU. Verdict gates per the spec §5.
"""
import glob
import math
from itertools import combinations
import numpy as np
import torch
DEV = "cuda" if torch.cuda.is_available() else None
TICK_NA = float("nan")
# ---------- data loading + continuous assembly (CPU staging) ----------
_MONTHS = "FGHJKMNQUVXZ"
def load_continuous(path):
"""Roll-neutralized continuous daily series from a root's parent OHLCV-1d.
Front = max-volume OUTRIGHT per day; daily return only when the front contract is
unchanged (roll-day returns ZEROED — no ratio-adjust, no fabricated roll jumps).
Returns (day_int[N], adj_close[N]) where adj_close = exp(cumsum(clean_return))."""
import re
import databento as db
root = path.split("/")[-1][:-4]
pat = re.compile("^" + re.escape(root) + "[" + _MONTHS + r"]\d{1,2}$") # outrights only (e.g. RBZ5)
df = db.DBNStore.from_file(path).to_df().reset_index()
df = df[df["close"] > 0].copy()
df = df[df["symbol"].astype(str).str.match(pat)]
if df.empty:
raise ValueError("no outright contracts after filter")
df["day"] = (df["ts_event"].astype("int64") // (86_400 * 10**9))
# (day, instrument) -> close lookup, for proper roll-day return of the HELD contract
cl = {(int(d), int(i)): float(c)
for d, i, c in zip(df["day"], df["instrument_id"], df["close"])}
idx = df.groupby("day")["volume"].idxmax()
f = df.loc[idx, ["day", "instrument_id", "close"]].sort_values("day")
days = f["day"].to_numpy(np.int64)
inst = f["instrument_id"].to_numpy(np.int64)
close = f["close"].to_numpy(np.float64)
lr = np.zeros(len(days))
for t in range(1, len(days)):
a, b = int(inst[t - 1]), int(inst[t])
if a == b:
lr[t] = np.log(close[t] / close[t - 1])
else: # roll: use the HELD (old front 'a') contract's own return through the roll day
ca_t = cl.get((int(days[t]), a))
if ca_t is not None and ca_t > 0:
lr[t] = np.log(ca_t / close[t - 1])
# else: 'a' not trading on day t → leave 0 (rare)
return days, np.exp(np.cumsum(lr))
def build_panel():
paths = sorted(glob.glob("data/surfer/*.dbn"))
series = {}
for p in paths:
root = p.split("/")[-1][:-4]
try:
d, c = load_continuous(p)
if len(d) > 300:
series[root] = (d, c)
except Exception as e:
print(f" skip {root}: {type(e).__name__} {e}")
roots = sorted(series)
alldays = sorted(set().union(*[set(series[r][0].tolist()) for r in roots]))
didx = {d: i for i, d in enumerate(alldays)}
C = np.full((len(alldays), len(roots)), np.nan)
for j, r in enumerate(roots):
d, c = series[r]
for dd, cc in zip(d.tolist(), c.tolist()):
C[didx[dd], j] = cc
return roots, np.array(alldays), C
# ---------- floor + validation (GPU) ----------
def sharpe(x): # x: [..., T] torch; Sharpe over last dim, NaN-aware
m = torch.nanmean(x, dim=-1)
v = torch.nanmean((x - m.unsqueeze(-1)) ** 2, dim=-1).clamp_min(1e-18).sqrt()
return m / v * math.sqrt(252)
def run(roots, days, C):
Ct = torch.tensor(C, device=DEV, dtype=torch.float64) # [T, N]
logC = torch.log(Ct)
R = torch.zeros_like(logC); R[1:] = logC[1:] - logC[:-1] # daily log returns
R = torch.nan_to_num(R, nan=0.0)
T, N = R.shape
# EWMA vol (per root), halflife 21d
a = 1 - math.exp(math.log(0.5) / 21)
v = torch.zeros_like(R); v[0] = R[0] ** 2
for t in range(1, T):
v[t] = a * R[t] ** 2 + (1 - a) * v[t - 1]
sig_vol = (v * 252).clamp_min(1e-12).sqrt() # annualized vol [T,N]
# CONTINUOUS vol-normalized TSMOM (Baz et al / AQR): tanh of the trend t-stat,
# averaged over 1/3/12mo lookbacks (252 skips last 21d). Strength in [-1,1] →
# multiplied by inverse-vol sizing below (no double vol-counting; sign-floor superseded).
import os
mode = os.environ.get("FLOOR_SIGNAL", "sign") # "sign" (canonical MOP/AQR) or "cont"
s = torch.zeros_like(R)
for L, skip in [(21, 0), (63, 0), (252, 21)]:
raw = torch.zeros_like(R)
for t in range(L + skip, T):
e = t - skip
raw[t] = logC[e] - logC[e - L] # trend log-return over L
if mode == "cont":
norm = raw / (sig_vol * math.sqrt(L / 252.0)).clamp_min(1e-6) # ≈ trend Sharpe
s = s + torch.tanh(norm)
else:
s = s + torch.sign(raw)
s = (s / 3.0)
s = torch.nan_to_num(s, nan=0.0)
# inverse-vol target weights, vol-targeted to 10% annual
# No-trade band on the continuous signal (turnover control; standard CTA practice).
# Pre-registered band = 0.10 in [-1,1] signal units: hold position until target moves > band.
band = 0.10
held = torch.zeros_like(s)
held[0] = s[0]
for t in range(1, T):
move = (s[t] - held[t - 1]).abs() > band
held[t] = torch.where(move, s[t], held[t - 1])
risk_budget = 0.10 / math.sqrt(N)
w = held * (risk_budget / sig_vol.clamp_min(1e-6))
w = torch.nan_to_num(w, nan=0.0)
# daily portfolio return (lag weights), net of cost
port = (w[:-1] * R[1:]).sum(dim=1) # [T-1]
turn = (w[1:] - w[:-1]).abs().sum(dim=1)
cost1 = torch.nan_to_num(turn * (2.0 / 1e4), nan=0.0) # ~2bp round-trip on weight turnover
def voltarget(x): # 10% annual, trailing-63d realized
sc = torch.ones_like(x)
for t in range(63, len(x)):
rv = x[t-63:t].std() * math.sqrt(252)
sc[t] = (0.10 / rv.clamp_min(1e-6))
return x * sc.clamp(0, 5)
net = voltarget(port - cost1)
net2x = voltarget(port - 2 * cost1) # SV cost stress (2x)
full_sr = float(sharpe(net))
full_sr_2x = float(sharpe(net2x))
# CPCV: 10 blocks, leave-2-out test → 45 paths
n = len(net); nb = 10; k = 2
blocks = [torch.arange(i*n//nb, (i+1)*n//nb, device=DEV) for i in range(nb)]
oos_sr = []
for combo in combinations(range(nb), k):
idx = torch.cat([blocks[b] for b in combo])
oos_sr.append(float(sharpe(net[idx])))
oos_sr = np.array(oos_sr)
oos_5pct = float(np.percentile(oos_sr, 5))
# IS(<2024)/OOS(>=2024) split by day epoch (days are UTC-day ints)
split_day = 19723 # 2024-01-01 in days since epoch
is_mask = days[1:] < split_day
oos_mask = ~is_mask
sr_is = float(sharpe(net[torch.tensor(is_mask, device=DEV)]))
sr_oos = float(sharpe(net[torch.tensor(oos_mask, device=DEV)])) if oos_mask.sum() > 30 else float("nan")
# Deflated Sharpe — HONEST n_trials = trend-floor variants tried (sign + continuous ≈ 3),
# NOT the 64 unrelated RL-microstructure commits.
n_trials = 3
sr_daily = full_sr / math.sqrt(252)
sr0 = (1/math.sqrt(n)) * ((1-0.5772)*_ppf(1-1.0/n_trials) + 0.5772*_ppf(1-1.0/(n_trials*math.e)))
dsr = _ncdf((sr_daily - sr0) * math.sqrt(n - 1))
med = float(np.median(oos_sr))
print("\n================ SURFER PHASE 0 — FLOOR VERDICT (GPU, continuous TSMOM + no-trade band) ================")
print(f"device={DEV} roots={len(roots)} days={n} span≈{n/252:.1f}y")
print(f"full-sample Sharpe (net) = {full_sr:+.3f} (2x-cost: {full_sr_2x:+.3f})")
print(f"IS(<2024) / OOS(>=2024) Sharpe = {sr_is:+.3f} / {sr_oos:+.3f}")
print(f"CPCV 45 paths: median={med:+.3f} 5th-pct={oos_5pct:+.3f}")
print(f"Deflated Sharpe (n_trials={n_trials}) = {dsr:.3f}")
print("---- DEVELOP-grade (is there a real edge worth building on?) ----")
d1, d2, d3 = med > 0, (sr_is > 0 and sr_oos > 0), full_sr_2x > 0
print(f" D1 CPCV median > 0 : {'PASS' if d1 else 'FAIL'} ({med:+.3f})")
print(f" D2 IS & OOS both positive : {'PASS' if d2 else 'FAIL'} ({sr_is:+.2f}/{sr_oos:+.2f})")
print(f" D3 survives 2x cost : {'PASS' if d3 else 'FAIL'} ({full_sr_2x:+.3f})")
print("---- DEPLOY-grade (bet real money?) ----")
p1, p3 = oos_5pct > 0, dsr > 0.95
print(f" P1 CPCV 5th-pct OOS > 0 : {'PASS' if p1 else 'FAIL'} ({oos_5pct:+.3f})")
print(f" P3 Deflated Sharpe > 0.95 : {'PASS' if p3 else 'FAIL'} ({dsr:.3f})")
print(f"==> DEVELOP: {'PASS → real edge; build the ML regime overlay (must beat this floor OOS)' if (d1 and d2 and d3) else 'FAIL → no developable edge'}")
print(f"==> DEPLOY : {'PASS' if (p1 and p3) else 'NOT YET (expected for a bare floor; ML overlay + tuning target this)'}")
def _ppf(p): # inverse normal cdf (Acklam approx)
from statistics import NormalDist
return NormalDist().inv_cdf(p)
def _ncdf(x):
from statistics import NormalDist
return NormalDist().cdf(x)
def main():
if DEV is None:
raise SystemExit("CUDA not available to torch")
roots, days, C = build_panel()
if not roots:
raise SystemExit("no data in data/surfer/*.dbn yet")
run(roots, days, C)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Phase A (non-price) — Fear & Greed sentiment gate (market-timing signal).
Does crypto sentiment (alternative.me Fear & Greed, daily since 2018) predict forward
crypto-market returns? Contrarian hypothesis: extreme fear -> buy, extreme greed -> sell.
Market = equal-weight return over the crypto_pit universe. Tests contrarian-level,
extreme-only, and sentiment-change signals -> forward 1/7/30d returns, IC + timing-strategy
Sharpe (net 5bp), per-year, OOS. A market-timing sleeve is directional -> diversifies the
market-neutral momentum book if it works.
"""
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
from signal_sweep import sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY = 86_400
def fng():
cache = "data/surfer/fng.json"
if os.path.exists(cache):
return json.load(open(cache))
d = json.load(urllib.request.urlopen(urllib.request.Request(
"https://api.alternative.me/fng/?limit=0&format=json", headers={"User-Agent": "curl/8"}), timeout=30))["data"]
out = {int(r["timestamp"]) // DAY: float(r["value"]) for r in d}
os.makedirs("data/surfer", exist_ok=True); json.dump(out, open(cache, "w"))
return {int(k): v for k, v in out.items()}
def main():
fg = fng()
syms, days, close, qv, fund = pit_sweep.load()
R = np.zeros_like(close); R[1:] = np.log(close)[1:] - np.log(close)[:-1]; R = np.where(np.isfinite(R), R, 0.0)
mkt = {int(days[t]): float(np.nanmean(R[t][np.isfinite(close[t])])) for t in range(len(days))} # EW market ret day t
common = sorted(set(fg) & set(mkt))
f = np.array([fg[d] for d in common]); r = np.array([mkt[d] for d in common])
cum = np.concatenate([[0], np.cumsum(r)])
year = (1970 + np.array(common) / 365.25).astype(int)
n = len(common); split = int(0.7 * n)
def fwd(h):
out = np.full(n, np.nan)
out[:n - h] = cum[h + 1:n + 1] - cum[1:n - h + 1] # sum of mkt returns t+1..t+h
return out
sigs = {
"contrarian_level": -(f - 50),
"extreme_only": np.where(f < 25, 1.0, np.where(f > 75, -1.0, 0.0)),
"sentiment_chg": np.concatenate([[np.nan] * 7, f[7:] - f[:-7]]), # rising sentiment (momentum)
}
print(f"\n===== FEAR&GREED GATE — market-timing, {n} days ({common[0]}..{common[-1]}), cost 5bp =====")
print(f"{'signal':>16} " + " ".join(f"h={h}:IC" for h in [1, 7, 30]) + " timing(h=7): full/IS/OOS/peryr")
for nm, s in sigs.items():
ics = []
for h in [1, 7, 30]:
fw = fwd(h); m = np.isfinite(s) & np.isfinite(fw)
ics.append(float(np.corrcoef(s[m], fw[m])[0, 1]) if m.sum() > 100 else float("nan"))
# timing strategy at h=7 (weekly hold): position = sign(s) lagged, daily mkt return, ~weekly turnover
pos = np.sign(s); pnl = np.full(n, np.nan)
pnl[1:] = pos[:-1] * r[1:]
turn = np.abs(np.diff(np.nan_to_num(pos)))
pnl[1:] -= turn * 5e-4
T = lambda x: torch.tensor(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
full = sharpe_t(T(pnl)); isr = sharpe_t(T(pnl[:split])); oos = sharpe_t(T(pnl[split:]))
py = " ".join(f"{y}:{sharpe_t(T(pnl[year==y])):+.1f}" for y in range(2019, 2027) if (year == y).sum() > 60)
print(f"{nm:>16} " + " ".join(f"{ic:+.3f}" for ic in ics) + f" {full:+.2f}/{isr:+.2f}/{oos:+.2f} {py}")
print("\nVERDICT: contrarian IC>0 (fear->up) + timing OOS Sharpe>0 net = real sentiment signal (directional sleeve).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# fxhnt combined-book daily forward-validation.
# Refresh the data, then book the new day's paper-NAV into the frozen forward track.
# - crypto X-sec momentum sleeve (alpha): free Binance fetch, advances daily
# - futures trend sleeve (hedge): Databento ohlcv, cost-capped in fetch_daily.py
# - fxhnt forward-track: books only days AFTER inception (genuine out-of-sample-in-time)
set -u
cd /home/jgrusewski/Work/foxhunt || exit 1
[ -f "$HOME/.secrets" ] && . "$HOME/.secrets" # DATABENTO_API_KEY (gitignored, never printed)
echo "===== $(date -u '+%Y-%m-%d %H:%M UTC') ====="
python3 scripts/surfer/fetch_crypto_pit.py 2>&1 | tail -1 # daily, free (Binance) — alpha sleeve
if [ "$(date -u +%u)" -eq 7 ]; then # futures only on Sundays — cost control
echo "[sunday] refreshing futures (Databento)"
python3 scripts/surfer/fetch_daily.py 2>&1 | tail -2
fi
/home/jgrusewski/.local/bin/fxhnt forward-track 2>&1 | grep -vE "BentoWarning|Warning|RuntimeWarning|divide|invalid value|stddev"

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Growth-discipline multi-premia backtest: high-equity (QQQ) core + trend crisis-alpha sleeve +
diversified book, mechanically rebalanced, optional cheap-futures leverage. vs QQQ / book / 70-30.
Also tests adding VRP (PUTW put-write) as a sleeve. Honest: execution-alpha (behavior gap, tax-loss
harvest) is real but counterfactual/account-dependent -> NOT in the backtest; noted as +1-3%/yr on top.
python3 growth_discipline_backtest.py [--fin 0.03]
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import book_series # noqa: E402
def yh(s):
try:
r = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{s}?interval=1d&range=25y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
a = r["indicators"].get("adjclose", [{}])[0].get("adjclose") or r["indicators"]["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(r["timestamp"], a) if c}
except Exception:
return {}
FIN = float(sys.argv[sys.argv.index("--fin") + 1]) if "--fin" in sys.argv else 0.03
def build(syms):
data = {s: yh(s) for s in syms}
common = sorted(set.intersection(*[set(data[s]) for s in syms]))
yrs = (datetime.date.fromisoformat(common[-1]) - datetime.date.fromisoformat(common[0])).days / 365.25
T = len(common)
P = {s: np.array([data[s][d] for d in common]) for s in syms}
R = {s: np.concatenate([[0], P[s][1:] / P[s][:-1] - 1]) for s in syms}
return common, yrs, T, P, R
def trend_sleeve(P, R, syms, T):
"""12-1 TS-momentum long/short on syms, equal-risk = managed-futures proxy."""
Pm = np.column_stack([P[s] for s in syms]); Rm = np.column_stack([R[s] for s in syms])
lc = np.log(Pm); sg = np.zeros((T, len(syms))); sg[252:] = np.sign(lc[252:] - lc[:-252])
vol = np.full((T, len(syms)), np.nan)
for t in range(63, T):
vol[t] = Rm[t - 63:t].std(0)
iv = 1 / np.where(vol > 0, vol, np.nan); w = np.nan_to_num(sg * iv)
g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
tr = np.zeros(T); tr[1:] = np.sum(w[:-1] * Rm[1:], axis=1); return tr
def report(title, cands, yrs):
def stats(r):
r = np.nan_to_num(r); eq = np.cumprod(1 + r); c = eq[-1] ** (1 / yrs) - 1
v = r.std() * math.sqrt(252); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
sh = (r.mean() * 252 - FIN) / v if v > 0 else 0
return c, v, sh, dd
def fv(c, P0=360000, pmt=8000, years=20):
rm = c / 12; N = years * 12; gg = (1 + rm) ** N; return P0 * gg + pmt * ((gg - 1) / rm)
print(f"\n=== {title} ===")
print(f" {'strategy':>30} | CAGR | vol | Shrp* | maxDD | $360k+$8k/mo 20y")
for nm, r in cands:
c, v, sh, dd = stats(r)
print(f" {nm:>30} | {100*c:>4.1f}% | {100*v:>3.0f}% | {sh:>+4.2f} | {100*dd:>+4.0f}% | ${fv(c):>13,.0f}")
def main():
# A) long history 2006-2026
common, yrs, T, P, R = build(["QQQ", "SPY", "IEF", "GLD", "DBC"])
tr = trend_sleeve(P, R, ["SPY", "IEF", "GLD", "DBC"], T)
R4 = np.column_stack([R[s] for s in ["SPY", "IEF", "GLD", "DBC"]])
book, _, _ = book_series(np.column_stack([R4, tr]))
qqq = R["QQQ"]
def lev(r, L): return L * r - (L - 1) * FIN / 252
gd70 = 0.70 * qqq + 0.15 * tr + 0.15 * book
gd60 = 0.60 * qqq + 0.20 * tr + 0.20 * book
report(f"A) Long history {common[0]}..{common[-1]} ({yrs:.0f}y), fin {100*FIN:.0f}%", [
("QQQ buy-hold", qqq),
("book (diversified)", book),
("70/30 QQQ+book", 0.7 * qqq + 0.3 * book),
("GD-70 (70Q/15trend/15book)", gd70),
("GD-60 (60Q/20trend/20book)", gd60),
("GD-70 @1.2x cheap lev", lev(gd70, 1.2)),
("GD-70 @1.3x cheap lev", lev(gd70, 1.3)),
], yrs)
print(" *Sharpe = excess over financing. (Execution-alpha — behavior gap ~1-2%/yr, tax-loss")
print(" harvest ~0.5-1.5%/yr taxable — is REAL but counterfactual, NOT in these numbers: add on top.)")
# B) recent window incl VRP (PUTW) + real managed-futures (DBMF)
common2, yrs2, T2, P2, R2 = build(["QQQ", "SPY", "IEF", "GLD", "DBC", "PUTW", "DBMF"])
tr2 = trend_sleeve(P2, R2, ["SPY", "IEF", "GLD", "DBC"], T2)
book2, _, _ = book_series(np.column_stack([np.column_stack([R2[s] for s in ["SPY", "IEF", "GLD", "DBC"]]), tr2]))
q2 = R2["QQQ"]; putw = R2["PUTW"]; dbmf = R2["DBMF"]
base = 0.70 * q2 + 0.15 * dbmf + 0.15 * book2 # GD with REAL managed-futures ETF
with_vrp = 0.60 * q2 + 0.15 * dbmf + 0.15 * book2 + 0.10 * putw
report(f"B) Recent {common2[0]}..{common2[-1]} ({yrs2:.0f}y) — does VRP add?", [
("QQQ buy-hold", q2),
("GD (70Q/15DBMF/15book)", base),
("GD + 10% VRP(PUTW)", with_vrp),
("PUTW alone (VRP)", putw),
("DBMF alone (trend)", dbmf),
], yrs2)
print(" carry: no clean liquid retail ETF (DBV-type funds are tiny/closed) -> not testable cleanly here.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""growth-discipline — forward-tracker (NAV simulation), works exactly like multistrat_paper.py.
Strategy: GD = w·QQQ (growth core) + (1-w)·multi-strat book (diversified low-dd sleeve), continuously
rebalanced. Runs ALONGSIDE the live IBKR-paper book to compare forward: more CAGR, deeper drawdown.
No capital, no orders — pure NAV tracking (the live book is the IBKR-paper one). Cron daily.
python3 growth_discipline_paper.py status cum return + ann-Sharpe + maxDD + current weights
python3 growth_discipline_paper.py run book new trading days, persist (cron)
python3 growth_discipline_paper.py weights [USD] today's $ split (QQQ vs book)
Env: GD_EQUITY_WEIGHT (default 0.70). Caveat: backtest GD-70 ~13%/yr CAGR, -38% maxDD (2006-26);
execution-alpha (behavior gap, tax-harvest) is on top, not modeled here.
"""
import datetime
import json
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import build, book_series, yhist # noqa: E402
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
STATE = os.path.join(_REPO, "data/surfer/gd_paper_state.json")
WEQ = float(os.environ.get("GD_EQUITY_WEIGHT", "0.70"))
def build_gd():
"""GD daily returns aligned to the book's trading dates."""
dates, R = build() # book instruments (SPY/IEF/GLD/PDBC/DBMF/BTC)
book, _, _ = book_series(R)
qp = yhist("QQQ") # {date: adjclose}
q = np.array([qp.get(d, np.nan) for d in dates])
qr = np.zeros(len(dates))
qr[1:] = np.where(np.isfinite(q[1:]) & np.isfinite(q[:-1]), q[1:] / q[:-1] - 1, 0.0)
gd = WEQ * qr + (1 - WEQ) * book
return dates, gd
def load_state():
if os.path.exists(STATE):
return json.load(open(STATE))
return {"last_date": "", "days": 0, "sum_r": 0.0, "sumsq_r": 0.0, "equity": 1.0, "peak": 1.0, "max_dd": 0.0}
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
if cmd in ("run", "paper"):
st = load_state(); dates, gd = build_gd()
os.makedirs(os.path.dirname(STATE), exist_ok=True)
if not st["last_date"]:
st["last_date"] = dates[-1]; json.dump(st, open(STATE, "w"))
print(f"GD forward tracking initialized from {dates[-1]} (w_QQQ={WEQ})"); return
booked = 0
for i in range(1, len(dates)):
if dates[i] <= st["last_date"]:
continue
r = float(gd[i])
st["days"] += 1; st["sum_r"] += r; st["sumsq_r"] += r * r
st["equity"] *= (1 + r); st["peak"] = max(st["peak"], st["equity"])
st["max_dd"] = min(st["max_dd"], st["equity"] / st["peak"] - 1); st["last_date"] = dates[i]; booked += 1
json.dump(st, open(STATE, "w"))
print(f"{datetime.date.today()} GD booked {booked} day(s) -> day {st['days']} (through {st['last_date']}) cum {100*(st['equity']-1):+.2f}%")
elif cmd == "weights":
cap = float(sys.argv[2]) if len(sys.argv) > 2 else 35000.0
print(f"GD target split for ${cap:,.0f} (continuously rebalanced):")
print(f" {'QQQ (growth core)':>22}: {100*WEQ:>5.1f}% ${cap*WEQ:>10,.0f}")
print(f" {'multi-strat book':>22}: {100*(1-WEQ):>5.1f}% ${cap*(1-WEQ):>10,.0f}")
else:
st = load_state()
n = st["days"]; m = (st["sum_r"] / n) if n else 0; var = (st["sumsq_r"] / n - m * m) if n > 1 else 0
sh = (m * 252) / (math.sqrt(max(var, 1e-12)) * math.sqrt(252)) if n > 1 and var > 0 else float("nan")
print(f"growth-discipline paper (w_QQQ={WEQ}) — day {n} (through {st['last_date'] or 'n/a'})")
print(f" cum {100*(st['equity']-1):+.2f}% ann-Sharpe {sh:+.2f} maxDD {100*st['max_dd']:+.1f}%")
print(f" (compare vs the live IBKR-paper book; GD = higher CAGR, deeper drawdown by design)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""FUSION backtest: regime-adaptive QQQ <-> crisis-alpha rotation (the synthesis of all the ideas).
QQQ core whose weight is driven by edge-decay-TRUST (trailing risk-adjusted health, EMA-smoothed,
resurrection-capable) — NOT vol-normalized (so it keeps QQQ's CAGR in calm regimes). When QQQ's trust
decays (sustained downtrend/crisis) it ROTATES into the crisis-alpha sleeve (trend / diversified book,
which RISES in crises) instead of to cash; resurrects back to QQQ as it heals. Plus a light drawdown
circuit-breaker as a tail backstop. Honest test: vs QQQ / book / static GD, full + IS/OOS split +
turnover (the whipsaw cost). Verdict: fusion must beat static GD on CAGR-per-drawdown (Calmar), esp OOS.
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import book_series # noqa: E402
def yh(s):
r = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{s}?interval=1d&range=25y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
a = r["indicators"].get("adjclose", [{}])[0].get("adjclose") or r["indicators"]["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(r["timestamp"], a) if c}
def edge_decay_trust(ret, win=126, ema=0.94):
"""trailing-Sharpe health -> theta in [0.1, 1], EMA-smoothed, resurrection-capable."""
T = len(ret); th = 1.0; out = np.ones(T)
for t in range(win, T):
seg = ret[t - win:t]; sr = seg.mean() / (seg.std() + 1e-9) * math.sqrt(252)
target = float(np.clip((sr + 0.5) / 1.0, 0.10, 1.0))
th = ema * th + (1 - ema) * target; out[t] = th
return out
def main():
syms = ["QQQ", "SPY", "IEF", "GLD", "DBC"]
data = {s: yh(s) for s in syms}
common = sorted(set.intersection(*[set(data[s]) for s in syms])); T = len(common)
yrs = (datetime.date.fromisoformat(common[-1]) - datetime.date.fromisoformat(common[0])).days / 365.25
P = {s: np.array([data[s][d] for d in common]) for s in syms}
R = {s: np.concatenate([[0], P[s][1:] / P[s][:-1] - 1]) for s in syms}
qqq = R["QQQ"]
R4 = np.column_stack([R[s] for s in ["SPY", "IEF", "GLD", "DBC"]]); P4 = np.column_stack([P[s] for s in ["SPY", "IEF", "GLD", "DBC"]])
# trend sleeve (12-1 TS-mom, equal-risk = managed-futures proxy) + diversified book
lc = np.log(P4); sg = np.zeros((T, 4)); sg[252:] = np.sign(lc[252:] - lc[:-252])
vol = np.full((T, 4), np.nan)
for t in range(63, T):
vol[t] = R4[t - 63:t].std(0)
iv = 1 / np.where(vol > 0, vol, np.nan); w = np.nan_to_num(sg * iv); g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
trend = np.zeros(T); trend[1:] = np.sum(w[:-1] * R4[1:], axis=1)
book, _, _ = book_series(np.column_stack([R4, trend]))
# FUSION: w_qqq = trust(QQQ); rotate (1-w) into the crisis-alpha sleeve; + tail drawdown-CB
theta = edge_decay_trust(qqq)
def fusion(sleeve, dd_deadband=0.15):
wq = theta.copy(); ws = 1 - wq
r = np.zeros(T); eq = pk = 1.0; turn = 0.0
for t in range(252, T - 1):
base = wq[t] * qqq[t + 1] + ws[t] * sleeve[t + 1]
dd = eq / pk - 1
cb = float(np.clip(1 - 3 * max(0.0, -dd - dd_deadband), 0.5, 1.0)) # tail backstop only (>15% dd)
r[t + 1] = cb * base; eq *= (1 + r[t + 1]); pk = max(pk, eq)
turn += abs(wq[t] - wq[t - 1])
return r, turn / yrs # annualized one-way turnover of the QQQ leg
fus_trend, turn_t = fusion(trend)
fus_book, turn_b = fusion(book)
static_gd = 0.7 * qqq + 0.3 * book
def stats(r, lo=0, hi=None):
seg = r[lo:hi] if hi else r[lo:]; seg = np.nan_to_num(seg)
n = (seg != 0).sum(); y = n / 252 if n else 1
eq = np.cumprod(1 + seg); cagr = eq[-1] ** (1 / y) - 1 if y > 0 else 0
v = seg.std() * math.sqrt(252); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
sh = (seg.mean() * 252 - 0.03) / v if v > 0 else 0
calmar = cagr / abs(dd) if dd < 0 else 0
return cagr, v, sh, dd, calmar
sp = int(0.6 * T)
print(f"FUSION BACKTEST — {common[0]}..{common[-1]} ({yrs:.0f}y), OOS split @ {common[sp]}")
print(f" {'strategy':>26} | seg | CAGR | maxDD | Sharpe* | Calmar | turn/yr")
cands = [("QQQ buy-hold", qqq, 0), ("book (diversified)", book, 0), ("static GD 70/30", static_gd, 0),
("FUSION QQQ<->trend", fus_trend, turn_t), ("FUSION QQQ<->book", fus_book, turn_b)]
for nm, r, turn in cands:
for seg, lab in [((0, None), "ALL"), ((0, sp), "IS "), ((sp, None), "OOS")]:
c, v, sh, dd, cal = stats(r, seg[0], seg[1])
tt = f"{turn:.1f}x" if (lab == "ALL" and turn) else ""
print(f" {nm if lab=='ALL' else '':>26} | {lab} | {100*c:>4.1f}% | {100*dd:>+4.0f}% | {sh:>+5.2f} | {cal:>5.2f} | {tt}")
print(" *Sharpe excess over financing. Calmar = CAGR/|maxDD| (higher=better frontier).")
print(" VERDICT: FUSION beats static GD on OOS Calmar = regime-rotation shifts the frontier out (worth it).")
print(" If not = whipsaw eats it; keep the simpler static GD.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Run the adaptive multi-strat book on IBKR PAPER trading (realistic fills, no capital).
Gets target weights from the harness (Yahoo data), connects to your IB Gateway/TWS paper account,
and rebalances the ETF book on it. Crypto sleeve -> IBIT (Bitcoin ETF: in-brokerage, no crypto-
exchange counterparty tail). Default DRY-RUN (shows intended orders); pass `rebalance` to place them.
SETUP (you do this — your credentials, never me):
1. Create a free IBKR paper-trading account (or use your live account's paper login).
2. Run IB Gateway (or TWS) and log in to the PAPER account.
3. In Gateway/TWS: API settings -> enable "ActiveX and Socket Clients", note the port
(IB Gateway paper = 4002, TWS paper = 7497). Add 127.0.0.1 to trusted IPs.
4. Then: python3 scripts/surfer/ibkr_paper_book.py status (test the connection)
python3 scripts/surfer/ibkr_paper_book.py dry (show intended orders)
python3 scripts/surfer/ibkr_paper_book.py rebalance (place orders on PAPER)
Override port: ... <mode> --port 7497
Sizing uses Yahoo last-close (consistent with the book); IBKR fills at market. Hysteresis: only
trade a name if its target $ drifts > 3% of NLV. Run weekly/monthly via cron once validated.
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import build, book_series, INSTR # noqa: E402
TICKER = {"equity": "SPY", "bond": "IEF", "gold": "GLD", "commod": "PDBC", "trend": "DBMF", "crypto": "IBIT"}
HYST = 0.03 # only trade if target weight drifts > 3% of NLV
def yhist_last(sym):
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=5d",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
c = [x for x in res["indicators"]["quote"][0]["close"] if x is not None]
return float(c[-1])
def target_book():
"""{ticker: weight} from the adaptive harness (Yahoo data)."""
dates, R = build()
book, w, L = book_series(R)
return {TICKER[nm]: float(w[j] * L) for j, (_, nm) in enumerate(INSTR)}
def main():
mode = sys.argv[1] if len(sys.argv) > 1 else "dry"
port = int(sys.argv[sys.argv.index("--port") + 1]) if "--port" in sys.argv else 4002
tw = target_book()
px = {t: yhist_last(t if t != "IBIT" else "IBIT") for t in tw} # IBIT trades on Yahoo too
print(f"target weights (adaptive book, {datetime.date.today()}):")
for t, w in tw.items():
print(f" {t:>6}: {100*w:>5.1f}% (px ~${px[t]:.2f})")
if mode == "target":
return
from ib_async import IB, Stock, MarketOrder
ib = IB()
try:
ib.connect("127.0.0.1", port, clientId=7, timeout=15)
except Exception as e:
print(f"\nCONNECT FAILED on 127.0.0.1:{port} — is IB Gateway/TWS running + logged in to PAPER, API enabled?")
print(f" ({type(e).__name__}: {str(e)[:80]}) | try --port 7497 for TWS paper")
return
accts = ib.managedAccounts()
acct = accts[0] if accts else "?"
is_paper = acct.startswith("DU") # IBKR convention: DU* = paper/demo, U* = live
nlv = next((float(v.value) for v in ib.accountSummary() if v.tag == "NetLiquidation"), 0.0)
cash = next((float(v.value) for v in ib.accountSummary() if v.tag == "TotalCashValue"), 0.0)
pos = {p.contract.symbol: p.position for p in ib.positions()}
print(f"\nIBKR account: {acct} [{'PAPER ✅' if is_paper else 'LIVE ⚠️ — NOT paper!'}] NLV ${nlv:,.0f} cash ${cash:,.0f} | positions: {pos or 'none'}")
if nlv <= 0:
print(" (no NLV — check the paper account is funded with paper cash)"); ib.disconnect(); return
if mode == "mini":
if not is_paper:
print(" ABORT: account is NOT a paper account (no DU prefix). Refusing to place a test order."); ib.disconnect(); return
from ib_async import Stock, MarketOrder
c = Stock("SPY", "SMART", "USD"); ib.qualifyContracts(c)
print("\nplacing MINI test order: BUY 1 SPY (market) on PAPER...")
o = MarketOrder("BUY", 1); o.account = acct
tr = ib.placeOrder(c, o)
for _ in range(20):
ib.sleep(1)
if tr.orderStatus.status in ("Filled", "Cancelled", "ApiCancelled", "Inactive"):
break
fills = [f"{f.execution.shares}@${f.execution.price}" for f in tr.fills]
print(f" order status: {tr.orderStatus.status} filled {tr.orderStatus.filled}/1 avgPx ${tr.orderStatus.avgFillPrice or 0:.2f} fills={fills or 'none'}")
ib.sleep(1)
newpos = {p.contract.symbol: p.position for p in ib.positions()}
print(f" positions now: {newpos or 'none'}")
print(f" → VALIDATED: this is the {'PAPER' if is_paper else 'LIVE'} env, order routing works." if tr.orderStatus.status == "Filled"
else " → order not filled yet (market may be closed — it'll fill at next open; status shows it routed).")
ib.disconnect(); return
orders = []
for t, w in tw.items():
tgt_sh = nlv * w / px[t]
cur = pos.get(t, 0.0)
delta = tgt_sh - cur
# entry from zero uses a small floor (0.5% NLV); rebalancing an existing position uses full hysteresis
thresh = (0.005 if cur == 0 else HYST) * nlv
if abs(delta * px[t]) > thresh:
orders.append((t, "BUY" if delta > 0 else "SELL", round(abs(delta), 4)))
print(f"\nintended orders (hysteresis {int(HYST*100)}% of NLV):")
for t, side, qty in orders:
print(f" {side:>4} {qty:>9.4f} {t} (~${qty*px[t]:,.0f})")
if not orders:
print(" none — book already within hysteresis band.")
if mode == "rebalance" and orders:
print("\nPLACING on PAPER...")
for t, side, qty in orders:
c = Stock(t, "SMART", "USD"); ib.qualifyContracts(c)
o = MarketOrder(side, qty); o.account = ib.managedAccounts()[0]
tr = ib.placeOrder(c, o); ib.sleep(1)
print(f" {side} {qty} {t}: {tr.orderStatus.status}")
ib.sleep(3)
print("done — check fills in Gateway/TWS.")
elif orders:
print("\n(dry-run — pass 'rebalance' to actually place these on the paper account.)")
ib.disconnect()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Harden the 4h MFT trend edge — kill the inflation sources before trusting it.
The mft_horizon_sweep found a 4h selective trend edge (OOS net +5.9t, t+7.4). But that t is
OVERSTATED: overlapping 4h holds across 5-min entries are not independent. This re-tests honestly:
1. ONE TRADE PER SESSION — the single highest-conviction morning setup, ~4h hold, flat-by-close.
Non-overlapping ⇒ trades are ~independent across sessions ⇒ honest t-stat. Also the realistic strat.
2. IS-DERIVED conviction cutoff — set on the in-sample sessions, applied to OOS (no pooled-threshold leak).
3. REAL RTH session — ET 09:3016:00 (not UTC-midnight); entry window = first 2.5h so 4h fits before close.
4. PER-YEAR breakdown — is the edge in every year, or one regime?
5. Cost robustness (1 and 2 ticks) + multiple-testing context.
Pre-registered: dir=sign(ret over H bars), conviction=|t-stat|=|ret_H|/(vol·√H), vol=trailing 12-bar.
Entry = highest-conviction valid bar in 09:30-12:00, exit = +H bars (same session), cost 1 (and 2) ticks.
Trade the session only if its best conviction ≥ IS cutoff (selectivity sweep). H=48 (4h) primary; 36/60 robustness.
"""
import glob
import math
import numpy as np
import pandas as pd
TICK = 0.25
BAR_NS = 5 * 60 * 10**9
HOLDS = [36, 48, 60] # 3h, 4h (primary), 5h
ENTRY_CUTOFF_MIN = 12 * 60 # latest entry 12:00 ET so a 4h hold fits before 16:00
RTH_LO, RTH_HI = 9 * 60 + 30, 16 * 60
def load_es_5min():
import databento as db
ts_all, c_all = [], []
for p in sorted(glob.glob("data/surfer/es1m/*.dbn")):
try:
arr = db.DBNStore.from_file(p).to_ndarray()
except Exception:
continue
if len(arr) == 0 or "close" not in arr.dtype.names:
continue
ts = arr["ts_event"].astype(np.int64)
c = arr["close"].astype(np.float64) / 1e9
m = c > 0
ts_all.append(ts[m]); c_all.append(c[m])
ts = np.concatenate(ts_all); c = np.concatenate(c_all)
_u, ui = np.unique(ts, return_index=True); ts, c = ts[ui], c[ui]
o = np.argsort(ts, kind="stable"); ts, c = ts[o], c[o]
b5 = ts // BAR_NS
_, first = np.unique(b5, return_index=True)
last = np.append(first[1:] - 1, len(b5) - 1)
return ts[last], c[last]
def rth_sessions(ts5, close5):
"""Restrict to ET RTH 09:30-16:00, return (close, session_id, minute_of_day_ET, year) RTH-only, sorted."""
idx = pd.to_datetime(ts5, utc=True).tz_convert("America/New_York")
mins = (idx.hour * 60 + idx.minute).to_numpy()
rth = (mins >= RTH_LO) & (mins < RTH_HI)
et_date = idx.strftime("%Y-%m-%d").to_numpy()
year = idx.year.to_numpy()
close = close5[rth]; sess_str = et_date[rth]; mod = mins[rth]; yr = year[rth]
sid = pd.factorize(sess_str)[0] # contiguous session ids (data already time-sorted)
return close, sid, mod, yr
def per_session_trades(close, sid, mod, yr, H, cost):
"""For each RTH session pick the single highest-conviction entry in 09:30-12:00, hold H bars to a
same-session exit. Returns arrays over sessions that HAVE a valid candidate:
(best_conv, net_ticks, year, sess_order)."""
lc = np.log(close)
n = len(lc)
# trailing per-bar vol (12-bar) and H-bar trend, only valid within the same session
r1 = np.zeros(n); r1[1:] = lc[1:] - lc[:-1]
same1 = np.zeros(n, bool); same1[1:] = sid[1:] == sid[:-1]
r1 = np.where(same1, r1, 0.0)
# rolling 12-bar std via cumsum
w = 12
cs = np.concatenate([[0.0], np.cumsum(r1)]); cs2 = np.concatenate([[0.0], np.cumsum(r1 * r1)])
vol = np.full(n, np.nan)
s = cs[w:] - cs[:-w]; s2 = cs2[w:] - cs2[:-w]
vol[w - 1:] = np.sqrt(np.maximum(s2 / w - (s / w) ** 2, 0.0))
# trailing H-bar trend over the RTH-contiguous series (overnight gap collapsed — conservative:
# the overnight move, where much drift lives, is excluded from the signal). No same-session
# requirement on the lookback (a 4h trailing trend can't fit in the morning entry window otherwise).
retH = np.full(n, np.nan); retH[H:] = lc[H:] - lc[:-H]
tstat = retH / np.maximum(vol * math.sqrt(H), 1e-9)
# forward H-bar move (ticks), exit MUST be same session (flat-by-close)
fwd = np.full(n, np.nan); fwd[:-H] = (close[H:] - close[:-H]) / TICK
sameH_fwd = np.zeros(n, bool); sameH_fwd[:-H] = sid[:-H] == sid[H:]
entry_ok = (mod <= ENTRY_CUTOFF_MIN) & sameH_fwd & np.isfinite(tstat) & np.isfinite(fwd) & (vol > 0)
direction = np.sign(retH)
net = direction * fwd - cost
conv = np.abs(tstat)
out_conv, out_net, out_yr, out_ord = [], [], [], []
# group by session, pick max-conviction valid entry
order = np.argsort(sid, kind="stable")
sid_s = sid[order]
bounds = np.append(np.append(0, np.nonzero(np.diff(sid_s))[0] + 1), len(sid_s))
for b in range(len(bounds) - 1):
rows = order[bounds[b]:bounds[b + 1]]
rows = rows[entry_ok[rows] & (direction[rows] != 0)]
if len(rows) == 0:
continue
best = rows[np.argmax(conv[rows])]
out_conv.append(conv[best]); out_net.append(net[best])
out_yr.append(yr[best]); out_ord.append(sid[best])
return (np.array(out_conv), np.array(out_net), np.array(out_yr), np.array(out_ord))
def stats(net):
n = len(net)
if n < 20:
return n, float("nan"), float("nan"), float("nan"), float("nan")
m = float(net.mean()); sd = float(net.std()) + 1e-12
t = m / (sd / math.sqrt(n))
hit = 100.0 * float((net > 0).mean())
ann_sharpe = (m / sd) * math.sqrt(252.0) # ~1 trade/session ⇒ ~252 trades/yr
return n, m, hit, t, ann_sharpe
def run():
ts5, close5 = load_es_5min()
close, sid, mod, yr = rth_sessions(ts5, close5)
n_sess = int(sid.max()) + 1
print("\n================ 4h MFT HARDENING (RTH, 1 trade/session, IS-cutoff, per-year) ================")
print(f"RTH 5-min bars={len(close)} sessions={n_sess} years={int(yr.min())}-{int(yr.max())}")
print("honest non-overlapping t-stat (1 trade/session); multiple-testing bar ≈ |t|>2.3 for ~10 configs\n")
for cost in (1.0, 2.0):
print(f"================= COST = {cost:.0f} tick round-trip =================")
for H in HOLDS:
conv, net, tyr, sord = per_session_trades(close, sid, mod, yr, H, cost)
if len(conv) < 50:
print(f" H={H}: too few sessions with a candidate ({len(conv)})"); continue
split = np.quantile(sord, 0.70) # first 70% of sessions = IS
is_m = sord < split; oos_m = ~is_m
hh = H * 5 / 60.0
print(f" --- H={H} ({hh:.0f}h hold) --- (IS sessions {int(is_m.sum())} / OOS {int(oos_m.sum())})")
print(f" {'select':>8} {'cutoff':>7} {'n_OOS':>6} {'net_t':>7} {'hit%':>6} {'t':>6} {'annSR':>6} {'trd/yr':>7}")
for sel in (1.00, 0.50, 0.25, 0.10): # trade top-sel% of IS-conviction days
cutoff = np.quantile(conv[is_m], 1.0 - sel) if sel < 1.0 else -np.inf
take = oos_m & (conv >= cutoff)
n, m, hit, t, sr = stats(net[take])
oos_years = (sord[oos_m].max() - split) / n_sess * (int(yr.max()) - int(yr.min()) + 1)
trd_yr = n / max(oos_years, 1e-9)
cuts = f"{cutoff:.2f}" if np.isfinite(cutoff) else "all"
print(f" {sel:>8.2f} {cuts:>7} {n:>6} {m:>+7.2f} {hit:>6.1f} {t:>+6.2f} {sr:>+6.2f} {trd_yr:>7.1f}")
# per-year at the pre-registered selectivity = top 25% of IS-conviction days
cutoff = np.quantile(conv[is_m], 0.75)
print(f" per-year (sel=0.25, cost={cost:.0f}, OOS+IS shown; IS years are not OOS):")
for y in range(int(yr.min()), int(yr.max()) + 1):
ym = (tyr == y) & (conv >= cutoff)
if int(ym.sum()) < 10:
continue
n, m, hit, t, sr = stats(net[ym])
flag = "OOS" if (np.median(sord[tyr == y]) >= split) else "is "
print(f" {y} [{flag}] n={n:>4} net={m:>+6.2f}t hit={hit:>5.1f}% t={t:>+5.2f}")
print()
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Apples-to-apples hardening of the sweep's OWN 4h edge — same 24h-continuous signal, but kill the
statistical inflation: ONE non-overlapping trade per UTC-day, IS-derived conviction cutoff, per-year.
The sweep (mft_horizon_sweep) used a 24h-continuous 5-min grid, day = UTC-day, hold = 48 bars = 4h of
clock time, top-decile conviction over HEAVILY OVERLAPPING entries → t+7.4 (inflated by overlap). Here:
same grid + signal, but max-conviction single entry per UTC-day (non-overlapping ⇒ honest t), threshold
fit on IS, reported per-year. cost 1 and 2 ticks. PASS: OOS net>0 & hit>50% & honest t≥2, stable per-year.
"""
import glob
import math
import numpy as np
TICK = 0.25
BAR_NS = 5 * 60 * 10**9
DAY_NS = 86_400 * 10**9
HOLDS = [48] # 4h (the sweep's standout); same grid as the sweep
def load_es_5min():
import databento as db
ts_all, c_all = [], []
for p in sorted(glob.glob("data/surfer/es1m/*.dbn")):
try:
arr = db.DBNStore.from_file(p).to_ndarray()
except Exception:
continue
if len(arr) == 0 or "close" not in arr.dtype.names:
continue
ts = arr["ts_event"].astype(np.int64); c = arr["close"].astype(np.float64) / 1e9
m = c > 0; ts_all.append(ts[m]); c_all.append(c[m])
ts = np.concatenate(ts_all); c = np.concatenate(c_all)
_u, ui = np.unique(ts, return_index=True); ts, c = ts[ui], c[ui]
o = np.argsort(ts, kind="stable"); ts, c = ts[o], c[o]
b5 = ts // BAR_NS
_, first = np.unique(b5, return_index=True)
last = np.append(first[1:] - 1, len(b5) - 1)
return ts[last], c[last]
def per_day_trades(close, day, yr, H, cost):
lc = np.log(close); n = len(lc)
r1 = np.zeros(n); r1[1:] = lc[1:] - lc[:-1]
same1 = np.zeros(n, bool); same1[1:] = day[1:] == day[:-1]
r1 = np.where(same1, r1, 0.0)
w = 12
cs = np.concatenate([[0.0], np.cumsum(r1)]); cs2 = np.concatenate([[0.0], np.cumsum(r1 * r1)])
vol = np.full(n, np.nan); s = cs[w:] - cs[:-w]; s2 = cs2[w:] - cs2[:-w]
vol[w - 1:] = np.sqrt(np.maximum(s2 / w - (s / w) ** 2, 0.0))
retH = np.full(n, np.nan); retH[H:] = lc[H:] - lc[:-H]
sameH_back = np.zeros(n, bool); sameH_back[H:] = day[H:] == day[:-H] # like the sweep: sameday(H)
tstat = retH / np.maximum(vol * math.sqrt(H), 1e-9)
fwd = np.full(n, np.nan); fwd[:-H] = (close[H:] - close[:-H]) / TICK
sameH_fwd = np.zeros(n, bool); sameH_fwd[:-H] = day[:-H] == day[H:]
ok = sameH_fwd & sameH_back & np.isfinite(tstat) & np.isfinite(fwd) & (vol > 0)
direction = np.sign(retH); net = direction * fwd - cost; conv = np.abs(tstat)
oc, on, oy, od = [], [], [], []
order = np.argsort(day, kind="stable"); ds = day[order]
bounds = np.append(np.append(0, np.nonzero(np.diff(ds))[0] + 1), len(ds))
for b in range(len(bounds) - 1):
rows = order[bounds[b]:bounds[b + 1]]
rows = rows[ok[rows] & (direction[rows] != 0)]
if len(rows) == 0:
continue
best = rows[np.argmax(conv[rows])]
oc.append(conv[best]); on.append(net[best]); oy.append(yr[best]); od.append(day[best])
return np.array(oc), np.array(on), np.array(oy), np.array(od)
def stats(net):
n = len(net)
if n < 20:
return n, float("nan"), float("nan"), float("nan"), float("nan")
m = float(net.mean()); sd = float(net.std()) + 1e-12
return n, m, 100.0 * float((net > 0).mean()), m / (sd / math.sqrt(n)), (m / sd) * math.sqrt(252.0)
def run():
ts5, close5 = load_es_5min()
day = (ts5 // DAY_NS).astype(np.int64)
yr = (1970 + (ts5 // (365.25 * DAY_NS))).astype(int) # approx calendar year from epoch days
print("\n============ APPLES-TO-APPLES 4h HARDENING (24h grid, 1 trade/UTC-day, IS-cutoff) ============")
print(f"5-min bars={len(close5)} days={int(day.max()-day.min())} multiple-testing bar ≈ |t|>2.3\n")
for cost in (1.0, 2.0):
for H in HOLDS:
conv, net, tyr, dd = per_day_trades(close5, day, yr, H, cost)
if len(conv) < 50:
print(f"cost={cost:.0f} H={H}: too few ({len(conv)})"); continue
split = np.quantile(dd, 0.70); is_m = dd < split; oos_m = ~is_m
print(f"=== cost={cost:.0f} tick, H={H} (4h), 1 trade/day (IS days {int(is_m.sum())} / OOS {int(oos_m.sum())}) ===")
print(f" {'select':>7} {'cutoff':>7} {'n_OOS':>6} {'net_t':>7} {'hit%':>6} {'t':>6} {'annSR':>6} {'trd/yr':>7}")
for sel in (1.00, 0.50, 0.25, 0.10):
cut = np.quantile(conv[is_m], 1.0 - sel) if sel < 1.0 else -np.inf
take = oos_m & (conv >= cut)
n, m, hit, t, sr = stats(net[take])
oosyrs = max((int(tyr[oos_m].max()) - int(tyr[oos_m].min()) + 1), 1)
cs = f"{cut:.2f}" if np.isfinite(cut) else "all"
print(f" {sel:>7.2f} {cs:>7} {n:>6} {m:>+7.2f} {hit:>6.1f} {t:>+6.2f} {sr:>+6.2f} {n/oosyrs:>7.1f}")
cut = np.quantile(conv[is_m], 0.75)
print(f" per-year (sel=0.25; IS years are not OOS):")
for y in range(int(tyr.min()), int(tyr.max()) + 1):
ym = (tyr == y) & (conv >= cut)
if int(ym.sum()) < 10:
continue
n, m, hit, t, sr = stats(net[ym])
flag = "OOS" if np.median(dd[tyr == y]) >= split else "is "
print(f" {y} [{flag}] n={n:>4} net={m:>+6.2f}t hit={hit:>5.1f}% t={t:>+5.2f}")
print()
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""MFT horizon sweep — does a SLOWER (lower-turnover) intraday trend edge clear costs?
Prong A (mft_quality_test) showed: at 15-min holds the net ≈ -1 tick = the cost itself (gross ≈ 0).
This sweeps the hold horizon UP (15m → full session) on clean ES OHLCV-1m. As the hold grows the
forward move (in ticks) grows ~√time while the 1-tick cost stays fixed, so cost/gross shrinks — this
isolates the question the user posed: keep costs low by trading SLOWER. If gross edge grows with
horizon (trend persistence), there's an MFT sweet spot; if gross stays ~0 everywhere, the price signal
is dead regardless of speed.
Pre-registered (NOT tuned): trend-follow, lookback = hold = H bars (5-min each); direction = sign(ret_H);
conviction = |t-stat| = |ret_H| / (per-bar vol · √H); entry close[t], exit close[t+H], intraday only
(same-day, flat-by-close); cost = 1.0 tick round-trip (crossing). Quality curve by conviction decile,
IS(first 70%) / OOS(last 30%). PASS bar: OOS top-decile net > 0 ticks & hit > 50% & |t| ≥ 2, at a
horizon with low trades/day (genuinely MFT).
"""
import glob
import math
import numpy as np
import torch
DEV = "cuda" if torch.cuda.is_available() else None
TICK = 0.25
COST_TICKS = 1.0
BAR_NS = 5 * 60 * 10**9
DAY_NS = 86_400 * 10**9
HOLDS = [3, 12, 24, 48, 78] # 15m, 1h, 2h, 4h, ~full session (5-min bars)
def load_es_5min():
import databento as db
es1m = sorted(glob.glob("data/surfer/es1m/*.dbn"))
ts_all, c_all = [], []
if es1m:
for p in es1m:
try:
arr = db.DBNStore.from_file(p).to_ndarray()
except Exception:
continue
if len(arr) == 0 or "close" not in arr.dtype.names:
continue
ts = arr["ts_event"].astype(np.int64)
c = arr["close"].astype(np.float64) / 1e9
m = c > 0
ts_all.append(ts[m]); c_all.append(c[m])
else:
for p in sorted(glob.glob("test_data/futures-baseline/ES.FUT/*.dbn.zst")):
try:
df = db.DBNStore.from_file(p).to_df().reset_index()
except Exception:
continue
if df.empty or "close" not in df.columns:
continue
df = df[df["close"] > 0]
if df.empty:
continue
dom = df["instrument_id"].value_counts().idxmax()
df = df[df["instrument_id"] == dom].sort_values("ts_event")
ts_all.append(df["ts_event"].astype("int64").to_numpy())
c_all.append(df["close"].to_numpy(np.float64))
ts = np.concatenate(ts_all); c = np.concatenate(c_all)
_u, _ui = np.unique(ts, return_index=True)
ts, c = ts[_ui], c[_ui]
o = np.argsort(ts, kind="stable"); ts, c = ts[o], c[o]
b5 = ts // BAR_NS
_, first = np.unique(b5, return_index=True)
last = np.append(first[1:] - 1, len(b5) - 1)
return ts[last], c[last]
def rolling_std(x, w):
z = torch.zeros(1, dtype=x.dtype, device=x.device)
cs = torch.cat([z, torch.cumsum(x, 0)])
cs2 = torch.cat([z, torch.cumsum(x * x, 0)])
s = cs[w:] - cs[:-w]
s2 = cs2[w:] - cs2[:-w]
mean = s / w
var = (s2 / w - mean * mean).clamp_min(0.0)
out = torch.full_like(x, float("nan"))
out[w - 1:] = var.sqrt()
return out
def run():
ts5, close5 = load_es_5min()
day = ts5 // DAY_NS
n_days = int(day.max() - day.min())
lc = torch.tensor(np.log(close5), device=DEV, dtype=torch.float64)
px = torch.tensor(close5, device=DEV, dtype=torch.float64)
dy = torch.tensor(day, device=DEV)
T = len(lc)
t = torch.arange(T, device=DEV)
split = int(0.7 * T)
def sameday(lag):
m = torch.zeros(T, dtype=torch.bool, device=DEV)
m[lag:] = dy[lag:] == dy[:-lag]
return m
r1 = torch.zeros_like(lc); r1[1:] = lc[1:] - lc[:-1]
r1 = torch.where(sameday(1), r1, torch.zeros_like(r1))
vol1 = rolling_std(r1, 12) # per-bar 5-min vol (fixed 1h window, pre-registered)
print("\n================ MFT HORIZON SWEEP (GPU, intraday trend, flat-by-close) ================")
print(f"device={DEV} 5-min bars={T} span days≈{n_days} cost={COST_TICKS} tick round-trip")
print("trend-follow: dir=sign(ret_H), conv=|ret_H|/(vol·√H). PASS: OOS top-decile net>0 & hit>50% & |t|≥2\n")
print(f"{'hold':>6} {'top-q':>6} {'n_OOS':>7} {'gross_t':>8} {'net_t':>8} {'cost/gr':>8} {'hit%':>6} {'t-stat':>7} {'trd/day':>8}")
print("-" * 78)
for H in HOLDS:
retH = torch.full_like(lc, float("nan")); retH[H:] = lc[H:] - lc[:-H]
tstat = retH / (vol1 * math.sqrt(H)).clamp_min(1e-9)
fwd_ticks = torch.full_like(px, float("nan"))
fwd_ticks[:-H] = (px[H:] - px[:-H]) / TICK
fwd_valid = torch.zeros(T, dtype=torch.bool, device=DEV)
fwd_valid[:-H] = dy[H:] == dy[:-H] # exit same day (flat-by-close)
valid = fwd_valid & sameday(H) & torch.isfinite(tstat) & torch.isfinite(vol1) & (vol1 > 0)
direction = torch.sign(retH)
net = direction * fwd_ticks - COST_TICKS
gross = direction * fwd_ticks
cand = valid & (direction != 0)
idx = torch.nonzero(cand, as_tuple=True)[0]
if len(idx) < 100:
print(f"{H:>6} (too few candidates: {len(idx)})"); continue
conv = tstat.abs()[idx]; tt = t[idx]
order = torch.argsort(conv, descending=True)
hold_hours = H * 5 / 60.0
for q in [0.05, 0.10, 1.00]:
k = max(int(q * len(idx)), 1)
sel = idx[order[:k]]
oos = sel[tt[order[:k]] >= split]
no = int(len(oos))
if no < 20:
print(f"{H:>6} {q:>6.2f} (OOS n<20)"); continue
g = gross[oos]; nt = net[oos]
gm = float(g.mean()); nm = float(nt.mean())
tval = nm / (float(nt.std()) / math.sqrt(no) + 1e-12)
hit = 100.0 * float((nt > 0).float().mean())
cost_gr = (COST_TICKS / gm * 100.0) if gm > 0 else float("nan")
trd_day = no / (0.3 * n_days) # OOS is last 30% of days
tag = f"{H}({hold_hours:.1f}h)" if q == 0.05 else ""
print(f"{tag:>6} {q:>6.2f} {no:>7} {gm:>+8.3f} {nm:>+8.3f} {cost_gr:>7.0f}% {hit:>6.1f} {tval:>+7.2f} {trd_day:>8.2f}")
print("-" * 78)
def main():
if DEV is None:
raise SystemExit("CUDA not available")
run()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Surfer MFT quality test — regime-adaptive, selective, intraday (GPU).
Tests the ACTUAL surfer thesis (not daily CTA): ~5-min decisions, ~15-min holds,
flat-by-close, regime-ADAPTIVE (ride trend / fade range / stand aside in chop),
and QUALITY over quantity (only the top-conviction waves). Intraday + flat-by-close
⇒ no overnight, no roll-gap problem.
Data: clean ES OHLCV-1m (test_data/futures-baseline/ES.FUT, front-month per quarter),
resampled to 5-min. Signal/regime/backtest/stats on the GPU (torch.cuda); load=CPU staging.
Pre-registered (NOT tuned), to bound overfit on a selective/small-sample test:
vol = trailing 12-bar (1h) std of 5-min returns
trend t = ret(12 bars) / (vol·√12) # 1h trend t-stat
TREND if |t| > 1.0 → trade WITH ret(12) # ride the wave
RANGE if |t| < 0.5 → trade AGAINST ret(3) # fade small chop toward mean
else (0.5..1.0) → STAND ASIDE (no trade)
conviction = |t| (trend) or |ret3|/(vol·√3) (range)
entry=close[t], exit=close[t+3] (15 min), cost = 1.0 tick round-trip (crossing).
QUALITY curve: rank candidate trades by conviction, take the top q%, report per-trade
net edge (ticks), hit-rate, count — IS(first 70%) and OOS(last 30%).
"""
import glob
import math
import numpy as np
import torch
DEV = "cuda" if torch.cuda.is_available() else None
TICK = 0.25
COST_TICKS = 1.0
BAR_NS = 5 * 60 * 10**9
DAY_NS = 86_400 * 10**9
def load_es_5min():
import databento as db
# Prefer the full-history continuous front-month pull (data/surfer/es1m/), else fall back
# to the local ~2y parent OHLCV-1m (futures-baseline/ES, per-quarter front-month pick).
es1m = sorted(glob.glob("data/surfer/es1m/*.dbn"))
ts_all, c_all = [], []
if es1m:
for p in es1m: # to_ndarray = light (to_df OOMs at 20GB)
try:
arr = db.DBNStore.from_file(p).to_ndarray()
except Exception:
continue
if len(arr) == 0 or "close" not in arr.dtype.names:
continue
ts = arr["ts_event"].astype(np.int64)
c = arr["close"].astype(np.float64) / 1e9 # raw 1e9 fixed-point → price
m = c > 0
ts_all.append(ts[m]); c_all.append(c[m])
else:
for p in sorted(glob.glob("test_data/futures-baseline/ES.FUT/*.dbn.zst")):
try:
df = db.DBNStore.from_file(p).to_df().reset_index()
except Exception:
continue
if df.empty or "close" not in df.columns:
continue
df = df[df["close"] > 0]
if df.empty:
continue
dom = df["instrument_id"].value_counts().idxmax() # front month for the quarter
df = df[df["instrument_id"] == dom].sort_values("ts_event")
ts_all.append(df["ts_event"].astype("int64").to_numpy())
c_all.append(df["close"].to_numpy(np.float64))
ts = np.concatenate(ts_all); c = np.concatenate(c_all)
_u, _ui = np.unique(ts, return_index=True) # dedup any overlapping ts at chunk seams
ts, c = ts[_ui], c[_ui]
o = np.argsort(ts, kind="stable"); ts, c = ts[o], c[o]
b5 = ts // BAR_NS # 5-min bin id
_, first = np.unique(b5, return_index=True)
last = np.append(first[1:] - 1, len(b5) - 1) # last 1-min close in each 5-min bin
return ts[last], c[last]
def rolling_std(x, w): # trailing-w std at each t (NaN first w-1); O(T) memory (cumsum, no unfold)
z = torch.zeros(1, dtype=x.dtype, device=x.device)
cs = torch.cat([z, torch.cumsum(x, 0)])
cs2 = torch.cat([z, torch.cumsum(x * x, 0)])
s = cs[w:] - cs[:-w]
s2 = cs2[w:] - cs2[:-w]
mean = s / w
var = (s2 / w - mean * mean).clamp_min(0.0)
out = torch.full_like(x, float("nan"))
out[w - 1:] = var.sqrt()
return out
def run():
ts5, close5 = load_es_5min()
day = ts5 // DAY_NS
lc = torch.tensor(np.log(close5), device=DEV, dtype=torch.float64)
px = torch.tensor(close5, device=DEV, dtype=torch.float64)
dy = torch.tensor(day, device=DEV)
T = len(lc)
t = torch.arange(T, device=DEV)
def sameday(lag): # day[i]==day[i-lag]
m = torch.zeros(T, dtype=torch.bool, device=DEV)
m[lag:] = dy[lag:] == dy[:-lag]
return m
r1 = torch.zeros_like(lc); r1[1:] = lc[1:] - lc[:-1]
r1 = torch.where(sameday(1), r1, torch.zeros_like(r1)) # zero overnight returns
vol = rolling_std(r1, 12) * math.sqrt(1.0) # per-bar vol (5-min)
ret12 = torch.full_like(lc, float("nan")); ret12[12:] = lc[12:] - lc[:-12]
ret3 = torch.full_like(lc, float("nan")); ret3[3:] = lc[3:] - lc[:-3]
tstat = ret12 / (vol * math.sqrt(12)).clamp_min(1e-9)
# forward 15-min (3 bars) PRICE move in ticks, intraday only
fwd_ticks = torch.full_like(px, float("nan"))
fwd_ticks[:-3] = (px[3:] - px[:-3]) / TICK
fwd_valid = torch.zeros(T, dtype=torch.bool, device=DEV)
fwd_valid[:-3] = dy[3:] == dy[:-3]
valid = fwd_valid & sameday(12) & torch.isfinite(tstat) & torch.isfinite(vol) & (vol > 0)
def evaluate(name, direction, conviction, candidate):
cand = candidate & valid & torch.isfinite(direction) & (direction != 0)
# net per-trade edge in ticks (enter close[t], exit close[t+3], 1-tick round-trip cost)
net = direction * fwd_ticks - COST_TICKS
idx = torch.nonzero(cand, as_tuple=True)[0]
if len(idx) < 50:
print(f" [{name}] too few candidates ({len(idx)})"); return
conv = conviction[idx]; netc = net[idx]; tt = t[idx]
split = int(0.7 * T)
is_m = tt < split; oos_m = ~is_m
print(f" [{name}] candidates={len(idx)} (IS {int(is_m.sum())} / OOS {int(oos_m.sum())})")
print(f" {'top-q':>6} {'n_OOS':>6} {'net_t/trade_OOS':>15} {'t-stat':>7} {'hit%_OOS':>9} {'IS':>8}")
order = torch.argsort(conv, descending=True)
for q in [0.05, 0.10, 0.25, 0.50, 1.00]:
k = max(int(q * len(idx)), 1)
sel = order[:k]
sm = oos_m[sel]; im = is_m[sel]
no = int(sm.sum())
if no < 10:
print(f" {q:>6.2f} (OOS n<10)"); continue
vals = netc[sel][sm]
net_oos = float(vals.mean())
tstat = net_oos / (float(vals.std()) / math.sqrt(no) + 1e-12)
hit_oos = float((vals > 0).float().mean())
net_is = float(netc[sel][im].mean()) if int(im.sum()) > 0 else float("nan")
print(f" {q:>6.2f} {no:>6} {net_oos:>+15.3f} {tstat:>+7.2f} {100*hit_oos:>8.1f}% {net_is:>+8.3f}")
print("\n================ SURFER MFT QUALITY TEST (GPU, intraday flat-by-close) ================")
print(f"device={DEV} 5-min bars={T} span days≈{int((day.max()-day.min()))} cost={COST_TICKS} tick round-trip")
print("net_t/trade = mean per-trade edge in TICKS after cost; quality bar: OOS top-decile > 0 & hit>50%\n")
# regime-adaptive direction + conviction
trend = tstat.abs() > 1.0
rang = tstat.abs() < 0.5
dir_adapt = torch.zeros_like(tstat)
dir_adapt = torch.where(trend, torch.sign(ret12), dir_adapt) # ride
dir_adapt = torch.where(rang, -torch.sign(ret3), dir_adapt) # fade
conv_adapt = torch.where(trend, tstat.abs(),
torch.where(rang, (ret3.abs() / (vol * math.sqrt(3)).clamp_min(1e-9)),
torch.zeros_like(tstat)))
evaluate("REGIME-ADAPTIVE (ride trend / fade range / aside)", dir_adapt, conv_adapt, (trend | rang))
# baselines for contrast
evaluate("naive MOMENTUM always", torch.sign(ret12), tstat.abs(), torch.ones(T, dtype=torch.bool, device=DEV))
evaluate("naive MEAN-REVERSION always", -torch.sign(ret3), (ret3.abs()/(vol*math.sqrt(3)).clamp_min(1e-9)),
torch.ones(T, dtype=torch.bool, device=DEV))
def main():
if DEV is None:
raise SystemExit("CUDA not available")
run()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Phase A — minute-horizon crypto signal gate (does deep ML have anything to extract?).
Before training Mamba2/CfC/TLOB, prove there's cost-surviving predictability at the minute
horizon. Free Binance 1m klines carry an OFI proxy (takerBuyQuote/quote). Test per-coin
short-horizon features -> forward returns, pooled, net of taker cost. If even simple features
show net edge, deep sequence models can plausibly extract more; if not, same wall as ES.
Features (info up to t): r1,r5,r15 (momentum/reversal), ofi (taker imbalance), ofi5 (smoothed),
vol_z. Targets: forward log-return over h in {1,5,15} min. Cost: 5bp round-trip taker.
Reports per-feature IC + best simple strategy per-trade net edge (bps) vs cost. GPU compute.
"""
import json
import math
import os
import sys
import time
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
OUT = "data/surfer/crypto1m"
SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "DOGEUSDT",
"ADAUSDT", "AVAXUSDT", "LINKUSDT", "LTCUSDT", "SUIUSDT", "NEARUSDT"]
DAYS = 60
COST_BP = 5.0
def _get(u):
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
def fetch(sym):
cache = f"{OUT}/{sym}.npz"
if os.path.exists(cache):
d = np.load(cache); return d["close"], d["qv"], d["tbq"]
end = int(time.time() * 1000); start = end - DAYS * 86_400_000
rows = []
cur = start
for _ in range(200):
k = _get(f"https://fapi.binance.com/fapi/v1/klines?symbol={sym}&interval=1m&startTime={cur}&limit=1500")
if not k:
break
rows += k
cur = k[-1][0] + 1
if len(k) < 1500 or cur >= end:
break
time.sleep(0.05)
d = {int(r[0]): (float(r[4]), float(r[7]), float(r[10])) for r in rows} # close, quoteVol, takerBuyQuote
ts = sorted(d)
close = np.array([d[t][0] for t in ts]); qv = np.array([d[t][1] for t in ts]); tbq = np.array([d[t][2] for t in ts])
os.makedirs(OUT, exist_ok=True)
np.savez(cache, close=close, qv=qv, tbq=tbq)
return close, qv, tbq
def roll_mean(x, w):
c = np.concatenate([[0], np.cumsum(x)])
out = np.full_like(x, np.nan); out[w - 1:] = (c[w:] - c[:-w]) / w; return out
def main():
feats_all = {k: [] for k in ["r1", "r5", "r15", "ofi", "ofi5", "vol_z"]}
fwd_all = {h: [] for h in [1, 5, 15]}
print(f"fetching 1m klines ({len(SYMS)} perps, {DAYS}d)...")
for s in SYMS:
close, qv, tbq = fetch(s)
lc = np.log(close)
r1 = np.full_like(lc, np.nan); r1[1:] = lc[1:] - lc[:-1]
r5 = np.full_like(lc, np.nan); r5[5:] = lc[5:] - lc[:-5]
r15 = np.full_like(lc, np.nan); r15[15:] = lc[15:] - lc[:-15]
ofi = np.where(qv > 0, 2 * tbq / qv - 1.0, 0.0) # taker buy imbalance in [-1,1]
ofi5 = roll_mean(ofi, 5)
vz = (qv - roll_mean(qv, 30)) / (roll_mean(qv, 30) + 1e-9)
feats = {"r1": r1, "r5": r5, "r15": r15, "ofi": ofi, "ofi5": ofi5, "vol_z": vz}
for k in feats_all:
feats_all[k].append(feats[k])
for h in fwd_all:
fwd = np.full_like(lc, np.nan); fwd[:-h] = lc[h:] - lc[:-h]
fwd_all[h].append(fwd)
F = {k: np.concatenate(v) for k, v in feats_all.items()}
FW = {h: np.concatenate(v) for h, v in fwd_all.items()}
print(f"\n===== MINUTE-HORIZON GATE — {len(SYMS)} perps, {DAYS}d, cost={COST_BP}bp round-trip =====")
print("IC = corr(feature_t, forward-return); per-trade NET = mean(sign(feat)*fwd) - cost, in bps")
print(f"{'feature':>8} " + " ".join(f"h={h}:IC/netbp" for h in [1, 5, 15]))
for fk, fv in F.items():
cells = []
for h, fw in FW.items():
m = np.isfinite(fv) & np.isfinite(fw)
a, b = fv[m], fw[m]
ic = float(np.corrcoef(a, b)[0, 1]) if len(a) > 1000 else float("nan")
net_bp = (float(np.mean(np.sign(a) * b)) - COST_BP / 1e4) * 1e4
cells.append(f"{ic:+.3f}/{net_bp:+.1f}")
print(f"{fk:>8} " + " ".join(cells))
print("\nVERDICT: any feature with positive per-trade NET bps (edge > cost) => minute-horizon signal")
print("exists for Mamba2/CfC/TLOB to extract -> build GPU model POC. All negative => same wall as ES.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Stress-test the one survivor: crypto cross-sectional momentum.
Three robustness probes on the broad-but-clean universe (funding-cov>0.90, >900d):
1. LOOKBACK SWEEP — is the edge smooth across 5..120d (real factor) or a single spike (overfit)?
2. COIN-BOOTSTRAP — resample WHICH coins are in the cross-section 200× (survivorship proxy):
does momentum stay positive regardless of which coins survived?
3. VOL-SCALED — does risk-adjusting the momentum signal (ret/vol) help?
Validation reused from the sweep (full/IS/OOS Sharpe, CPCV, Deflated Sharpe).
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import crypto_sweep as cs # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
cs.MIN_FUNDCOV, cs.MIN_DAYS = 0.90, 900
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, fund = cs.load_crypto()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
Reff = R - np.where(np.isfinite(fund), fund, 0.0)
vol30 = np.full_like(lc, np.nan)
for t in range(30, T):
vol30[t] = np.nanstd(R[t - 30:t], axis=0)
print(f"\n===== MOMENTUM ROBUSTNESS — {N} clean coins, {T}d =====")
print("\n[1] LOOKBACK SWEEP (XS momentum, ~10bp cost)")
print(f"{'lookback':>9} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5}")
for L in [5, 10, 20, 30, 45, 60, 90, 120]:
pnl = pnl_w(xs_weights(trailing(lc, L)), Reff, cost_bp=10)
v = validate(pnl, days, 8)
print(f"{L:>9} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f}")
print("\n[2] VOL-SCALED vs RAW (30d)")
for nm, sg in [("raw_mom30", trailing(lc, 30)), ("volscaled_mom30", trailing(lc, 30) / np.where(vol30 > 0, vol30, np.nan))]:
v = validate(pnl_w(xs_weights(sg), Reff, cost_bp=10), days, 8)
print(f" {nm:>16}: full {v['full']:>+.2f} OOS {v['oos']:>+.2f} CPCVmed {v['med']:>+.2f}")
print("\n[3] COIN-BOOTSTRAP — mom_30 on 200 random half-universes (survivorship proxy)")
rng = np.random.default_rng(42)
sig30 = trailing(lc, 30)
sh = []
for _ in range(200):
cols = rng.choice(N, size=max(N // 2, 10), replace=False)
sub = np.full((T, N), np.nan); sub[:, cols] = sig30[:, cols]
pnl = pnl_w(xs_weights(sub), Reff[:, :], cost_bp=10) # weights already zero outside cols
s = sharpe_t(torch.tensor(pnl, device=DEV, dtype=torch.float64))
if not math.isnan(s):
sh.append(s)
sh = np.array(sh)
print(f" draws={len(sh)} mean Sharpe {sh.mean():+.2f} median {np.median(sh):+.2f} "
f"5th-pct {np.percentile(sh,5):+.2f} frac>0 {np.mean(sh>0):.2f}")
print("\nVERDICT: smooth lookback curve + bootstrap frac>0 near 1.0 + 5th-pct>0 => robust real factor.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Multi-asset futures test: does breadth (39 roots) unlock an edge, and is a CTA trend book
a genuine DIVERSIFIER to the crypto momentum book?
(A) XS momentum (was null on 22 roots) + TS trend-following (directional CTA) on the 39-asset
daily futures panel — deflated, per-year, realistic cost.
(B) The prize: correlation of the futures TS-trend book to the crypto XS-momentum book (overlap
2019-2026) + the combined two-market book. Low correlation + both positive = real diversification.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import load_panel, build_returns, xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import pit_sweep # noqa: E402
from surfer_poc import compute_weights, CFG # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
roots, days, close, op, inst, weekday = load_panel()
lc, R, ov, intr = build_returns(close, op, inst)
T, N = close.shape
vol60 = roll(np.std, R, 60)
invvol = np.where(vol60 > 0, 1.0 / vol60, 0.0)
year = (1970 + days / 365.25).astype(int)
yrs = list(range(2011, 2027))
def wdir(sig): # directional unit-gross (TS trend, net exposure)
s = np.nan_to_num(sig)
g = np.sum(np.abs(s), axis=1, keepdims=True); g[g == 0] = 1
return s / g
def peryear(p, yy):
return [sharpe_t(torch.tensor(p[yy[1:] == y], device=DEV, dtype=torch.float64))
if (yy[1:] == y).sum() > 30 else float("nan") for y in yrs]
print(f"\n===== (A) MULTI-ASSET FUTURES — {N} roots, {T} days =====")
print(f"{'signal':>16} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5}")
sigs = {}
for L in [20, 60, 120]:
sigs[f"XS_mom_{L}"] = pnl_w(xs_weights(trailing(lc, L)), R, cost_bp=1.5)
for L in [60, 120, 250]:
sigs[f"TS_trend_{L}"] = pnl_w(wdir(np.sign(trailing(lc, L)) * invvol), R, cost_bp=1.5)
NT = 40 # cumulative-session deflation (honest)
best_ts, best_ts_pnl, best_med = None, None, -9
for nm, p in sigs.items():
v = validate(p, days, NT)
print(f"{nm:>16} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f}")
if nm.startswith("TS_trend") and v["med"] > best_med:
best_ts, best_ts_pnl, best_med = nm, p, v["med"]
print(f" futures TS-trend per-year ({best_ts}): " + " ".join(f"{x:+.2f}" if not math.isnan(x) else " n/a" for x in peryear(best_ts_pnl, year)))
# ---------- (B) DIVERSIFIER: futures trend vs crypto momentum ----------
syms, cdays, cclose, cqv, cfund = pit_sweep.load()
cR = np.zeros_like(cclose); cR[1:] = np.log(cclose)[1:] - np.log(cclose)[:-1]
cR = np.where(np.isfinite(cR), cR, 0.0)
cfund = np.where(np.isfinite(cfund), cfund, 0.0)
cw, _ = compute_weights(cclose, cqv, cdays, CFG)
cReff = cR - cfund
crypto_book = np.sum(cw[:-1] * cReff[1:], axis=1) # crypto momentum daily PnL (cdays[1:])
fut_book = best_ts_pnl # futures trend daily PnL (days[1:])
# align on common epoch-days
fd, cd = days[1:], cdays[1:]
fmap = {int(d): fut_book[i] for i, d in enumerate(fd)}
cmap = {int(d): crypto_book[i] for i, d in enumerate(cd)}
common = sorted(set(fmap) & set(cmap))
fa = np.array([fmap[d] for d in common]); ca = np.array([cmap[d] for d in common])
m = np.isfinite(fa) & np.isfinite(ca)
corr = float(np.corrcoef(fa[m], ca[m])[0, 1])
sf, sc = float(np.std(fa[m])), float(np.std(ca[m]))
comb = ((1 / sf) * fa[m] + (1 / sc) * ca[m]) / (1 / sf + 1 / sc)
cyear = (1970 + np.array(common) / 365.25).astype(int)[m]
print(f"\n===== (B) DIVERSIFIER CHECK — futures-trend vs crypto-momentum (overlap {len(fa[m])} days) =====")
print(f"futures-trend Sharpe {sharpe_t(torch.tensor(fa[m],device=DEV,dtype=torch.float64)):+.2f} "
f"crypto-momentum Sharpe {sharpe_t(torch.tensor(ca[m],device=DEV,dtype=torch.float64)):+.2f}")
print(f"CORRELATION = {corr:+.2f} combined-book Sharpe = {sharpe_t(torch.tensor(comb,device=DEV,dtype=torch.float64)):+.2f}")
print("combined per-year: " + " ".join(f"{y}:{sharpe_t(torch.tensor(comb[cyear==y],device=DEV,dtype=torch.float64)):+.2f}"
for y in range(2019, 2027) if (cyear == y).sum() > 30))
print("\nVERDICT: low |corr| + combined Sharpe > each alone = genuine cross-market diversification.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""The quant-fund method: combine MANY weak cross-sectional signals via a combiner ladder.
Assemble ~13 weak signals on the liquid US-equity universe, then run a ladder of combiners
from dumb to fancy and ask the disciplined question: does COMBINING beat the BEST SINGLE signal
OOS, and does the nonlinear ML beat the LINEAR combiner OOS (or just overfit the 3.2y)?
best-single -> equal-weight -> IC-weighted -> ridge(IS-fit) -> gradient-boost(IS-fit)
Strict IS/OOS split, realistic illiquidity-scaled cost, weekly rebalance. Reject if the
combination doesn't beat best-single + equal-weight OOS after deflation.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll, trailing # noqa: E402
from signal_sweep import xs_weights, validate, sharpe_t # noqa: E402
from sklearn.linear_model import Ridge # noqa: E402
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DV_FLOOR, TOPK = 5e6, 1000
def zc(x):
mu = np.nanmean(x, axis=1, keepdims=True); sd = np.nanstd(x, axis=1, keepdims=True)
return np.nan_to_num((x - mu) / np.where(sd > 0, sd, 1))
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
univ = np.zeros((T, N), bool)
for t in range(T):
e = np.where((dv30[t] > DV_FLOOR) & np.isfinite(close[t]))[0]
if len(e) > 50:
univ[t, e[np.argsort(-dv30[t, e])[:TOPK]]] = True
rmax252 = roll(np.max, lc, 252)
amih = roll(np.mean, np.abs(R) / np.maximum(np.nan_to_num(dvol), 1.0), 21)
# ~13 weak cross-sectional signals (each z-scored per day inside the universe)
raw = {
"mom_21": trailing(lc, 21), "mom_63": trailing(lc, 63), "mom_126": trailing(lc, 126),
"mom_252": trailing(lc, 252), "rev_5": -trailing(lc, 5), "rev_10": -trailing(lc, 10),
"lowvol": -vol63, "amihud": amih, "max20": -roll(np.max, R, 20),
"accel": trailing(lc, 10) - trailing(lc, 63), "hi52": lc - rmax252,
"vol_chg": roll(np.std, R, 21) - vol63, "skew63": -roll(lambda a, axis: ((a - a.mean(axis))**3).mean(axis) / (a.std(axis)**3 + 1e-9), R, 63),
}
names = list(raw)
sig = np.stack([np.where(univ, v, np.nan) for v in raw.values()], axis=2) # [T,N,K]
sigz = np.stack([zc(np.where(univ, v, np.nan)) for v in raw.values()], axis=2)
K = len(names)
fwd = np.full((T, N), np.nan); fwd[:-1] = R[1:] # next-day return
split = int(0.65 * T)
# pooled IS dataset for fitted combiners
ist = np.zeros(T, bool); ist[:split] = True
Xall = sigz.reshape(T * N, K)
yall = fwd.reshape(T * N)
rowok = np.isfinite(yall) & np.isfinite(Xall).all(1) & np.repeat(univ.reshape(T * N), 1)
isrow = rowok & np.repeat(ist[:, None], N, 1).reshape(T * N)
Xis, yis = Xall[isrow], yall[isrow]
print(f"signals K={K}, universe/day~{int(univ.sum(1).mean())}, IS obs={len(yis):,}")
ridge = Ridge(alpha=10.0).fit(Xis, yis)
gb = HistGradientBoostingRegressor(max_depth=3, max_iter=120, learning_rate=0.05,
l2_regularization=1.0, min_samples_leaf=200).fit(Xis, yis)
comp_ridge = (sigz.reshape(T * N, K) @ ridge.coef_).reshape(T, N)
Xpred = np.nan_to_num(sigz.reshape(T * N, K))
comp_gb = gb.predict(Xpred).reshape(T, N)
# IC-weighted (trailing 60d IC per signal, causal)
comp_ic = np.zeros((T, N))
for t in range(60, T):
w = []
for k in range(K):
a = sigz[t - 60:t, :, k].reshape(-1); b = fwd[t - 60:t].reshape(-1)
m = np.isfinite(a) & np.isfinite(b)
w.append(np.corrcoef(a[m], b[m])[0, 1] if m.sum() > 200 else 0.0)
comp_ic[t] = np.nan_to_num(sigz[t] @ np.nan_to_num(np.array(w)))
comp_eq = np.nansum(sigz, axis=2)
rt_cost = np.clip(40.0 / np.sqrt(np.maximum(dv30, 1.0) / 1e6), 3.0, 60.0) / 1e4
def book(comp):
s = comp.copy(); s[~univ] = np.nan
w = xs_weights(s)
a = 2.0 / 6
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
wh = w.copy(); last = 0
for t in range(T):
if t % 5 == 0:
last = t
wh[t] = w[last]
g = np.sum(wh[:-1] * R[1:], axis=1) - np.sum(np.abs(wh[1:] - wh[:-1]) * rt_cost[1:], axis=1)
return g
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
NT = K + 5
print(f"\n===== MULTI-SIGNAL COMBINATION (liquid equities, net cost, deflate N={NT}) =====")
# best single
singles = {nm: validate(book(sigz[:, :, k]), days, NT) for k, nm in enumerate(names)}
bs = max(singles.items(), key=lambda kv: kv[1]["oos"])
print(f"BEST SINGLE signal: {bs[0]} OOS {bs[1]['oos']:+.2f} full {bs[1]['full']:+.2f} DSR {bs[1]['dsr']:.2f}")
print(f"{'combiner':>14} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5}")
for nm, comp in [("equal", comp_eq), ("IC-weighted", comp_ic), ("ridge(IS)", comp_ridge), ("gradboost(IS)", comp_gb)]:
v = validate(book(comp), days, NT)
print(f"{nm:>14} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f}")
print("\nVERDICT: combination OOS > best-single OOS => combining works. gradboost OOS > ridge OOS => ML adds;")
print("gradboost < ridge => ML overfits 3.2y (linear is the real-quant answer). DSR>0.5 = deploy-grade.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Leak-free WALK-FORWARD validation of the multi-signal combiner — the decisive test.
The single IS/OOS gradboost result (OOS +1.14, DSR 0.62) is overfit-suspect (one short OOS
window, high-capacity model, only fitted combiners work). Real test: re-fit the combiner on an
EXPANDING window and predict ONLY the next block forward (never peeking). Concatenate the
out-of-sample predictions, build the book on the OOS period only, and compare gradboost vs ridge
vs equal vs best-single — all leak-free. If gradboost still wins OOS, it's real; if it collapses,
it was memorization.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll, trailing # noqa: E402
from signal_sweep import xs_weights, validate, sharpe_t # noqa: E402
from sklearn.linear_model import Ridge # noqa: E402
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DV_FLOOR, TOPK = 5e6, 1000
def zc(x):
mu = np.nanmean(x, axis=1, keepdims=True); sd = np.nanstd(x, axis=1, keepdims=True)
return np.nan_to_num((x - mu) / np.where(sd > 0, sd, 1))
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
univ = np.zeros((T, N), bool)
for t in range(T):
e = np.where((dv30[t] > DV_FLOOR) & np.isfinite(close[t]))[0]
if len(e) > 50:
univ[t, e[np.argsort(-dv30[t, e])[:TOPK]]] = True
rmax252 = roll(np.max, lc, 252)
amih = roll(np.mean, np.abs(R) / np.maximum(np.nan_to_num(dvol), 1.0), 21)
raw = {"mom_21": trailing(lc, 21), "mom_63": trailing(lc, 63), "mom_126": trailing(lc, 126),
"mom_252": trailing(lc, 252), "rev_5": -trailing(lc, 5), "rev_10": -trailing(lc, 10),
"lowvol": -vol63, "amihud": amih, "max20": -roll(np.max, R, 20),
"accel": trailing(lc, 10) - trailing(lc, 63), "hi52": lc - rmax252,
"vol_chg": roll(np.std, R, 21) - vol63}
names = list(raw); K = len(names)
sigz = np.stack([zc(np.where(univ, v, np.nan)) for v in raw.values()], axis=2) # [T,N,K] causal
fwd = np.full((T, N), np.nan); fwd[:-1] = R[1:]
rt_cost = np.clip(40.0 / np.sqrt(np.maximum(dv30, 1.0) / 1e6), 3.0, 60.0) / 1e4
INIT, STEP = int(0.45 * T), 42 # ~1.4y initial, refit every ~2mo
comp_gb = np.full((T, N), np.nan); comp_ri = np.full((T, N), np.nan)
Xall = sigz.reshape(T * N, K); yall = fwd.reshape(T * N); uflat = univ.reshape(T * N)
for s in range(INIT, T, STEP):
e = min(s + STEP, T)
tr = np.zeros(T, bool); tr[:s] = True
rows = np.repeat(tr[:, None], N, 1).reshape(T * N) & uflat & np.isfinite(yall) & np.isfinite(Xall).all(1)
Xtr, ytr = Xall[rows], yall[rows]
if len(ytr) < 5000:
continue
ri = Ridge(alpha=10.0).fit(Xtr, ytr)
gb = HistGradientBoostingRegressor(max_depth=3, max_iter=120, learning_rate=0.05,
l2_regularization=1.0, min_samples_leaf=200).fit(Xtr, ytr)
blk = sigz[s:e].reshape((e - s) * N, K)
comp_ri[s:e] = (np.nan_to_num(blk) @ ri.coef_).reshape(e - s, N)
comp_gb[s:e] = gb.predict(np.nan_to_num(blk)).reshape(e - s, N)
def book(comp):
c = comp.copy(); c[~univ] = np.nan
w = xs_weights(c)
a = 2.0 / 6
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
wh = w.copy(); last = 0
for t in range(T):
if t % 5 == 0:
last = t
wh[t] = w[last]
return np.sum(wh[:-1] * R[1:], axis=1) - np.sum(np.abs(wh[1:] - wh[:-1]) * rt_cost[1:], axis=1)
# OOS period = [INIT:] only
oos = np.zeros(T - 1, bool); oos[INIT:] = True
T_ = lambda x: torch.tensor(np.asarray(x)[INIT:][np.isfinite(np.asarray(x)[INIT:])], device=DEV, dtype=torch.float64)
yr = year[1:]
eqp = book(np.nansum(sigz, axis=2))
best = max(range(K), key=lambda k: sharpe_t(T_(book(sigz[:, :, k]))))
print(f"\n===== WALK-FORWARD (leak-free) MULTI-SIGNAL COMBINE — OOS only ({int(oos.sum())} days) =====")
print(f"K={K} signals, refit every {STEP}d on expanding window")
def line(nm, p):
po = np.asarray(p)
v = validate(po[INIT:], days, K + 4)
print(f"{nm:>18} OOS Sharpe {sharpe_t(T_(p)):+.2f} CPCVmed {v['med']:+.2f} DSR {v['dsr']:.2f}")
line(f"best-single({names[best]})", book(sigz[:, :, best]))
line("equal-weight", eqp)
line("ridge WALK-FWD", comp_ri)
line("gradboost WALK-FWD", comp_gb)
print("\nVERDICT: gradboost WF OOS > ridge WF and > best-single => the ML combination is REAL (leak-free).")
print("If gradboost WF collapses to ~ridge or below => the single-split +1.14 was memorization.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Nano multi-strat: operate like a hedge fund. Combine uncorrelated premia (equity, bond, gold,
commodity, trend, crypto) risk-parity-weighted + vol-targeted; show the correlation matrix (the
diversification engine), combined Sharpe vs each piece alone, and how leverage scales the return.
Not market-neutral — it's the diversified-premia + leverage (All-Weather/alt-risk-premia) model.
The point: each stream is modest, but uncorrelated streams combine to a higher Sharpe, then leverage
turns modest Sharpe into real return. Risk management = the engine's actual job."""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import load_panel, build_returns # noqa: E402
import pit_sweep # noqa: E402
TV = 0.10
def vt(r, tv=TV, win=63, cap=4.0):
out = np.zeros_like(r)
for t in range(win, len(r)):
rv = np.std(r[t - win:t]) * math.sqrt(252)
out[t] = r[t] * min(cap, tv / (rv + 1e-9))
return out
def stats(r):
r = r[np.isfinite(r)]
if len(r) < 50 or r.std() == 0:
return (float("nan"),) * 4
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann, vol, ann / vol, dd
def run(streams, days, year, label):
names = list(streams)
M = np.column_stack([vt(streams[n]) for n in names]) # each vol-normalized to 10%
T = M.shape[0]
print(f"\n===== {label} ({T} days) =====")
print(" standalone Sharpe: " + " ".join(f"{n}:{stats(streams[n])[2]:+.2f}" for n in names))
# correlation matrix of the (vol-normalized) streams
C = np.corrcoef(np.nan_to_num(M).T)
print(" correlation matrix:")
print(" " + " ".join(f"{n[:5]:>6}" for n in names))
for i, n in enumerate(names):
print(f" {n[:5]:>5} " + " ".join(f"{C[i, j]:>+6.2f}" for j in range(len(names))))
avg_corr = (C.sum() - len(names)) / (len(names) ** 2 - len(names))
# combined: equal-risk-weight then vol-target the bundle
combo = vt(np.nanmean(M, axis=1))
a, v, sh, dd = stats(combo)
best = max(stats(streams[n])[2] for n in names)
print(f" avg pairwise corr: {avg_corr:+.2f} (low = diversification works)")
print(f" COMBINED (risk-parity + vol-target 10%): Sharpe {sh:+.2f} ann {100*a:+.1f}% maxDD {100*dd:+.1f}%")
print(f" vs best single stream Sharpe {best:+.2f} -> diversification lift {sh-best:+.2f}")
print(f" per-year Sharpe: " + " ".join(f"{y}:{stats(combo[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 100))
print(f" LEVERAGE scaling (same Sharpe {sh:+.2f}): "
+ " ".join(f"{lev}x->{100*a*lev:+.0f}%/yr@{int(100*v*lev)}%vol" for lev in (1, 2, 3)))
return combo
def main():
roots, days, close, op, inst = load_panel()[:5]
lc, R, ov, intr = build_returns(close, op, inst)
T, N = close.shape
year = (1970 + days / 365.25).astype(int)
def C(r):
return roots.index(r)
# trend: diversified long/short TS-momentum across all futures, vol-targeted
vol63 = np.full_like(lc, np.nan)
for t in range(63, T):
vol63[t] = np.nanstd(R[t - 63:t], axis=0)
iv = 1.0 / np.where(vol63 > 0, vol63, np.nan)
tsig = np.full_like(lc, np.nan); tsig[252:] = np.sign(lc[252:] - lc[:-252])
avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0) & np.isfinite(tsig)
w = np.where(avail, tsig * iv, 0.0); g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
trend = np.zeros(T); trend[1:] = np.sum(w[:-1] * R[1:], axis=1)
fut = {"equity": R[:, C("ES")], "bond": R[:, C("ZN")], "gold": R[:, C("GC")],
"commod": R[:, C("CL")], "trend": trend}
run(fut, days, year, "TRADITIONAL 5-stream (2010-2026)")
# +crypto: align BTC daily returns to futures days
syms, cdays, cc, _, _ = pit_sweep.load()
j = syms.index("BTCUSDT"); lcb = np.log(cc[:, j])
btc = {int(cdays[t]): (lcb[t] - lcb[t - 1]) for t in range(1, len(cdays)) if np.isfinite(lcb[t]) and np.isfinite(lcb[t - 1])}
mask = np.array([int(d) in btc for d in days])
idx = np.where(mask)[0]
if len(idx) > 300:
sub = {k: v[idx] for k, v in fut.items()}
sub["crypto"] = np.array([btc[int(days[i])] for i in idx])
run(sub, days[idx], (1970 + days[idx] / 365.25).astype(int), "6-stream +CRYPTO (2019-2026)")
print("\nVERDICT: combined Sharpe > best single (diversification real) + leverage scales modest Sharpe")
print("to real return = the hedge-fund operating model. Honest: this is alt-risk-premia (~0.5-1.0 live),")
print("levered; NOT market-neutral (long beta falls in everything-down); leverage adds tail + financing.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Deployable nano multi-strat book with 2x leverage + the foxhunt risk-management layer.
Streams (risk-parity, vol-normalized): equity, bond, gold, commodity, trend, crypto.
RISK LAYER (the foxhunt principles applied to the book):
1. vol-target -> scale exposure to TARGET_VOL on trailing realized vol
2. drawdown CB -> de-lever at -15% (1x) / -25% (0.5x) / -35% (0.25x) [CMDP circuit-breaker]
3. corr de-risk -> when avg cross-stream corr spikes (diversification breaking in a crisis), cut leverage
4. Kelly cap -> leverage <= Kelly fraction (mean/var), never over-lever a degrading book
L[t] = min(MAXLEV, L_vol, L_kelly) * dd_mult * corr_mult, applied to yesterday's signal.
Financing cost charged on the borrowed (>1x) portion. Backtest risk-managed-2x vs naive-2x; emit
live target weights for a given capital. ETF mapping for deployment in the verdict.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import load_panel, build_returns # noqa: E402
import pit_sweep # noqa: E402
TARGET_VOL = 0.10
MAXLEV = 2.0
FIN = 0.06 # 6%/yr financing on borrowed portion
DD1, DD2, DD3 = -0.15, -0.25, -0.35
CORR_HI = 0.45 # avg pairwise corr above which to de-risk
def volnorm(r, tv=TARGET_VOL, win=63):
out = np.zeros_like(r)
for t in range(win, len(r)):
rv = np.std(r[t - win:t]) * math.sqrt(252)
out[t] = r[t] * min(5.0, tv / (rv + 1e-9))
return out
def stats(r):
r = r[np.isfinite(r)]
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann, vol, (ann / vol if vol > 0 else float("nan")), dd
def build_streams():
roots, days, close, op, inst = load_panel()[:5]
lc, R, ov, intr = build_returns(close, op, inst)
T, N = close.shape
vol63 = np.full_like(lc, np.nan)
for t in range(63, T):
vol63[t] = np.nanstd(R[t - 63:t], axis=0)
iv = 1.0 / np.where(vol63 > 0, vol63, np.nan)
tsig = np.full_like(lc, np.nan); tsig[252:] = np.sign(lc[252:] - lc[:-252])
avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0) & np.isfinite(tsig)
w = np.where(avail, tsig * iv, 0.0); g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
trend = np.zeros(T); trend[1:] = np.sum(w[:-1] * R[1:], axis=1)
c = {r: roots.index(r) for r in ("ES", "ZN", "GC", "CL")}
fut = {"equity": R[:, c["ES"]], "bond": R[:, c["ZN"]], "gold": R[:, c["GC"]], "commod": R[:, c["CL"]], "trend": trend}
syms, cdays, cc, _, _ = pit_sweep.load()
j = syms.index("BTCUSDT"); lcb = np.log(cc[:, j])
btc = {int(cdays[t]): (lcb[t] - lcb[t - 1]) for t in range(1, len(cdays)) if np.isfinite(lcb[t]) and np.isfinite(lcb[t - 1])}
idx = np.array([i for i, d in enumerate(days) if int(d) in btc])
streams = {k: v[idx] for k, v in fut.items()}
streams["crypto"] = np.array([btc[int(days[i])] for i in idx])
return streams, days[idx], (1970 + days[idx] / 365.25).astype(int)
def risk_managed(M, maxlev, with_risk=True):
"""M: [T, S] vol-normalized stream returns. Returns book daily returns under the risk layer."""
base = np.nanmean(M, axis=1) # risk-parity combination
T = len(base)
out = np.zeros(T); eq = 1.0; peak = 1.0
for t in range(63, T - 1):
rv = np.std(base[t - 63:t]) * math.sqrt(252)
L = min(maxlev, TARGET_VOL / (rv + 1e-9)) # (1) vol-target, capped at maxlev
if with_risk:
mu = base[t - 63:t].mean() * 252; var = (base[t - 63:t].std() ** 2) * 252
L = min(L, max(0.0, mu / (var + 1e-9))) # (4) Kelly cap
dd = eq / peak - 1.0 # (2) drawdown circuit-breaker
ddm = 1.0 if dd > DD1 else (0.5 if dd > DD2 else (0.25 if dd > DD3 else 0.0))
cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T) # (3) correlation de-risk
ac = (cm.sum() - cm.shape[0]) / (cm.shape[0] ** 2 - cm.shape[0])
corrm = 1.0 if ac < CORR_HI else max(0.4, 1 - (ac - CORR_HI) * 2)
L *= ddm * corrm
L = max(0.0, min(L, maxlev))
r = L * base[t + 1] - FIN * max(L - 1.0, 0.0) / 252 # financing on borrowed portion
out[t + 1] = r
eq *= (1 + r); peak = max(peak, eq)
return out
def main():
streams, days, year = build_streams()
names = list(streams)
M = np.column_stack([volnorm(streams[n]) for n in names])
print(f"DEPLOYABLE MULTI-STRAT BOOK — {len(names)} streams, {M.shape[0]} days ({names})")
for lab, wr, lev in [("risk-managed 2x", True, 2.0), ("NAIVE 2x (no risk layer)", False, 2.0),
("risk-managed 1x", True, 1.0)]:
r = risk_managed(M, lev, wr)
a, v, sh, dd = stats(r)
print(f" {lab:>26}: Sharpe {sh:+.2f} ann {100*a:+.1f}% vol {100*v:.0f}% maxDD {100*dd:+.1f}%")
rm = risk_managed(M, 2.0, True)
print(f" per-year Sharpe (risk-managed 2x): " + " ".join(f"{y}:{stats(rm[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 150))
# live sizing: current leverage + target $ per stream for capital
base = np.nanmean(M, axis=1); t = len(base) - 1
rv = np.std(base[t - 63:t]) * math.sqrt(252)
Lv = min(MAXLEV, TARGET_VOL / (rv + 1e-9))
cap = 35000
print(f"\n LIVE SIZING (today): vol-target leverage {Lv:.2f}x -> deploy ${cap*Lv:,.0f} gross on ${cap:,.0f}")
per = cap * Lv / len(names)
etf = {"equity": "SPY", "bond": "IEF", "gold": "GLD", "commod": "PDBC", "trend": "DBMF", "crypto": "BTC(spot)"}
for n in names:
print(f" {n:>7} ({etf[n]:>9}): ${per:,.0f}")
print("\n VERDICT: risk-managed 2x should keep maxDD bounded (~-20-30%) vs naive 2x (~-40-50%) at higher")
print(" return than 1x -> the hedge-fund model at retail. Deploy via the ETFs above (2x Reg-T margin) +")
print(" small crypto sleeve. Same ~0.7-Sharpe book; leverage+risk-layer = the fund. Honest: still beta,")
print(" drawdowns real, financing drags, single-window crypto sleeve -> size crypto small.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Multi-strat book v2 — upgraded with foxhunt risk-stack IDEAS (not crude reimplementation):
- PER-STREAM EDGE-DECAY TRUST (Page-Hinkley-style decay detection -> continuous theta in [0,1])
that down-weights a stream whose premium is degrading and re-weights it when it recovers
(resurrection discipline) -> ADAPTIVE allocation vs static risk-parity.
[pearl_edge_decay_detection_is_a_missing_abstraction_layer + dead_signal_resurrection_discipline]
- KELLY-fraction leverage cap [pearl_position_sizing_missing_adaptation_layer]
- CMDP drawdown circuit-breaker [pearl_cmdp_consec_loss_counter]
- correlation-spike de-risk (diversification breaks in crises)
Compares static-risk-parity vs edge-decay-adaptive, both risk-managed 2x. Does the foxhunt idea help?
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_book import build_streams, volnorm, stats, TARGET_VOL, FIN, DD1, DD2, DD3, CORR_HI # noqa: E402
def edge_decay_trust(stream, win=126, ema=0.94):
"""Page-Hinkley-style per-stream edge health -> theta in [0.1,1]. Detects decay (trailing
risk-adj return falling), floors at 0.1 so a dead stream can RESURRECT when it recovers."""
T = len(stream); theta = np.ones(T)
th = 1.0
for t in range(win, T):
seg = stream[t - win:t]
sr = seg.mean() / (seg.std() + 1e-9) * math.sqrt(252)
target = float(np.clip((sr - (-0.5)) / (0.5 - (-0.5)), 0.1, 1.0)) # SR>0.5 ->1, <-0.5 ->0.1
th = ema * th + (1 - ema) * target # smooth (asymmetric-ish via EMA)
theta[t] = th
return theta
def risk_layer(combo, M, maxlev, with_risk=True):
T = len(combo); out = np.zeros(T); eq = 1.0; peak = 1.0
for t in range(63, T - 1):
rv = np.std(combo[t - 63:t]) * math.sqrt(252)
L = min(maxlev, TARGET_VOL / (rv + 1e-9))
if with_risk:
mu = combo[t - 63:t].mean() * 252; var = (combo[t - 63:t].std() ** 2) * 252
L = min(L, max(0.0, mu / (var + 1e-9)))
dd = eq / peak - 1.0
ddm = 1.0 if dd > DD1 else (0.5 if dd > DD2 else (0.25 if dd > DD3 else 0.0))
cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T)
ac = (cm.sum() - cm.shape[0]) / (cm.shape[0] ** 2 - cm.shape[0])
corrm = 1.0 if ac < CORR_HI else max(0.4, 1 - (ac - CORR_HI) * 2)
L *= ddm * corrm
L = max(0.0, min(L, maxlev))
r = L * combo[t + 1] - FIN * max(L - 1.0, 0.0) / 252
out[t + 1] = r; eq *= (1 + r); peak = max(peak, eq)
return out
def main():
streams, days, year = build_streams()
names = list(streams)
M = np.column_stack([volnorm(streams[n]) for n in names])
T = M.shape[0]
# edge-decay trust per stream
Theta = np.column_stack([edge_decay_trust(M[:, i]) for i in range(len(names))])
static = np.nanmean(M, axis=1) # equal risk-parity
tw = Theta / np.maximum(Theta.sum(1, keepdims=True), 1e-9) # trust-weighted
adaptive = np.nansum(tw * np.nan_to_num(M), axis=1)
print(f"MULTI-STRAT v2 (foxhunt edge-decay-adaptive) — {len(names)} streams, {T} days")
print(f" streams: {names}")
for lab, combo in [("static risk-parity", static), ("edge-decay ADAPTIVE", adaptive)]:
for ln, lev, wr in [("2x risk-managed", 2.0, True), ("naive 2x", 2.0, False)]:
r = risk_layer(combo, M, lev, wr); a, v, sh, dd = stats(r)
print(f" {lab:>20} | {ln:>16}: Sharpe {sh:+.2f} ann {100*a:+.1f}% maxDD {100*dd:+.1f}%")
# show what the trust layer is doing (avg theta per stream + recent)
print(" edge-trust (avg | latest) per stream:")
for i, n in enumerate(names):
print(f" {n:>7}: {Theta[126:, i].mean():.2f} | {Theta[-1, i]:.2f}")
radap = risk_layer(adaptive, M, 2.0, True)
print(f" per-year Sharpe (adaptive 2x): " + " ".join(f"{y}:{stats(radap[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 150))
print("\n VERDICT: edge-decay-adaptive Sharpe > static AND lower maxDD = the foxhunt trust-layer idea adds")
print(" real value (down-weights decaying streams, resurrects recovered ones). If ~equal, static suffices.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Multi-strat book v3 — risk layer rebuilt with foxhunt's ADAPTIVE-controller discipline.
Fixes the v1/v2 static risk layer (which crushed returns — a one-way latch per
pearl_cmdp_consec_loss_counter_is_one_way_latch). Controllers (all floored / resurrection-capable):
- EMA online vol (not fixed-window std) [Welford/EMA online stats]
- Kelly leverage with FLOOR + bootstrap [pearl_bootstrap_must_respect_clamp_range]
- drawdown de-lever CONTINUOUS + self-recovering [fix the one-way latch -> resurrection]
- correlation de-risk Z-SCORED vs own distribution [adaptive, not fixed threshold]
- leverage floor so nothing dies permanently [pearl_dead_signal_resurrection_discipline]
Combination = edge-decay-adaptive (v2). Compare naive 2x / static-risk 2x / ADAPTIVE-risk 2x.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_book import build_streams, volnorm, stats, TARGET_VOL, FIN, risk_managed # noqa: E402
from multistrat_book_v2 import edge_decay_trust # noqa: E402
KELLY_FLOOR, LEV_FLOOR = 0.5, 0.3
DD_DEADBAND, DD_SENS, DD_FLOOR = 0.05, 3.0, 0.40
def adaptive_risk(combo, M, maxlev):
T = len(combo); out = np.zeros(T); eq = 1.0; peak = 1.0
ema_mu = float(np.nanmean(combo[:63])); ema_var = float(np.nanvar(combo[:63])) + 1e-12
chist = []
for t in range(63, T - 1):
x = combo[t]
ema_mu = 0.97 * ema_mu + 0.03 * x # EMA online stats
ema_var = 0.97 * ema_var + 0.03 * (x - ema_mu) ** 2
rv = math.sqrt(max(ema_var, 1e-12) * 252)
L_vol = TARGET_VOL / (rv + 1e-9)
kelly = min(maxlev, max(KELLY_FLOOR, (ema_mu * 252) / (ema_var * 252 + 1e-9))) # floored Kelly
dd = eq / peak - 1.0
dd_mult = float(np.clip(1.0 - DD_SENS * max(0.0, -dd - DD_DEADBAND), DD_FLOOR, 1.0)) # continuous + recovers
cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T)
ac = (cm.sum() - cm.shape[0]) / (cm.shape[0] ** 2 - cm.shape[0])
chist.append(ac)
if len(chist) > 60:
h = np.array(chist[-120:]); z = (ac - h.mean()) / (h.std() + 1e-9)
corr_mult = float(np.clip(1.0 - 0.20 * max(0.0, z), 0.5, 1.0))
else:
corr_mult = 1.0
L = float(np.clip(min(L_vol, kelly) * dd_mult * corr_mult, LEV_FLOOR, maxlev))
r = L * combo[t + 1] - FIN * max(L - 1.0, 0.0) / 252
out[t + 1] = r; eq *= (1 + r); peak = max(peak, eq)
return out
def main():
streams, days, year = build_streams()
names = list(streams)
M = np.column_stack([volnorm(streams[n]) for n in names])
Theta = np.column_stack([edge_decay_trust(M[:, i]) for i in range(len(names))])
tw = Theta / np.maximum(Theta.sum(1, keepdims=True), 1e-9)
combo = np.nansum(tw * np.nan_to_num(M), axis=1) # edge-decay-adaptive combination
print(f"MULTI-STRAT v3 (adaptive allocation + adaptive risk layer) — {len(names)} streams, {M.shape[0]} days")
res = {
"naive 2x (no risk layer)": np.concatenate([[0], 2.0 * combo[1:] - FIN * 1.0 / 252]),
"static risk layer 2x": risk_managed(M, 2.0, True), # v1 static (recomputes nanmean inside)
"ADAPTIVE risk layer 2x": adaptive_risk(combo, M, 2.0),
"ADAPTIVE risk layer 1x": adaptive_risk(combo, M, 1.0),
}
for lab, r in res.items():
a, v, sh, dd = stats(r)
print(f" {lab:>26}: Sharpe {sh:+.2f} ann {100*a:+.1f}% vol {100*v:.0f}% maxDD {100*dd:+.1f}%")
radap = res["ADAPTIVE risk layer 2x"]
print(f" per-year Sharpe (ADAPTIVE 2x): " + " ".join(f"{y}:{stats(radap[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 150))
print("\n VERDICT: ADAPTIVE-risk 2x should beat naive 2x AND static-risk 2x on Sharpe AND maxDD =")
print(" proper foxhunt-style risk management makes leverage survivable without killing return.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""Production rebalance bot for the adaptive multi-strat book on IBKR — lean enterprise core.
Only what money-touching code actually needs: env config (no secrets in code), paper/live guard,
leverage cap, drawdown circuit-breaker (HWM-based), once-per-period idempotency, post-trade
reconciliation, structured JSON audit log. Default DRY-RUN. Designed to run from cron.
Run:
python3 multistrat_bot.py status # account + targets + gates, no trading
python3 multistrat_bot.py dry # full plan incl. orders, no placement (default)
python3 multistrat_bot.py run # place orders (still requires MULTISTRAT_EXECUTE=true)
Env (all optional unless noted):
IB_HOST=127.0.0.1 IB_PORT=4002 IB_CLIENT_ID=7
MULTISTRAT_MAXLEV=1.0 gross exposure cap (× NLV); book is unlevered
MULTISTRAT_HYST=0.03 rebalance hysteresis (× NLV); entry-from-zero uses 0.5% floor
MULTISTRAT_REBALANCE_DAYS=7 min days between rebalances (idempotency)
MULTISTRAT_DD_HALT=0.20 halt trading if NLV < HWM·(1this)
MULTISTRAT_MAX_ORDER=0.30 refuse any single order > this × NLV (fat-finger guard)
MULTISTRAT_EXECUTE=false must be "true" for `run` to actually place
MULTISTRAT_ALLOW_LIVE_CONFIRMED must equal "I_UNDERSTAND_REAL_MONEY" to trade a non-paper acct
MULTISTRAT_STATE / MULTISTRAT_LOG override state/log paths
"""
import datetime as dt
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import build, book_series, INSTR # noqa: E402
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TICKER = {"equity": "SPY", "bond": "IEF", "gold": "GLD", "commod": "PDBC", "trend": "DBMF", "crypto": "IBIT"}
def cfg(k, d):
return os.environ.get(k, d)
C = {
"host": cfg("IB_HOST", "127.0.0.1"), "port": int(cfg("IB_PORT", "4002")), "cid": int(cfg("IB_CLIENT_ID", "7")),
"maxlev": float(cfg("MULTISTRAT_MAXLEV", "1.0")), "hyst": float(cfg("MULTISTRAT_HYST", "0.03")),
"rebal_days": int(cfg("MULTISTRAT_REBALANCE_DAYS", "7")), "dd_halt": float(cfg("MULTISTRAT_DD_HALT", "0.20")),
"max_order": float(cfg("MULTISTRAT_MAX_ORDER", "0.30")),
"data_tol": float(cfg("MULTISTRAT_DATA_TOL", "0.25")), # halt if broker summary vs positions disagree > this
"execute": cfg("MULTISTRAT_EXECUTE", "false").lower() == "true",
"live_ok": cfg("MULTISTRAT_ALLOW_LIVE_CONFIRMED", "") == "I_UNDERSTAND_REAL_MONEY",
"state": cfg("MULTISTRAT_STATE", os.path.join(_REPO, "data/surfer/multistrat_bot_state.json")),
"log": cfg("MULTISTRAT_LOG", os.path.join(_REPO, "data/surfer/multistrat_bot.log")),
}
def log(level, event, **kv):
rec = {"ts": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"), "level": level, "event": event, **kv}
os.makedirs(os.path.dirname(C["log"]), exist_ok=True)
with open(C["log"], "a") as f:
f.write(json.dumps(rec) + "\n")
print(f"[{level}] {event} " + " ".join(f"{k}={v}" for k, v in kv.items()))
def load_state():
try:
return json.load(open(C["state"]))
except Exception:
return {"last_rebalance": None, "hwm_nlv": 0.0, "rebalances": 0}
def save_state(s):
os.makedirs(os.path.dirname(C["state"]), exist_ok=True)
json.dump(s, open(C["state"], "w"), indent=2)
def yhist_last(sym):
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=5d",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
ts = res["timestamp"]; c = [x for x in res["indicators"]["quote"][0]["close"] if x is not None]
age_days = (dt.datetime.now(dt.timezone.utc) - dt.datetime.fromtimestamp(ts[-1], dt.timezone.utc)).days
return float(c[-1]), age_days
def target_weights():
_, R = build()
_, w, lev = book_series(R)
return {TICKER[nm]: float(w[j] * lev) for j, (_, nm) in enumerate(INSTR)}
def gates(nlv, acct_is_paper, price_age, tw, state, data_gap=0.0):
"""Pre-trade safety gates. Returns (ok, [reasons])."""
fail = []
if not acct_is_paper and not C["live_ok"]:
fail.append("LIVE account but MULTISTRAT_ALLOW_LIVE_CONFIRMED not set — refusing to trade real money")
if nlv <= 0:
fail.append("NLV <= 0")
if data_gap > C["data_tol"]:
fail.append(f"ACCOUNT DATA INCONSISTENT: NLV vs cash+positions gap {100*data_gap:.0f}% > {100*C['data_tol']:.0f}% — broker data unreliable, refusing to size")
gross = sum(tw.values())
if gross > C["maxlev"] + 1e-6:
fail.append(f"gross exposure {gross:.2f} > maxlev {C['maxlev']}")
if price_age > 4:
fail.append(f"stale prices ({price_age}d old) — data feed issue")
hwm = max(state.get("hwm_nlv", 0.0), nlv)
if hwm > 0 and nlv < hwm * (1 - C["dd_halt"]):
fail.append(f"DRAWDOWN CIRCUIT-BREAKER: NLV ${nlv:,.0f} < HWM ${hwm:,.0f}·(1-{C['dd_halt']}) — halting")
return (len(fail) == 0, fail)
def main():
mode = sys.argv[1] if len(sys.argv) > 1 else "dry"
state = load_state()
tw = target_weights()
prices, max_age = {}, 0
for t in tw:
px, age = yhist_last(t); prices[t] = px; max_age = max(max_age, age)
log("INFO", "targets", mode=mode, weights={t: round(w, 3) for t, w in tw.items()}, price_age_d=max_age)
from ib_async import IB, Stock, MarketOrder
ib = IB()
last_err = None
for attempt in range(3):
try:
ib.connect(C["host"], C["port"], clientId=C["cid"], timeout=15); break
except Exception as e:
last_err = e; ib.sleep(3)
if not ib.isConnected():
log("ERROR", "connect_failed", host=C["host"], port=C["port"], err=str(last_err)[:80]); return 1
try:
accts = ib.managedAccounts(); acct = accts[0] if accts else "?"
is_paper = acct.startswith("DU")
summ = {v.tag: float(v.value) for v in ib.accountSummary() if v.tag in ("NetLiquidation", "TotalCashValue")}
nlv = summ.get("NetLiquidation", 0.0); cash = summ.get("TotalCashValue", 0.0)
port_mv = sum(it.marketValue for it in ib.portfolio())
pos = {p.contract.symbol: p.position for p in ib.positions()}
# IBKR (esp. paper) can report summary fields inconsistent with actual positions. Cross-check
# NLV against an independent estimate (cash + actual position market value); size on the
# CONSERVATIVE (lower) value so a glitch never inflates positions; gate if they disagree badly.
est_portfolio = cash + port_mv
data_gap = abs(nlv - est_portfolio) / max(nlv, est_portfolio, 1.0)
rel_nlv = min(nlv, est_portfolio) if (nlv > 0 and est_portfolio > 0) else max(nlv, est_portfolio)
log("INFO", "account", id=acct, paper=is_paper, nlv=round(nlv), cash=round(cash), port_mv=round(port_mv), reliable_nlv=round(rel_nlv), data_gap=round(data_gap, 3), positions=pos or None)
if data_gap > 0.02:
log("WARN", "account_data_inconsistent", nlv=round(nlv), cash_plus_positions=round(est_portfolio), gap_pct=round(100 * data_gap, 1), note="broker summary disagrees with actual positions (IBKR paper quirk) — sizing on conservative NLV")
ok, reasons = gates(rel_nlv, is_paper, max_age, tw, state, data_gap)
for r in reasons:
log("CRITICAL" if "CIRCUIT" in r or "LIVE" in r or "INCONSISTENT" in r else "WARN", "gate_fail", reason=r)
if not ok:
log("ERROR", "aborted", gates_failed=len(reasons)); return 2
# update high-water mark (only after gates pass = healthy NLV)
state["hwm_nlv"] = max(state.get("hwm_nlv", 0.0), rel_nlv)
# idempotency: skip a `run` if rebalanced within the window
if mode == "run" and state.get("last_rebalance"):
last = dt.date.fromisoformat(state["last_rebalance"])
age = (dt.date.today() - last).days
if age < C["rebal_days"]:
log("INFO", "skip_idempotent", days_since=age, window=C["rebal_days"]); return 0
# compute orders (entry-from-zero floor 0.5% NLV; rebalance uses hysteresis)
orders = []
for t, w in tw.items():
tgt = rel_nlv * w / prices[t]; cur = pos.get(t, 0.0); delta = tgt - cur
thresh = (0.005 if cur == 0 else C["hyst"]) * rel_nlv
val = abs(delta * prices[t])
if val <= thresh:
continue
if val > C["max_order"] * rel_nlv:
log("WARN", "order_capped", ticker=t, value=round(val), cap=round(C["max_order"] * nlv))
qty = int(round(abs(delta))) # whole shares — IBKR API rejects fractional (error 10243)
if qty == 0:
log("INFO", "skip_subshare", ticker=t, note="rounds to 0 whole shares")
continue
orders.append((t, "BUY" if delta > 0 else "SELL", qty, round(val)))
for t, side, qty, val in orders:
log("INFO", "planned_order", ticker=t, side=side, qty=qty, value=val)
if not orders:
log("INFO", "in_band", note="book within hysteresis — nothing to do"); return 0
if mode != "run":
log("INFO", "dry_run", n_orders=len(orders), note="set mode=run + MULTISTRAT_EXECUTE=true to place"); return 0
if not C["execute"]:
log("WARN", "execute_disabled", note="MULTISTRAT_EXECUTE != true — not placing"); return 0
# place + reconcile
placed = []
for t, side, qty, val in orders:
try:
c = Stock(t, "SMART", "USD"); ib.qualifyContracts(c)
o = MarketOrder(side, qty); o.account = acct
tr = ib.placeOrder(c, o); ib.sleep(2)
log("INFO", "order_sent", ticker=t, side=side, qty=qty, status=tr.orderStatus.status)
placed.append(tr)
except Exception as e:
log("ERROR", "order_failed", ticker=t, err=str(e)[:120]) # continue — partial book is recoverable next run
ib.sleep(3)
accepted = sum(1 for tr in placed if tr.orderStatus.status not in ("Cancelled", "ApiCancelled", "Inactive"))
filled = sum(1 for tr in placed if tr.orderStatus.status == "Filled")
newpos = {p.contract.symbol: p.position for p in ib.positions()}
if accepted > 0: # mark the period done only if orders were actually accepted (not all rejected)
state["last_rebalance"] = dt.date.today().isoformat(); state["rebalances"] = state.get("rebalances", 0) + 1
else:
log("WARN", "no_orders_accepted", note="all rejected — not marking rebalance; will retry next run")
log("INFO", "reconcile", placed=len(placed), accepted=accepted, filled=filled, positions=newpos or None)
return 0
finally:
save_state(state)
ib.disconnect()
if __name__ == "__main__":
sys.exit(main() or 0)

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Full drawdown profile of the deployable ETF book (not just max-DD): depth, frequency, duration,
time-underwater, recovery, worst episodes, ulcer index. The live-experience picture."""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import book_series, INSTR # noqa: E402
def yhist(sym):
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=10y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
ts = res["timestamp"]; ind = res["indicators"]
adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None}
def dd_profile(book, dates, label):
eq = np.cumprod(1 + book)
peak = np.maximum.accumulate(eq)
dd = eq / peak - 1.0
# episodes: contiguous underwater stretches
eps = []
i = 0
while i < len(dd):
if dd[i] < -0.005:
j = i
while j < len(dd) and dd[j] < -0.0001:
j += 1
seg = dd[i:j]; trough = i + int(np.argmin(seg))
eps.append((i, trough, min(j, len(dd) - 1), float(seg.min())))
i = j
else:
i += 1
eps.sort(key=lambda e: e[3])
underwater = float((dd < -0.005).mean())
# longest underwater stretch (days)
longest = cur = 0
for x in dd:
cur = cur + 1 if x < -0.005 else 0; longest = max(longest, cur)
ulcer = float(np.sqrt(np.mean((dd * 100) ** 2)))
ann = book[np.isfinite(book)].mean() * 252
print(f"\n===== {label} drawdown profile ({len(dates)}d) =====")
print(f" max DD {100*dd.min():+.1f}% | avg DD (when underwater) {100*dd[dd<-0.005].mean():+.1f}% | % time underwater {100*underwater:.0f}%")
print(f" longest underwater stretch: {longest} trading days (~{longest/21:.1f} months)")
print(f" ulcer index {ulcer:.2f} | Calmar (ann/|maxDD|) {ann/abs(dd.min()+1e-9):.2f} | # drawdowns >2%: {sum(1 for e in eps if e[3]<-0.02)}")
print(f" worst 5 episodes (depth | peak->trough->recovery, days):")
for s, tr, rec, dep in eps[:5]:
recd = "recovered" if rec < len(dd) - 1 and dd[rec] > -0.005 else "ongoing/end"
print(f" {100*dep:>+6.1f}% {dates[s]} -> {dates[tr]} -> {dates[rec]} ({tr-s}d down, {rec-tr}d up, {recd})")
return dd
def main():
data = {nm: yhist(sym) for sym, nm in INSTR}
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
R = np.zeros((len(dates), len(INSTR)))
for j, (_, nm) in enumerate(INSTR):
s = np.array([data[nm][d] for d in dates]); R[1:, j] = s[1:] / s[:-1] - 1
book, w, L = book_series(R)
dd_profile(book, dates, "adaptive multi-strat book")
# 60/40 for context
je = [nm for _, nm in INSTR].index("equity"); jb = [nm for _, nm in INSTR].index("bond")
r6040 = np.zeros(len(dates)); r6040[1:] = 0.6 * R[1:, je] + 0.4 * R[1:, jb]
dd_profile(r6040, dates, "60/40 (context)")
print("\n READ: the book's drawdowns are shallow (~-5%), infrequent, short-recovery vs 60/40's -21% deep ones.")
print(" That shallow/short DD profile is the real value -- you stay invested, never panic-sell, compound steadily.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Backtest the EXACT deployable ETF book (the multistrat_paper live pipeline) on ~6y Yahoo history.
Real instruments (SPY/IEF/GLD/PDBC/DBMF + BTC-USD), same adaptive pipeline as the live harness
(vol-norm -> edge-decay trust -> trust-weighted combo -> adaptive risk layer, unlevered). Window is
limited by DBMF inception (~2019), so it spans COVID-2020, bear-2022, bulls-2021/2024 = multiple
regimes. Per-year Sharpe = the regime/robustness check; compared to plain 60/40 (SPY/IEF).
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import INSTR, book_series # exact live pipeline # noqa: E402
def yhist(sym, rng="10y"):
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range={rng}",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
ts = res["timestamp"]; ind = res["indicators"]
adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None}
def stats(r):
r = r[np.isfinite(r) & (r != 0)]
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann, vol, (ann / vol if vol > 0 else float("nan")), dd
def main():
data = {nm: yhist(sym) for sym, nm in INSTR}
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
R = np.zeros((len(dates), len(INSTR)))
for j, (_, nm) in enumerate(INSTR):
s = np.array([data[nm][d] for d in dates]); R[1:, j] = s[1:] / s[:-1] - 1
print(f"DEPLOYABLE ETF BOOK backtest — {len(dates)} days ({dates[0]}..{dates[-1]})")
book, w, L = book_series(R)
a, v, sh, dd = stats(book)
print(f" adaptive multi-strat (unlevered): Sharpe {sh:+.2f} ann {100*a:+.1f}% vol {100*v:.1f}% maxDD {100*dd:+.1f}%")
# 60/40 benchmark over same window
j_eq = [nm for _, nm in INSTR].index("equity"); j_bd = [nm for _, nm in INSTR].index("bond")
r6040 = np.zeros(len(dates)); r6040[1:] = 0.6 * R[1:, j_eq] + 0.4 * R[1:, j_bd]
a2, v2, sh2, dd2 = stats(r6040)
print(f" 60/40 (SPY/IEF) benchmark: Sharpe {sh2:+.2f} ann {100*a2:+.1f}% vol {100*v2:.1f}% maxDD {100*dd2:+.1f}%")
# equity buy-hold
aeq, veq, sheq, ddeq = stats(np.concatenate([[0], R[1:, j_eq]]))
print(f" equity buy-hold (SPY): Sharpe {sheq:+.2f} ann {100*aeq:+.1f}% maxDD {100*ddeq:+.1f}%")
yr = np.array([int(d[:4]) for d in dates])
print(" per-year Sharpe (adaptive book): " + " ".join(f"{y}:{stats(book[yr == y])[2]:+.1f}" for y in sorted(set(yr)) if (yr == y).sum() > 100))
print(f" current target weights: " + " ".join(f"{nm}:{100*w[j]*L:.0f}%" for j, (_, nm) in enumerate(INSTR)))
print("\n VERDICT: adaptive book Sharpe >= 60/40 AND lower maxDD across most years = the deployable book")
print(" delivers on real ETFs. If ~60/40, the diversification+adaptive layer earns its keep modestly.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""What can we DO with diversification: add more uncorrelated net-positive streams (the sqrt(N) lever).
Base 6 (SPY/IEF/GLD/PDBC/DBMF/BTC) + candidates: VNQ(REITs), TIP(infl-linked), HYG(HY credit),
EFA(intl eq), EEM(EM eq), PUTW(vol-risk-premium/buy-write). Same adaptive pipeline (edge-decay
trust auto-down-weights the bad ones). Does the expanded book beat the base-6? The trust layer
decides which candidates earn their place.
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import book_series # exact adaptive pipeline # noqa: E402
BASE = [("SPY", "equity"), ("IEF", "bond"), ("GLD", "gold"), ("PDBC", "commod"), ("DBMF", "trend"), ("BTC-USD", "crypto")]
CAND = [("VNQ", "reit"), ("TIP", "tips"), ("HYG", "hycredit"), ("EFA", "intl"), ("EEM", "em"), ("PUTW", "volprem")]
def yhist(sym):
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=10y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
ts = res["timestamp"]; ind = res["indicators"]
adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None}
def stats(r):
r = r[np.isfinite(r) & (r != 0)]
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann, vol, (ann / vol if vol > 0 else float("nan")), dd
def run(instr, data, label):
dates = sorted(set.intersection(*[set(data[nm]) for _, nm in instr]))
R = np.zeros((len(dates), len(instr)))
for j, (_, nm) in enumerate(instr):
s = np.array([data[nm][d] for d in dates]); R[1:, j] = s[1:] / s[:-1] - 1
book, w, L = book_series(R)
a, v, sh, dd = stats(book)
yr = np.array([int(d[:4]) for d in dates])
py = " ".join(f"{y}:{stats(book[yr==y])[2]:+.1f}" for y in sorted(set(yr)) if (yr == y).sum() > 100)
print(f" {label:>16} ({len(instr)} streams, {len(dates)}d): Sharpe {sh:+.2f} ann {100*a:+.1f}% maxDD {100*dd:+.1f}%")
print(f" per-year: {py}")
return w, [nm for _, nm in instr]
def main():
allinstr = BASE + CAND
data = {nm: yhist(sym) for sym, nm in allinstr}
print(f"EXPANDED BOOK (diversification lever) — base 6 vs +candidates")
run(BASE, data, "base-6")
w, names = run(allinstr, data, "expanded-12")
print(f"\n trust-layer weights in expanded book (which candidates earned their place):")
order = sorted(range(len(names)), key=lambda j: -w[j])
for j in order:
print(f" {names[j]:>9}: {100*w[j]:>5.1f}%")
print("\n VERDICT: expanded Sharpe > base-6 = more uncorrelated streams lift the book (diversification works).")
print(" If ~equal, the candidates are too correlated / not net-positive -> 6 streams already captures it.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Faster confidence WITHOUT waiting forward: (1) long-history backtest (~2006, incl 2008/2011/2015/
2018/2020/2022 = many more regimes) on long-lived ETFs, (2) block-bootstrap CI on the Sharpe.
Long-history ETFs (no DBMF/crypto, which start 2019): SPY/IEF/GLD/DBC + a DIY trend sleeve (12-1
momentum long/short on the 4, vol-targeted). Same adaptive pipeline. Per-year Sharpe incl 2008 +
bootstrap distribution = how robust is the design across regimes, and how stable is the estimate."""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import book_series # noqa: E402
ETFS = ["SPY", "IEF", "GLD", "DBC"]
def yhist(sym):
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=25y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
ts = res["timestamp"]; ind = res["indicators"]
adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None}
def main():
data = {s: yhist(s) for s in ETFS}
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
P = np.column_stack([[data[s][d] for d in dates] for s in ETFS])
T, n = P.shape
R = np.zeros((T, n)); R[1:] = P[1:] / P[:-1] - 1
# DIY trend sleeve: 12-1 momentum long/short on the 4 ETFs, equal risk
lc = np.log(P); sig = np.zeros((T, n)); sig[252:] = np.sign(lc[252:] - lc[:-252])
vol = np.full((T, n), np.nan)
for t in range(63, T):
vol[t] = R[t - 63:t].std(0)
iv = 1.0 / np.where(vol > 0, vol, np.nan)
w = np.nan_to_num(sig * iv); g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
trend = np.zeros(T); trend[1:] = np.sum(w[:-1] * R[1:], axis=1)
Rall = np.column_stack([R, trend]) # 5 streams
book, _, _ = book_series(Rall)
yr = np.array([int(d[:4]) for d in dates])
def st(r):
r = r[np.isfinite(r) & (r != 0)]; a = r.mean() * 252; v = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return (a / v if v > 0 else float("nan")), dd
sh, dd = st(book)
print(f"LONG-HISTORY BOOK (SPY/IEF/GLD/DBC + trend, no crypto) — {dates[0]}..{dates[-1]} ({T}d)")
print(f" full: Sharpe {sh:+.2f} maxDD {100*dd:+.1f}%")
print(f" per-year Sharpe (incl crises): " + " ".join(f"{y}:{st(book[yr==y])[0]:+.1f}" for y in sorted(set(yr)) if (yr == y).sum() > 100))
print(f" worst years maxDD: " + " ".join(f"{y}:{100*st(book[yr==y])[1]:+.0f}%" for y in [2008, 2011, 2015, 2018, 2020, 2022] if (yr == y).sum() > 100))
# block-bootstrap CI on Sharpe (block ~21d, 2000 resamples)
b = book[np.isfinite(book)]; nb = len(b); bl = 21
rng = np.random.default_rng(7)
shs = []
for _ in range(2000):
idx = rng.integers(0, nb - bl, size=nb // bl)
samp = np.concatenate([b[i:i + bl] for i in idx])
v = samp.std() * math.sqrt(252)
shs.append(samp.mean() * 252 / v if v > 0 else 0)
shs = np.array(shs)
print(f"\n bootstrap Sharpe CI (2000 block-resamples): p5 {np.percentile(shs,5):+.2f} p50 {np.percentile(shs,50):+.2f} p95 {np.percentile(shs,95):+.2f}")
print(f" P(Sharpe>0.5): {100*np.mean(shs>0.5):.0f}% P(Sharpe>1.0): {100*np.mean(shs>1.0):.0f}%")
print("\n VERDICT: positive across crisis years (2008/2020/2022) + bootstrap CI floor >0.5 = robust design,")
print(" fast confidence without waiting forward. (Caveat: no DBMF/crypto here -> weaker than full book.)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""multistrat — local paper-forward of the adaptive multi-strat book (deployable spec, Phase 1).
6 streams via free Yahoo daily adj-close: SPY/IEF/GLD/PDBC/DBMF + BTC-USD. Pipeline (deterministic
from history each run): vol-normalize each stream -> edge-decay trust theta (down-weight decaying /
resurrect recovered) -> trust-weighted combination -> adaptive risk layer (EMA vol, Kelly-floor,
continuous self-recovering drawdown de-lever, z-score correlation de-risk, leverage-floor),
UNLEVERED (MAXLEV 1.0). Forward-start, idempotent per UTC day, catch-up. No capital, no agents.
python3 multistrat_paper.py status cum return + annualized + current target weights & leverage
python3 multistrat_paper.py run book new trading days, persist (cron)
python3 multistrat_paper.py weights [USD] today's $ allocation per ETF
(alias paper == run)
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
STATE = os.path.join(_REPO, "data/surfer/multistrat_state.json")
INSTR = [("SPY", "equity"), ("IEF", "bond"), ("GLD", "gold"), ("PDBC", "commod"), ("DBMF", "trend"), ("BTC-USD", "crypto")]
TARGET_VOL, MAXLEV, KELLY_FLOOR, LEV_FLOOR = 0.10, 1.0, 0.5, 0.3
DD_DEADBAND, DD_SENS, DD_FLOOR = 0.05, 3.0, 0.40
def get(url, tries=4):
import time
for a in range(tries):
try:
return json.loads(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())
except Exception:
if a == tries - 1:
raise
time.sleep(2 * (a + 1))
def yhist(sym):
res = get(f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=2y")["chart"]["result"][0]
ts = res["timestamp"]; ind = res["indicators"]
adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None}
def build():
data = {nm: yhist(sym) for sym, nm in INSTR}
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
# drop today's INCOMPLETE intraday bar if present — this is a daily-CLOSE strategy; an in-session
# partial bar spikes the vol estimate and corrupts the leverage (e.g. halves it). Use completed closes.
today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
if dates and dates[-1] == today:
dates = dates[:-1]
R = np.zeros((len(dates), len(INSTR)))
for j, (_, nm) in enumerate(INSTR):
s = np.array([data[nm][d] for d in dates])
R[1:, j] = s[1:] / s[:-1] - 1
return dates, R
def volnorm(col, tv=TARGET_VOL, win=63):
out = np.zeros_like(col)
for t in range(win, len(col)):
rv = col[t - win:t].std() * math.sqrt(252)
out[t] = col[t] * min(5.0, tv / (rv + 1e-9))
return out
def trust(col, win=126, a=0.06):
th = 1.0; out = np.ones_like(col)
for t in range(win, len(col)):
seg = col[t - win:t]; sr = seg.mean() / (seg.std() + 1e-9) * math.sqrt(252)
th = (1 - a) * th + a * float(np.clip((sr + 0.5) / 1.0, 0.1, 1.0))
out[t] = th
return out
def book_series(R):
S = R.shape[1]
M = np.column_stack([volnorm(R[:, j]) for j in range(S)])
TH = np.column_stack([trust(M[:, j]) for j in range(S)])
tw = TH / np.maximum(TH.sum(1, keepdims=True), 1e-9)
combo = np.nansum(tw * M, axis=1)
T = len(combo); book = np.zeros(T); eq = 1.0; peak = 1.0
emu = float(np.nanmean(combo[:63])); evar = float(np.nanvar(combo[:63])) + 1e-12; chist = []
Lc = np.zeros(T)
for t in range(63, T - 1):
x = combo[t]; emu = 0.97 * emu + 0.03 * x; evar = 0.97 * evar + 0.03 * (x - emu) ** 2
L = min(MAXLEV, TARGET_VOL / (math.sqrt(max(evar, 1e-12) * 252) + 1e-9))
L = min(L, max(KELLY_FLOOR, (emu * 252) / (evar * 252 + 1e-9)))
dd = eq / peak - 1.0
L *= float(np.clip(1 - DD_SENS * max(0.0, -dd - DD_DEADBAND), DD_FLOOR, 1.0))
sub = np.nan_to_num(M[t - 63:t]); vmask = sub.std(0) > 1e-12 # only streams that vary (avoid /0 in corrcoef -> NaN)
if int(vmask.sum()) >= 2:
k = int(vmask.sum()); cm = np.corrcoef(sub[:, vmask].T); ac = float((np.nansum(cm) - k) / (k * k - k))
else:
ac = 0.0
chist.append(ac)
if len(chist) > 60:
h = np.array(chist[-120:]); z = (ac - h.mean()) / (h.std() + 1e-9)
L *= float(np.clip(1 - 0.2 * max(0.0, z), 0.5, 1.0))
L = float(np.clip(L, LEV_FLOOR, MAXLEV)); Lc[t] = L
book[t + 1] = L * combo[t + 1]; eq *= (1 + book[t + 1]); peak = max(peak, eq)
return book, tw[-1], Lc[-2] if T > 1 else 0.0
def load_state():
if os.path.exists(STATE):
return json.load(open(STATE))
return {"last_date": "", "days": 0, "sum_r": 0.0, "sumsq_r": 0.0, "equity": 1.0, "peak": 1.0, "max_dd": 0.0}
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
if cmd in ("run", "paper"):
st = load_state(); dates, R = build(); book, w, L = book_series(R)
os.makedirs(os.path.dirname(STATE), exist_ok=True)
if not st["last_date"]:
st["last_date"] = dates[-1]; json.dump(st, open(STATE, "w"))
print(f"initialized forward tracking from {dates[-1]}"); return
booked = 0
for i in range(1, len(dates)):
if dates[i] <= st["last_date"]:
continue
r = float(book[i])
st["days"] += 1; st["sum_r"] += r; st["sumsq_r"] += r * r
st["equity"] *= (1 + r); st["peak"] = max(st["peak"], st["equity"])
st["max_dd"] = min(st["max_dd"], st["equity"] / st["peak"] - 1); st["last_date"] = dates[i]; booked += 1
json.dump(st, open(STATE, "w"))
print(f"{datetime.date.today()} booked {booked} day(s) -> day {st['days']} (through {st['last_date']}) "
f"cum {100*(st['equity']-1):+.2f}% leverage {L:.2f}")
elif cmd == "weights":
cap = float(sys.argv[2]) if len(sys.argv) > 2 else 35000.0
dates, R = build(); _, w, L = book_series(R)
print(f"target allocation for ${cap:,.0f} (leverage {L:.2f}x, unlevered book):")
for j, (sym, nm) in enumerate(INSTR):
print(f" {nm:>7} ({sym:>8}): {100*w[j]*L:>5.1f}% ${cap*w[j]*L:>8,.0f}")
else:
st = load_state()
n = st["days"]; m = (st["sum_r"] / n) if n else 0; var = (st["sumsq_r"] / n - m * m) if n > 1 else 0
sh = (m * 252) / (math.sqrt(max(var, 1e-12)) * math.sqrt(252)) if n > 1 and var > 0 else float("nan")
print(f"adaptive multi-strat paper — day {n} (through {st['last_date'] or 'n/a'})")
print(f" cum {100*(st['equity']-1):+.2f}% ann-Sharpe {sh:+.2f} maxDD {100*st['max_dd']:+.1f}%")
dates, R = build(); _, w, L = book_series(R)
print(f" current leverage {L:.2f}x | target weights:")
for j, (sym, nm) in enumerate(INSTR):
print(f" {nm:>7} ({sym:>8}): {100*w[j]*L:>5.1f}%")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Does an ML vol-nowcaster (proxy for CfC/Mamba2) improve the risk layer's de-lever timing vs EMA?
Target: next-20d realized vol of the book's combination. Forecasters: EMA (baseline, what the book
uses) vs gradient-boosting (walk-forward, leak-free) on vol features. Compare (1) OOS vol-forecast
skill, (2) downstream: vol-targeting with ML-vol vs EMA-vol -> Sharpe / maxDD / realized-vol stability.
If GB ~ EMA downstream, the heavier CfC/Mamba2 won't help (more capacity = more overfit). Vol clusters,
so this is the fair shot.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_book import build_streams, volnorm # noqa: E402
from multistrat_book_v2 import edge_decay_trust # noqa: E402
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
TV = 0.10
def ema(x, span):
a = 2 / (span + 1); out = np.zeros_like(x); m = 0.0
for t in range(len(x)):
m = a * x[t] + (1 - a) * m; out[t] = m
return out
def main():
streams, days, year = build_streams()
names = list(streams)
M = np.column_stack([volnorm(streams[n]) for n in names])
TH = np.column_stack([edge_decay_trust(M[:, i]) for i in range(len(names))])
tw = TH / np.maximum(TH.sum(1, keepdims=True), 1e-9)
combo = np.nansum(tw * np.nan_to_num(M), axis=1)
T = len(combo)
# realized next-20d vol (target) and features (all causal)
H = 20
fwd = np.full(T, np.nan)
for t in range(T - H):
fwd[t] = combo[t + 1:t + 1 + H].std() * math.sqrt(252)
sq = combo ** 2
f_ema20 = np.sqrt(ema(sq, 20) * 252) # EMA vol (the baseline the book uses)
feats = np.column_stack([
np.sqrt(ema(sq, 10) * 252), np.sqrt(ema(sq, 20) * 252), np.sqrt(ema(sq, 60) * 252),
np.abs(combo), np.array([combo[max(0, t - 5):t].std() for t in range(T)]) * math.sqrt(252),
np.array([combo[max(0, t - 60):t].std() for t in range(T)]) * math.sqrt(252)])
# walk-forward GB vol forecast
gb_fc = np.full(T, np.nan); INIT, STEP = 252, 60
for s in range(INIT, T - H, STEP):
e = min(s + STEP, T - H)
tr = np.arange(63, s)
ok = np.isfinite(fwd[tr]) & np.isfinite(feats[tr]).all(1)
gb = HistGradientBoostingRegressor(max_depth=3, max_iter=150, learning_rate=0.05, min_samples_leaf=30).fit(feats[tr][ok], fwd[tr][ok])
gb_fc[s:e] = gb.predict(feats[s:e])
valid = np.arange(INIT, T - H)
v = valid[np.isfinite(fwd[valid]) & np.isfinite(gb_fc[valid])]
print(f"VOL-NOWCASTER TEST — {T} days, OOS {len(v)}")
print(f" OOS vol-forecast corr w/ realized: EMA {np.corrcoef(f_ema20[v], fwd[v])[0,1]:+.2f} GB {np.corrcoef(gb_fc[v], fwd[v])[0,1]:+.2f}")
print(f" OOS RMSE (lower=better): EMA {np.sqrt(np.mean((f_ema20[v]-fwd[v])**2)):.3f} GB {np.sqrt(np.mean((gb_fc[v]-fwd[v])**2)):.3f}")
# downstream: vol-target with each forecast (leverage = TV/forecast, capped 1x, floor 0.3)
def book(fc):
L = np.clip(TV / (fc + 1e-9), 0.3, 1.0)
r = np.zeros(T); r[1:] = L[:-1] * combo[1:]
rr = r[v[0]:v[-1]]; rr = rr[np.isfinite(rr)]
ann = rr.mean() * 252; vol = rr.std() * math.sqrt(252)
eq = np.cumprod(1 + rr); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann / vol if vol > 0 else float("nan"), dd, vol
se, dde, ve = book(f_ema20); sg, ddg, vg = book(gb_fc)
print(f" downstream book: EMA-vol Sharpe {se:+.2f} maxDD {100*dde:+.1f}% realizedVol {100*ve:.1f}%")
print(f" GB-vol Sharpe {sg:+.2f} maxDD {100*ddg:+.1f}% realizedVol {100*vg:.1f}%")
print("\n VERDICT: GB-vol downstream Sharpe/maxDD meaningfully > EMA-vol = an ML nowcaster helps the risk")
print(" layer (then CfC/Mamba2 worth it). If ~equal = EMA already captures vol-clustering; keep it simple.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""News-sentiment test with a real small LLM (FinBERT) on open news data (FNSPID).
Stream a chunk of FNSPID headlines -> FinBERT sentiment -> daily sentiment per ticker -> test if
it predicts returns. KEY: same-day corr (contemporaneous, not tradeable) vs next-day corr (the only
retail-tradeable horizon) + a net-of-cost OOS long/short trade. Honest prior (research): news real
but reaction instant + alpha dies at cost + decaying."""
import csv
import datetime
import io
import json
import math
import os
import sys
import urllib.request
import numpy as np
CHUNK = 70 * 1024 * 1024 # ~70MB stream (sorted by ticker -> early-alphabet large caps)
MAX_SCORE = 18000 # cap FinBERT inferences (GPU time)
def stream_news():
req = urllib.request.Request("https://huggingface.co/datasets/Zihan1004/FNSPID/resolve/main/Stock_news/nasdaq_exteral_data.csv",
headers={"User-Agent": "curl/8", "Range": f"bytes=0-{CHUNK}"})
buf = urllib.request.urlopen(req, timeout=120).read().decode("utf-8", "replace")
buf = buf[:buf.rfind("\n")] # drop partial last line
rows = []
for r in csv.reader(io.StringIO(buf)):
if len(r) < 4 or r[1] == "Date":
continue
try:
d = r[1][:10]; sym = r[3].strip(); title = r[2].strip()
datetime.date.fromisoformat(d)
if sym and title and len(title) > 8:
rows.append((d, sym, title))
except Exception:
continue
return rows
def yclose(sym):
try:
res = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=5y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
ts = res["timestamp"]; adj = res["indicators"].get("adjclose", [{}])[0].get("adjclose") or res["indicators"]["quote"][0]["close"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None}
except Exception:
return {}
def main():
print("streaming FNSPID news chunk...")
rows = stream_news()
syms = {}
for d, s, t in rows:
syms.setdefault(s, 0); syms[s] += 1
top = [s for s, n in sorted(syms.items(), key=lambda kv: -kv[1]) if n >= 50][:12]
rows = [r for r in rows if r[1] in top][:MAX_SCORE]
print(f" {len(rows)} headlines, {len(top)} tickers: {top}")
from transformers import pipeline
import torch
clf = pipeline("sentiment-analysis", model="ProsusAI/finbert", device=0 if torch.cuda.is_available() else -1, truncation=True, max_length=64)
titles = [r[2][:200] for r in rows]
print(" scoring with FinBERT...")
out = clf(titles, batch_size=64)
score = {"positive": 1.0, "negative": -1.0, "neutral": 0.0}
sent = {} # (sym, date) -> [scores]
for r, o in zip(rows, out):
sent.setdefault((r[1], r[0]), []).append(score[o["label"]] * o["score"])
daily = {k: float(np.mean(v)) for k, v in sent.items()}
prices = {s: yclose(s) for s in top}
# build aligned (sentiment[t], sameday ret[t], nextday ret[t+1]) cross-sectionally
S, R0, R1 = [], [], []
rec = []
for (sym, d), sc in daily.items():
px = prices.get(sym, {})
days = sorted(px)
if d not in px:
# map to next trading day
nd = [x for x in days if x >= d]
if not nd:
continue
d = nd[0]
i = days.index(d) if d in days else -1
if i < 1 or i + 1 >= len(days):
continue
r0 = px[days[i]] / px[days[i - 1]] - 1
r1 = px[days[i + 1]] / px[days[i]] - 1
rec.append((d, sym, sc, r0, r1))
rec.sort()
S = np.array([x[2] for x in rec]); R0 = np.array([x[3] for x in rec]); R1 = np.array([x[4] for x in rec])
n = len(S)
print(f"\n aligned sentiment-day observations: {n}")
print(f" corr(sentiment, SAME-day return): {np.corrcoef(S, R0)[0,1]:+.3f} (contemporaneous = news real but not tradeable)")
print(f" corr(sentiment, NEXT-day return): {np.corrcoef(S, R1)[0,1]:+.3f} (the only retail-tradeable horizon)")
# next-day long/short trade by sentiment sign, net 10bp, IS/OOS by time
order = np.argsort([x[0] for x in rec]); S, R1 = S[order], R1[order]
sig = np.sign(S); pnl = sig * R1 - 0.0010 * (np.abs(sig) > 0)
sp = int(0.6 * n)
def sh(x):
x = x[np.isfinite(x)]; return x.mean() / (x.std() + 1e-9) * math.sqrt(252) if len(x) > 20 and x.std() > 0 else float("nan")
print(f" next-day sentiment trade (net 10bp): IS Sharpe {sh(pnl[:sp]):+.2f} OOS Sharpe {sh(pnl[sp:]):+.2f}")
print("\n VERDICT: same-day corr >> next-day corr ~0 + OOS trade ~0/neg = news is real but its reaction is")
print(" INSTANT (contemporaneous, HFT-captured); no tradeable next-day drift for retail. Matches the research.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""PEAD gate: does post-earnings-announcement drift survive in small/neglected names, net of cost?
The strongest classic anomaly that persists BECAUSE it's too small/illiquid for funds to arb.
Proxy (no earnings-date data needed): an "earnings-like surprise" = a large abnormal-volume single
-day move (|ret| > K_SIG x trailing-vol AND volume > K_VOL x trailing-avg). PEAD predicts
CONTINUATION: enter at the event-day CLOSE (leak-free, after the full move), hold N days, signed by
the surprise direction. Measure net-of-cost drift, t-stat, long vs short, IS/OOS, by liquidity band.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll # noqa: E402
K_SIG, K_VOL = 2.0, 2.0 # surprise = |ret|>2sigma AND volume>2x average
LO, HI = 1e6, 30e6 # small-but-tradeable $/day band (where PEAD survives)
HORIZONS = [1, 3, 5, 10, 20]
def tstat(x):
x = x[np.isfinite(x)]
return float(x.mean() / (x.std() / math.sqrt(len(x)))) if len(x) > 30 and x.std() > 0 else float("nan")
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
vol20 = roll(np.std, R, 20) # trailing std (excludes t -> causal)
advol = roll(np.mean, np.nan_to_num(dvol), 20)
year = (1970 + days / 365.25).astype(int)
rt_cost = np.clip(40.0 / np.sqrt(np.maximum(advol, 1.0) / 1e6), 5.0, 80.0) / 1e4 # round-trip, illiquidity-scaled
valid = (np.isfinite(close) & (vol20 > 0) & np.isfinite(advol) & (advol > LO) & (advol < HI))
surprise = valid & (np.abs(R) > K_SIG * vol20) & (dvol > K_VOL * advol)
surprise[:21] = False; surprise[T - max(HORIZONS):] = False
sgn = np.sign(R)
print(f"PEAD gate (DBEQ, band ${LO/1e6:.0f}-{HI/1e6:.0f}M/day, surprise=|ret|>{K_SIG}sig & vol>{K_VOL}x)")
print(f" total surprise events: {int(surprise.sum())} (avg cost {1e4*rt_cost[surprise].mean():.0f}bp round-trip)")
oos = np.repeat((year >= 2025)[:, None], N, 1)
up = surprise & (sgn > 0); dn = surprise & (sgn < 0)
print(f"\n{'horizon':>8} {'n':>7} {'gross%':>7} {'net%':>7} {'t(net)':>7} {'fracpos':>8} | {'LONG net%':>9} {'SHORT net%':>10} | {'OOS net%':>9}")
for h in HORIZONS:
dr = np.full((T, N), np.nan); dr[:T - h] = lc[h:] - lc[:T - h] # dr[t] = lc[t+h]-lc[t]
signed = sgn * dr
net = signed - rt_cost
g = signed[surprise]; nt = net[surprise]
ln = net[up]; sh = net[dn]
oo = net[surprise & oos]
print(f"{h:>8} {int(np.isfinite(nt).sum()):>7} {100*np.nanmean(g):>+7.2f} {100*np.nanmean(nt):>+7.2f} "
f"{tstat(nt):>+7.1f} {np.nanmean(nt>0):>8.2f} | {100*np.nanmean(ln):>+9.2f} {100*np.nanmean(sh):>+10.2f} | {100*np.nanmean(oo):>+9.2f}")
# by surprise strength at the 10d horizon
h = 10
dr = np.full((T, N), np.nan); dr[:T - h] = lc[h:] - lc[:T - h]
net = sgn * dr - rt_cost
strong = surprise & (np.abs(R) > 3 * vol20)
print(f"\n 10d net by surprise strength: 2-3sig {100*np.nanmean(net[surprise & ~strong]):+.2f}% "
f">3sig {100*np.nanmean(net[strong]):+.2f}% (PEAD: bigger surprise -> bigger drift)")
print("\nVERDICT: net drift > 0 with t(net) > 3 (deflated) AND positive OOS AND long-side works = real PEAD edge")
print("worth a proper book. If net ~0 or t<2 or OOS collapses = eaten by cost / arbed even in small names.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Real PEAD test: actual earnings surprises (Nasdaq) x DBEQ prices, leak-free, net of cost.
For each earnings event (ticker, date, surprise%): direction = sign(surprise). Enter at the CLOSE
of the trading day AFTER the announcement (leak-free — announcement + initial reaction already
public), hold N days, signed by surprise. Measure net-of-cost drift, t-stat, long vs short, IS/OOS,
by surprise magnitude, in the small/neglected liquidity band where PEAD survives.
"""
import datetime
import glob
import json
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll # noqa: E402
LO, HI = 1e6, 50e6
HORIZONS = [1, 3, 5, 10, 20, 40]
EPOCH = datetime.date(1970, 1, 1)
def tstat(x):
x = np.asarray(x); x = x[np.isfinite(x)]
return float(x.mean() / (x.std() / math.sqrt(len(x)))) if len(x) > 30 and x.std() > 0 else float("nan")
def main():
insts, days, close, dvol = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]
advol = roll(np.mean, np.nan_to_num(dvol), 20)
rt_cost = np.clip(40.0 / np.sqrt(np.maximum(advol, 1.0) / 1e6), 5.0, 80.0) / 1e4
col = {int(i): k for k, i in enumerate(insts)}
sym = json.load(open("data/surfer/dbeq_symbology.json"))
mp = sym.get("result", sym)
tick2col = {}
for t, ranges in mp.items():
if ranges:
iid = int(ranges[0]["s"])
if iid in col:
tick2col[t] = col[iid]
# load earnings events
files = glob.glob("data/surfer/earnings/*.json")
ev_day, ev_col, ev_sgn, ev_mag = [], [], [], []
matched = 0
for f in files:
d = datetime.date.fromisoformat(os.path.basename(f)[:-5])
epoch = (d - EPOCH).days
idx = int(np.searchsorted(days, epoch, "left"))
t_enter = idx + 1 # close of day after announcement (leak-free)
if t_enter >= T - max(HORIZONS):
continue
for tk, surp in json.load(open(f)):
c = tick2col.get(tk)
if c is None or surp in (None, "", "N/A"):
continue
try:
s = float(str(surp).replace("%", "").replace(",", ""))
except ValueError:
continue
if not np.isfinite(close[t_enter, c]) or not (LO < advol[t_enter, c] < HI):
continue
ev_day.append(t_enter); ev_col.append(c); ev_sgn.append(np.sign(s)); ev_mag.append(abs(s)); matched += 1
ev_day = np.array(ev_day); ev_col = np.array(ev_col); ev_sgn = np.array(ev_sgn); ev_mag = np.array(ev_mag)
yr = (1970 + days[ev_day] / 365.25).astype(int)
print(f"PEAD (real surprises) — {len(files)} earnings days loaded, {matched} events matched to DBEQ "
f"(band ${LO/1e6:.0f}-{HI/1e6:.0f}M, avg cost {1e4*rt_cost[ev_day, ev_col].mean():.0f}bp)")
if matched < 200:
print(" (few events — let the fetch finish, then re-run)"); return
oosm = yr >= 2025
cost = rt_cost[ev_day, ev_col]
print(f"\n{'horizon':>8} {'gross%':>7} {'net%':>7} {'t(net)':>7} {'fracpos':>8} | {'LONG net%':>9} {'SHORT net%':>10} | {'OOS net%':>9}")
for h in HORIZONS:
fut = lc[ev_day + h, ev_col] - lc[ev_day, ev_col]
signed = ev_sgn * fut
net = signed - cost
up = ev_sgn > 0; dn = ev_sgn < 0
print(f"{h:>8} {100*np.nanmean(signed):>+7.2f} {100*np.nanmean(net):>+7.2f} {tstat(net):>+7.1f} "
f"{np.nanmean(net > 0):>8.2f} | {100*np.nanmean(net[up]):>+9.2f} {100*np.nanmean(net[dn]):>+10.2f} | {100*np.nanmean(net[oosm]):>+9.2f}")
# by surprise magnitude at 20d (PEAD: bigger surprise -> bigger drift)
h = 20
fut = lc[ev_day + h, ev_col] - lc[ev_day, ev_col]; net = ev_sgn * fut - cost
q = np.nanpercentile(ev_mag, [33, 66])
for lab, m in [("small surprise", ev_mag <= q[0]), ("mid", (ev_mag > q[0]) & (ev_mag <= q[1])), ("large surprise", ev_mag > q[1])]:
print(f" 20d net, {lab:>14}: {100*np.nanmean(net[m]):+.2f}% (n={int(m.sum())})")
print("\nVERDICT: net>0, t(net)>3, positive OOS, long-side works, AND bigger-surprise->bigger-drift = real PEAD.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Phase 1 — hunt & validate NEW crypto signals; build the linear-blend baseline RL must beat.
Each candidate is theoretically distinct from momentum (different driver), run through the same
gauntlet that killed carry/low-vol: PIT universe (top-50 by dollar-vol, dead coins in), death-excl,
~10bp cost, full/IS/OOS Sharpe, CPCV-median, coin-bootstrap (frac>0), and CORRELATION to momentum.
Survivors (CPCVmed>0 & OOS>0 & bootstrap frac>0>=0.8) -> linear blend = the baseline to beat.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
TOPK = 50
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, qv, fund = pit_sweep.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = roll(np.mean, qv, 30)
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff = np.where(tradeable, R - fund, 0.0)
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
univ[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
year = (1970 + days / 365.25).astype(int)
# rolling BTC-beta -> residual returns (for residual momentum + IVOL)
bj = syms.index("BTCUSDT")
rb = R[:, bj]
beta = np.zeros((T, N)); W = 60
for t in range(W, T):
rw = rb[t - W:t]; vb = rw.var() + 1e-12
cov = (R[t - W:t] * rw[:, None]).mean(0) - R[t - W:t].mean(0) * rw.mean()
beta[t] = cov / vb
resid = R - beta * rb[:, None]
resid_cum = np.cumsum(np.where(np.isfinite(resid), resid, 0.0), axis=0)
def trail_arr(A, L):
out = np.full_like(A, np.nan); out[L:] = A[L:] - A[:-L]; return out
sigs = {
"mom_30 (ref)": trailing(lc, 30),
"resid_mom_30": trail_arr(resid_cum, 30),
"MAX_lottery": -roll(np.max, R, 20), # short extreme-spike (lottery) coins
"ivol_low": -roll(np.std, resid, 30), # low idiosyncratic vol
"amihud_illiq": roll(np.mean, np.abs(R) / np.maximum(qv, 1.0), 30), # illiquidity premium
"accel": trailing(lc, 10) - trailing(lc, 30), # momentum acceleration
}
def book(sig):
s = sig.copy(); s[~univ] = np.nan
return pnl_w(xs_weights(s), Reff, cost_bp=10)
mom_pnl = book(sigs["mom_30 (ref)"])
rng = np.random.default_rng(3)
print(f"\n===== PHASE 1 — candidate crypto signals (PIT top{TOPK}, death-excl, 10bp) =====")
print(f"{'signal':>16} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'boot>0':>7} {'corr_mom':>8}")
pnls = {}
for nm, sg in sigs.items():
p = book(sg); pnls[nm] = p
v = validate(p, days, 40)
# coin-bootstrap
sh = []
for _ in range(80):
cols = rng.choice(N, size=max(N // 2, 10), replace=False)
sub = np.full((T, N), np.nan); sub[:, cols] = sg[:, cols]
s = sharpe_t(torch.tensor(book(sub), device=DEV, dtype=torch.float64))
if not math.isnan(s):
sh.append(s)
bootp = float(np.mean(np.array(sh) > 0)) if sh else float("nan")
a, b = mom_pnl, p; m = np.isfinite(a) & np.isfinite(b)
corr = float(np.corrcoef(a[m], b[m])[0, 1])
print(f"{nm:>16} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {bootp:>7.2f} {corr:>+8.2f}")
v["_boot"] = bootp
sigs[nm] = (sg, v)
# survivors -> linear blend baseline (sum of cross-sectional z-scores)
def zc(x):
mu = np.nanmean(x, axis=1, keepdims=True); sd = np.nanstd(x, axis=1, keepdims=True)
return np.nan_to_num((x - mu) / np.where(sd > 0, sd, 1))
surv = [nm for nm, (sg, v) in sigs.items() if v["med"] > 0 and v["oos"] > 0 and v.get("_boot", 0) >= 0.8]
print(f"\nSURVIVORS (CPCVmed>0 & OOS>0 & boot>0>=0.8): {surv}")
if len(surv) >= 2:
blend = sum(zc(sigs[nm][0]) for nm in surv)
bp = book(blend)
vb = validate(bp, days, 40)
T_ = lambda x: torch.tensor(x, device=DEV, dtype=torch.float64)
py = " ".join(f"{y}:{sharpe_t(T_(bp[year[1:]==y])):+.2f}" for y in range(2020, 2027) if (year[1:] == y).sum() > 30)
print(f"LINEAR-BLEND BASELINE ({len(surv)} signals): full {vb['full']:+.2f} OOS {vb['oos']:+.2f} CPCVmed {vb['med']:+.2f}")
print(" per-year: " + py)
print(" -> this is the bar an RL combiner must BEAT out-of-sample (deflated) to justify itself.")
else:
print("Too few survivors for a blend — RL has no signal set to combine; momentum stays solo.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Execution-realism stress for point-in-time crypto momentum — is the magnitude believable?
Layers on the realistic frictions that could inflate the paper Sharpe:
- cost 10→20→30 bp on turnover (smaller coins cost more)
- WINSORIZE per-coin daily returns to ±cap (you cannot fill a short into an 80%/day collapse)
- EXCLUDE each coin's final K days (delisting death-spiral: untradeable → zero PnL there)
- liquidity FLOOR: only trade coins with trailing dollar-vol > floor
If mom_20/30 stays positive EVERY year under the combined stress, the edge magnitude is real.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep as ps # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, qv, fund = ps.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = np.zeros((T, N))
for t in range(30, T):
dv30[t] = qv[t - 30:t].mean(axis=0)
year = (1970 + days / 365.25).astype(int)
# death-spiral mask: each coin's final 5 valid days = untradeable
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
def variant(cost_bp, winsor, excl_death, dv_floor, L=20, TOPK=50):
Reff = (R - fund)
if winsor is not None:
Reff = np.clip(Reff, -winsor, winsor)
if excl_death:
Reff = np.where(tradeable, Reff, 0.0)
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > dv_floor) & np.isfinite(close[t]))[0]
if len(elig) == 0:
continue
k = min(TOPK, len(elig))
univ[t, elig[np.argsort(-dv30[t, elig])[:k]]] = True
sig = trailing(lc, L).copy(); sig[~univ] = np.nan
pnl = pnl_w(xs_weights(sig), Reff, cost_bp=cost_bp)
v = validate(pnl, days, 6)
py = [sharpe_t(torch.tensor(pnl[year[1:] == y], device=DEV, dtype=torch.float64))
if (year[1:] == y).sum() > 30 else float("nan") for y in range(2020, 2027)]
return v, py
print(f"\n===== EXECUTION-REALISM STRESS — PIT momentum (mom_20, TOPK=50) — {N} coins =====")
print(f"{'variant':>34} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} | per-year 2020..2026")
cfgs = [
("baseline 10bp", dict(cost_bp=10, winsor=None, excl_death=False, dv_floor=0)),
("20bp cost", dict(cost_bp=20, winsor=None, excl_death=False, dv_floor=0)),
("winsor ±20%", dict(cost_bp=10, winsor=0.20, excl_death=False, dv_floor=0)),
("excl death-spiral (last 5d)", dict(cost_bp=10, winsor=None, excl_death=True, dv_floor=0)),
("dv-floor $5M/day", dict(cost_bp=10, winsor=None, excl_death=False, dv_floor=5e6)),
("COMBINED 20bp+wins20+death+$5M", dict(cost_bp=20, winsor=0.20, excl_death=True, dv_floor=5e6)),
("HARSH 30bp+wins10+death+$10M", dict(cost_bp=30, winsor=0.10, excl_death=True, dv_floor=1e7)),
]
for nm, cfg in cfgs:
v, py = variant(**cfg)
pys = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py)
print(f"{nm:>34} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} | {pys}")
print("\nVERDICT: positive every year under COMBINED/HARSH => believable, sizable edge (not paper).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Point-in-time survivorship test for crypto cross-sectional momentum.
Universe is rebuilt EACH DAY: among coins with active trailing-30d dollar-volume (>0),
take the top-K by that volume. Dead coins (LUNA, SRM, ...) are IN the cross-section while
they trade and DROP OUT (after carrying their crash) when volume dies. No lookahead:
volume & momentum use only past data; weights apply to next-day return. If momentum's
edge survives here — especially 2022 with LUNA included — it is real beyond the bootstrap proxy.
"""
import glob
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def load():
syms, data = [], {}
_base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # cwd-independent
for p in sorted(glob.glob(os.path.join(_base, "data/surfer/crypto_pit/*.npz"))):
d = np.load(p); s = p.split("/")[-1][:-4]
data[s] = (d["day"].astype(np.int64), d["close"].astype(float), d["qvol"].astype(float), d["funding"].astype(float))
syms.append(s)
syms = sorted(syms)
days = np.array(sorted(set().union(*[set(data[s][0].tolist()) for s in syms])))
di = {int(v): i for i, v in enumerate(days.tolist())}
T, N = len(days), len(syms)
close = np.full((T, N), np.nan); qv = np.full((T, N), np.nan); fund = np.full((T, N), np.nan)
for j, s in enumerate(syms):
dd, cc, vv, ff = data[s]
for k in range(len(dd)):
r = di[int(dd[k])]; close[r, j] = cc[k]; qv[r, j] = vv[k]; fund[r, j] = ff[k]
return syms, days, close, qv, fund
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, qv, fund = load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
Reff = R - np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = np.full((T, N), 0.0) # trailing 30d mean dollar-volume
for t in range(30, T):
dv30[t] = qv[t - 30:t].mean(axis=0)
dead = sum(1 for j in range(N) if qv[-30:, j].mean() == 0)
year = (1970 + days / 365.25).astype(int)
print(f"\n===== POINT-IN-TIME MOMENTUM — {N} coins ({dead} dead/delisted), {T}d =====")
for TOPK in [30, 50]:
# daily universe = top-K by trailing dollar-vol among actively-trading coins
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig) == 0:
continue
k = min(TOPK, len(elig))
top = elig[np.argsort(-dv30[t, elig])[:k]]
univ[t, top] = True
print(f"\n--- TOPK={TOPK} (avg coins/day in-universe = {univ.sum(1).mean():.0f}) ---")
print(f"{'lookback':>9} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5} | per-year (2020..2026)")
for L in [10, 20, 30]:
sig = trailing(lc, L).copy()
sig[~univ] = np.nan # restrict signal to PIT universe
w = xs_weights(sig)
pnl = pnl_w(w, Reff, cost_bp=10)
v = validate(pnl, days, 6)
py = []
for y in range(2020, 2027):
m = year[1:] == y
py.append(sharpe_t(torch.tensor(pnl[m], device=DEV, dtype=torch.float64)) if m.sum() > 30 else float("nan"))
pys = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py)
print(f"{L:>9} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f} | {pys}")
print("\nVERDICT: if mom_20-30 stays positive (esp. 2022 with LUNA in-universe) => survivorship-robust REAL edge.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""The strategy class we never tested: HARVEST structural premia with the risk system, not predict.
Diversified multi-asset futures (equity/rates/metals/energy/ags/FX). Build a ladder:
equal-weight -> inverse-vol (risk-parity) -> + vol-target -> + trend-overlay (long-flat by 252d trend,
the crisis-alpha de-risk). Measure RISK-ADJUSTED (Sharpe, ann-vol, max-DD, Calmar, per-year) vs
benchmarks (equal-weight, equity-only, 60/40). No prediction/alpha needed — just disciplined
harvesting + risk control (what the engine is actually good at). Roll-zeroed returns (understates
carry; fine for comparing the risk-management strategies).
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import load_panel, build_returns # noqa: E402
TARGET_VOL = 0.10 # 10% annualized portfolio vol
def metrics(r):
r = r[np.isfinite(r)]
if len(r) < 50 or r.std() == 0:
return dict(sr=float("nan"), ann=float("nan"), vol=float("nan"), dd=float("nan"), calmar=float("nan"))
ann = r.mean() * 252; vol = r.std() * math.sqrt(252); sr = ann / vol
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return dict(sr=sr, ann=ann, vol=vol, dd=dd, calmar=ann / (abs(dd) + 1e-9))
def main():
roots, days, close, op, inst, weekday = load_panel()
lc, R, ov, intr = build_returns(close, op, inst)
T, N = close.shape
year = (1970 + days / 365.25).astype(int)
vol63 = np.full_like(lc, np.nan)
for t in range(63, T):
vol63[t] = np.nanstd(R[t - 63:t], axis=0)
iv = 1.0 / np.where(vol63 > 0, vol63, np.nan)
avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0)
def port(weights):
w = np.nan_to_num(weights)
w = np.where(avail, w, 0.0)
g = np.sum(np.abs(w), axis=1, keepdims=True); g[g == 0] = 1
w = w / g # unit gross (fully invested)
return np.sum(w[:-1] * R[1:], axis=1)
def voltarget(r):
out = np.zeros_like(r)
for t in range(63, len(r)):
rv = np.std(r[t - 63:t]) * math.sqrt(252)
out[t] = r[t] * min(3.0, TARGET_VOL / (rv + 1e-9)) # scale to target vol, cap 3x leverage
return out
# weight schemes
eqw = np.where(avail, 1.0, 0.0)
rp = np.where(avail, iv, 0.0) # inverse-vol risk parity
trend = (np.full_like(lc, np.nan)); trend[252:] = lc[252:] - lc[:-252]
rp_trend = np.where(avail & (trend > 0), iv, 0.0) # trend-overlay: hold only uptrending assets
books = {
"equal-weight": port(eqw),
"risk-parity (inv-vol)": port(rp),
"risk-parity + vol-target": voltarget(port(rp)),
"RP + trend + vol-target": voltarget(port(rp_trend)),
}
# benchmarks
es = roots.index("ES") if "ES" in roots else 0
bench = {"equity-only (ES)": R[1:, es]}
if "ZN" in roots:
zn = roots.index("ZN")
w6040 = np.zeros((T, N)); w6040[:, es] = 0.6; w6040[:, zn] = 0.4
bench["60/40 (ES/ZN)"] = port(w6040)
print(f"\n===== PREMIUM HARVEST (diversified futures, {N} assets, {T} days, target {int(TARGET_VOL*100)}% vol) =====")
print(f"{'strategy':>26} {'Sharpe':>7} {'ann%':>6} {'vol%':>6} {'maxDD%':>7} {'Calmar':>7}")
for nm, r in {**bench, **books}.items():
m = metrics(r)
print(f"{nm:>26} {m['sr']:>+7.2f} {100*m['ann']:>+6.1f} {100*m['vol']:>5.1f} {100*m['dd']:>+7.1f} {m['calmar']:>7.2f}")
print("\nper-year Sharpe (RP + trend + vol-target):")
rtv = books["RP + trend + vol-target"]
print(" " + " ".join(f"{y}:{metrics(rtv[year[1:]==y])['sr']:+.1f}" for y in range(2010, 2027) if (year[1:] == y).sum() > 100))
print("\nVERDICT: if risk-managed harvest beats benchmarks on Sharpe/Calmar (esp lower maxDD) = the engine's")
print("real job (risk management of real premia) delivers, no alpha needed. Modest but robust = the honest win.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Clean premium-harvest: risk-parity across ~5 broad ASSET CLASSES (not 39 instruments).
Equity (ES), bonds (ZN), gold (GC), energy (CL), crypto (BTC) — genuinely uncorrelated premia.
Risk-parity (inv-vol) + vol-target + optional trend-overlay, vs 60/40 and buy-and-hold equity.
Runs 4-asset (2010-2026, longer) and 5-asset (+BTC, 2019-2026). The fair test of whether the
risk system adds value over plain 60/40 when applied to the RIGHT universe.
Futures = roll-zeroed price returns (understates commodity carry); BTC = clean close-close.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import load_panel, build_returns # noqa: E402
import pit_sweep # noqa: E402
TVOL = 0.10
def metrics(r):
r = np.asarray(r); r = r[np.isfinite(r)]
if len(r) < 50 or r.std() == 0:
return dict(sr=float("nan"), ann=float("nan"), dd=float("nan"), cal=float("nan"))
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return dict(sr=ann / vol, ann=ann, dd=dd, cal=ann / (abs(dd) + 1e-9))
def build():
roots, fdays, close, op, inst = load_panel()[:5]
lc, R, ov, intr = build_returns(close, op, inst)
fcol = {r: roots.index(r) for r in ["ES", "ZN", "GC", "CL"] if r in roots}
fut = {r: {int(fdays[t]): R[t, c] for t in range(len(fdays))} for r, c in fcol.items()}
syms, cdays, cc, cqv, cf = pit_sweep.load()
j = syms.index("BTCUSDT"); lcb = np.log(cc[:, j])
btc = {int(cdays[t]): (lcb[t] - lcb[t - 1]) for t in range(1, len(cdays)) if np.isfinite(lcb[t]) and np.isfinite(lcb[t - 1])}
return fut, btc
def run(assets, rets, label):
days = np.array(sorted(set.intersection(*[set(rets[a]) for a in assets])))
M = np.array([[rets[a][int(d)] for a in assets] for d in days]) # [T, K]
M = np.where(np.isfinite(M), M, 0.0)
T, K = M.shape
year = (1970 + days / 365.25).astype(int)
vol = np.full((T, K), np.nan)
for t in range(63, T):
vol[t] = M[t - 63:t].std(0)
iv = 1.0 / np.where(vol > 0, vol, np.nan)
trend = np.full((T, K), np.nan)
L = 126
for t in range(L, T):
trend[t] = M[t - L:t].sum(0)
def book(w):
w = np.nan_to_num(w); g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1
w = w / g
return np.sum(w[:-1] * M[1:], axis=1)
def vt(r):
out = np.zeros_like(r)
for t in range(63, len(r)):
rv = np.std(r[t - 63:t]) * math.sqrt(252)
out[t] = r[t] * min(3.0, TVOL / (rv + 1e-9))
return out
rp = book(iv)
rp_t = book(np.where(trend > 0, iv, 0.0))
es_i = assets.index("ES") if "ES" in assets else 0
bench_eq = M[1:, es_i]
w60 = np.zeros((T, K)); w60[:, es_i] = 0.6
if "ZN" in assets:
w60[:, assets.index("ZN")] = 0.4
print(f"\n===== {label}: {assets} ({T} days) =====")
print(f"{'strategy':>24} {'Sharpe':>7} {'ann%':>6} {'maxDD%':>7} {'Calmar':>7}")
for nm, r in [("equity-only (ES)", bench_eq), ("60/40", book(w60)),
("risk-parity", rp), ("RP + vol-target", vt(rp)),
("RP + trend + vol-target", vt(rp_t))]:
m = metrics(r)
print(f"{nm:>24} {m['sr']:>+7.2f} {100*m['ann']:>+6.1f} {100*m['dd']:>+7.1f} {m['cal']:>7.2f}")
best = vt(rp)
print(" per-year Sharpe (RP+vol-target): " + " ".join(
f"{y}:{metrics(best[year[1:]==y])['sr']:+.1f}" for y in sorted(set(year)) if (year[1:] == y).sum() > 100))
def main():
fut, btc = build()
run(["ES", "ZN", "GC", "CL"], {**fut}, "4-asset (2010-2026)")
run(["ES", "ZN", "GC", "CL", "BTC"], {**fut, "BTC": btc}, "5-asset +crypto (2019-2026)")
print("\nVERDICT: if RP/vol-target beats 60/40 on Sharpe AND maxDD = risk system adds real value on the")
print("right universe (the engine's true job). If 60/40 still wins, the deployable answer is just simple harvest.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""The one evidence-backed non-crypto lever: add a diversified TREND sleeve to 60/40.
Research verdict: trend/managed-futures is the only premium genuinely uncorrelated-to-equity AND
convex in crises (made money in 2022 when stocks+bonds both fell). We tested trend standalone
(marginal) and as a filter (hurt) — never as a DIVERSIFYING SLEEVE on 60/40, the specific thing
the evidence supports. Build a long/SHORT diversified time-series-momentum book (vol-targeted),
blend with 60/40 at various weights, measure Sharpe + maxDD + crisis-year behavior. Honest
question: does 60/40 + trend beat plain 60/40 on risk-adjusted terms (~0.7 -> ~0.8-0.9)?
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import load_panel, build_returns # noqa: E402
TVOL = 0.10
def metrics(r):
r = np.asarray(r); r = r[np.isfinite(r)]
if len(r) < 50 or r.std() == 0:
return dict(sr=float("nan"), ann=float("nan"), dd=float("nan"), cal=float("nan"))
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return dict(sr=ann / vol, ann=ann, dd=dd, cal=ann / (abs(dd) + 1e-9))
def voltarget(r, win=63):
out = np.zeros_like(r)
for t in range(win, len(r)):
rv = np.std(r[t - win:t]) * math.sqrt(252)
out[t] = r[t] * min(3.0, TVOL / (rv + 1e-9))
return out
def main():
roots, days, close, op, inst = load_panel()[:5]
lc, R, ov, intr = build_returns(close, op, inst)
T, N = close.shape
year = (1970 + days / 365.25).astype(int)
vol63 = np.full_like(lc, np.nan)
for t in range(63, T):
vol63[t] = np.nanstd(R[t - 63:t], axis=0)
iv = 1.0 / np.where(vol63 > 0, vol63, np.nan)
avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0)
es, zn = roots.index("ES"), roots.index("ZN")
r6040 = 0.6 * R[1:, es] + 0.4 * R[1:, zn] # plain 60/40 (daily returns)
# diversified long/SHORT time-series-momentum (CTA) sleeve, vol-targeted
tsig = np.full_like(lc, np.nan)
tsig[252:] = np.sign(lc[252:] - lc[:-252]) # long if 12mo up, short if down
w = np.where(avail & np.isfinite(tsig), tsig * iv, 0.0)
g = np.sum(np.abs(w), axis=1, keepdims=True); g[g == 0] = 1
w = w / g
rtrend_raw = np.sum(w[:-1] * R[1:], axis=1)
rtrend = voltarget(rtrend_raw) # the managed-futures sleeve
# normalize both legs to ~TVOL so blend weights are meaningful, then blend
def norm(r):
return voltarget(r)
base = norm(r6040)
sleeve = rtrend
yr = year[1:]
print(f"\n===== 60/40 + TREND SLEEVE ({N}-asset long/short CTA, vol-targeted) =====")
print(f"{'blend':>22} {'Sharpe':>7} {'ann%':>6} {'maxDD%':>7} {'Calmar':>7}")
blends = [("100% 60/40", 1.0, 0.0), ("80/20 +trend", 0.8, 0.2), ("70/30 +trend", 0.7, 0.3),
("60/40 +trend", 0.6, 0.4), ("50/50 +trend", 0.5, 0.5), ("100% trend", 0.0, 1.0)]
rows = {}
for nm, a, b in blends:
r = a * base + b * sleeve
rows[nm] = r; m = metrics(r)
print(f"{nm:>22} {m['sr']:>+7.2f} {100*m['ann']:>+6.1f} {100*m['dd']:>+7.1f} {m['cal']:>7.2f}")
# correlation of the two legs (the diversification engine)
mask = np.isfinite(base) & np.isfinite(sleeve) & (base != 0) & (sleeve != 0)
corr = np.corrcoef(base[mask], sleeve[mask])[0, 1]
print(f"\n corr(60/40, trend) = {corr:+.2f} (negative/low = the diversification that lifts Sharpe + cuts DD)")
print(" CRISIS-YEAR check — Sharpe by year (does trend save the equity/bond drawdowns?):")
for nm in ["100% 60/40", "70/30 +trend"]:
r = rows[nm]
py = " ".join(f"{y}:{metrics(r[yr==y])['sr']:+.1f}" for y in [2015, 2018, 2020, 2022, 2025] if (yr == y).sum() > 100)
print(f" {nm:>14}: {py}")
print("\nVERDICT: if 70/30 or 60/40+trend Sharpe > plain 60/40 AND maxDD smaller (esp 2022) = the evidence-backed")
print("lever works; ~0.7 -> ~0.8-0.9 with crisis protection. If not, 60/40 is genuinely the retail ceiling.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""The decisive gate both the critic and the exhaustion-researcher named: does the
exhaustion-short carry MARGINAL ALPHA over plain residual momentum, or is it collinear?
Build, on the PIT crypto panel: (1) residual-momentum L/S book [validated], (2) exhaustion
L/S book (z-sum of: neg residual-mom, fading dollar-volume rank, extreme funding, drawdown).
Then: correlation, and regress exhaustion daily returns on momentum daily returns -> the
intercept (alpha) is what matters. High corr + ~0 alpha => momentum re-spelled => drop it.
Also A/B the squeeze veto (no short when funding<=0). Realistic: Reff = return - funding
(shorts pay positive funding), death-excl, 10bp.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
TOPK = 50
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def zc(x):
mu = np.nanmean(x, axis=1, keepdims=True); sd = np.nanstd(x, axis=1, keepdims=True)
return np.nan_to_num((x - mu) / np.where(sd > 0, sd, 1))
def main():
syms, days, close, qv, fund = pit_sweep.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = roll(np.mean, qv, 30)
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff = np.where(tradeable, R - fund, 0.0)
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
univ[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
year = (1970 + days / 365.25).astype(int)
# residual (beta-stripped) momentum
mkt = np.array([R[t][univ[t]].mean() if univ[t].any() else 0.0 for t in range(T)])
beta = np.zeros((T, N)); Wb = 60
for t in range(Wb, T):
mw = mkt[t - Wb:t]; vb = mw.var() + 1e-12
beta[t] = ((R[t - Wb:t] * mw[:, None]).mean(0) - R[t - Wb:t].mean(0) * mw.mean()) / vb
rcum = np.cumsum(R - beta * mkt[:, None], axis=0)
resid_mom = np.full((T, N), np.nan); resid_mom[20:] = rcum[20:] - rcum[:-20]
# exhaustion terms (all oriented so HIGH => short candidate)
dvr = np.full((T, N), np.nan) # cross-sectional dollar-vol rank (0..1)
for t in range(T):
e = np.where(univ[t])[0]
if len(e) > 1:
r = dv30[t, e].argsort().argsort().astype(float) / (len(e) - 1)
dvr[t, e] = r
dvr_slope = np.full((T, N), np.nan); dvr_slope[30:] = dvr[30:] - dvr[:-30] # falling rank = fading
fund7 = roll(np.mean, fund, 7) # crowded-long carry
rmax = roll(np.max, lc, 90); dd = lc - rmax # drawdown-from-ATH (<=0)
exh = zc(-resid_mom) + zc(-dvr_slope) + zc(fund7) + zc(-dd) # additive, equal-weight (no fitting)
def book(sig, veto_short=None):
s = sig.copy(); s[~univ] = np.nan
w = xs_weights(s)
if veto_short is not None:
w = np.where((w < 0) & veto_short, 0.0, w) # drop shorts where veto true
return pnl_w(w, Reff, cost_bp=10)
pnl_mom = book(resid_mom)
pnl_exh = book(-exh) # long low-exhaustion / short high
squeeze_veto = fund <= 0 # don't short crowded-short (squeeze fuel)
pnl_exh_veto = book(-exh, veto_short=squeeze_veto)
def stats(p):
v = validate(p, days, 50); return v
T_ = lambda x: torch.tensor(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
print(f"\n===== EXHAUSTION-SHORT MARGINAL-ALPHA GATE (PIT top{TOPK}, death-excl, 10bp, deflate N=50) =====")
for nm, p in [("residual_momentum", pnl_mom), ("exhaustion", pnl_exh), ("exhaustion+veto", pnl_exh_veto)]:
v = stats(p)
print(f" {nm:>18}: full {v['full']:+.2f} IS {v['is_']:+.2f} OOS {v['oos']:+.2f} CPCVmed {v['med']:+.2f} DSR {v['dsr']:.2f}")
# THE decisive test: regress exhaustion returns on momentum returns -> marginal alpha
a, b = pnl_mom, pnl_exh
m = np.isfinite(a) & np.isfinite(b)
x, y = a[m], b[m]
corr = float(np.corrcoef(x, y)[0, 1])
beta1 = float(np.cov(x, y)[0, 1] / (np.var(x) + 1e-12))
resid = y - beta1 * x
alpha_daily = float(resid.mean())
alpha_ann_sr = alpha_daily / (resid.std() + 1e-12) * math.sqrt(365)
alpha_t = alpha_daily / (resid.std() / math.sqrt(len(resid)) + 1e-12)
print(f"\n REGRESS exhaustion ~ momentum: corr {corr:+.2f} beta {beta1:+.2f}")
print(f" marginal alpha: ann-Sharpe {alpha_ann_sr:+.2f} t-stat {alpha_t:+.2f} (>2 = real distinct edge)")
# does combining beat momentum alone (OOS)?
sm, se = np.nanstd(a), np.nanstd(b)
comb = (np.nan_to_num(a) / sm + np.nan_to_num(b) / se)
vc = stats(comb); vm = stats(pnl_mom)
print(f"\n combined(mom+exh) OOS {vc['oos']:+.2f} vs momentum-alone OOS {vm['oos']:+.2f} "
f"=> {'ADDS' if vc['oos'] > vm['oos'] + 0.1 else 'no improvement'}")
print("\nVERDICT: high corr + alpha t<2 + no OOS improvement => exhaustion is momentum re-spelled -> DROP (ship Comp-1).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Proper research: (a) robust momentum construction + (b) diversifying second edge.
All point-in-time (universe = daily top-50 by trailing dollar-volume, dead coins included) and
under REALISM stress (Reff winsorized ±20%/day, death-spiral last-5d excluded, 20bp cost) — we
optimize for robust/realistic performance, not headline.
(a) 6 pre-registered momentum constructions judged by STRESSED Sharpe + degradation ratio + every-year.
(b) canonical diversifiers (carry, TS-trend, long/short reversal, low-vol, size) judged by correlation
to the momentum sleeve + COMBINED inverse-vol book Sharpe (a weak-but-uncorrelated sleeve can help).
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep as ps # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
WINSOR, COST, TOPK = 0.20, 20.0, 50
def tr(lc, L, skip=0):
out = np.full_like(lc, np.nan)
if skip == 0:
out[L:] = lc[L:] - lc[:-L]
else: # return from t-L-skip to t-skip (classic 12-1 skip)
out[L + skip:] = lc[L:-skip] - lc[:-(L + skip)]
return out
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def row_rank(x): # per-row percentile rank in [-0.5,0.5], nan-aware
out = np.full_like(x, np.nan)
for t in range(x.shape[0]):
m = np.isfinite(x[t])
if m.sum() > 1:
r = x[t, m].argsort().argsort().astype(float)
out[t, m] = r / (m.sum() - 1) - 0.5
return out
def main():
syms, days, close, qv, fund = ps.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = roll(np.mean, qv, 30)
year = (1970 + days / 365.25).astype(int)
yrs = list(range(2020, 2027))
# PIT universe + tradeable (death-spiral) mask
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
univ[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff_base = R - fund
Reff_str = np.where(tradeable, np.clip(Reff_base, -WINSOR, WINSOR), 0.0)
vol20 = roll(np.std, R, 20)
def pnl(sig, reff, cost):
s = sig.copy(); s[~univ] = np.nan
return pnl_w(xs_weights(s), reff, cost_bp=cost)
def peryear(p):
return [sharpe_t(torch.tensor(p[year[1:] == y], device=DEV, dtype=torch.float64))
if (year[1:] == y).sum() > 30 else float("nan") for y in yrs]
# ---------- (a) momentum constructions ----------
raw20 = tr(lc, 20)
cons = {
"raw_20": raw20,
"rank_20": row_rank(raw20),
"volscaled_20": raw20 / np.where(vol20 > 0, vol20, np.nan),
"pathconsist_20": roll(np.mean, np.sign(R), 20),
"skiprecent_20s3": tr(lc, 20, skip=3),
"winsorinput_20": roll(np.sum, np.clip(R, -0.05, 0.05), 20),
}
print(f"\n===== (a) ROBUST MOMENTUM CONSTRUCTION (PIT top-{TOPK}, stress=winsor±{int(WINSOR*100)}%+death+{int(COST)}bp) =====")
print(f"{'construction':>16} {'base_full':>9} {'str_full':>8} {'str_OOS':>7} {'str_CPCV':>8} {'ratio':>5} | per-year(stress) 2020..26")
abest, abest_pnl, abest_score = None, None, -9
for nm, sg in cons.items():
pb = pnl(sg, Reff_base, 10.0); ps_ = pnl(sg, Reff_str, COST)
sb = sharpe_t(torch.tensor(pb, device=DEV, dtype=torch.float64))
v = validate(ps_, days, 6)
ratio = v["full"] / sb if sb and not math.isnan(sb) and sb != 0 else float("nan")
py = peryear(ps_)
pys = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py)
nneg = sum(1 for p in py if not math.isnan(p) and p < 0)
score = v["med"] - 0.3 * nneg # robust: high stressed CPCV-med, few negative years
print(f"{nm:>16} {sb:>+9.2f} {v['full']:>+8.2f} {v['oos']:>+7.2f} {v['med']:>+8.2f} {ratio:>5.2f} | {pys}")
if score > abest_score:
abest, abest_pnl, abest_score = nm, ps_, score
print(f" -> robust momentum sleeve = {abest}")
# ---------- (b) diversifying second edge ----------
sleeve = abest_pnl
cand = {
"carry_7": -roll(np.mean, fund, 7),
"TStrend_30": np.sign(tr(lc, 30)) * np.where(univ, 1.0, np.nan),
"LTreversal_180": -tr(lc, 180),
"STreversal_2": -tr(lc, 2),
"lowvol": -vol20,
"size_small": -np.log(np.where(dv30 > 0, dv30, np.nan)),
}
print(f"\n===== (b) DIVERSIFYING EDGE vs momentum sleeve ({abest}) — stress applied =====")
print(f"{'candidate':>16} {'standalone':>10} {'corr_to_mom':>11} {'combined':>9} | combined per-year 2020..26")
sl = torch.tensor(sleeve, device=DEV, dtype=torch.float64)
sl_sd = float(sl.std())
combos = []
for nm, sg in cand.items():
pc = pnl(sg, Reff_str, COST)
a, b = np.asarray(sleeve), np.asarray(pc)
m = np.isfinite(a) & np.isfinite(b)
corr = float(np.corrcoef(a[m], b[m])[0, 1]) if m.sum() > 50 else float("nan")
sc = float(torch.tensor(pc, device=DEV, dtype=torch.float64).std())
wm, wc = (1 / sl_sd), (1 / sc if sc > 0 else 0)
comb = (wm * a + wc * b) / (wm + wc)
vc = sharpe_t(torch.tensor(comb, device=DEV, dtype=torch.float64))
py = peryear(comb)
pys = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py)
sa = sharpe_t(torch.tensor(pc, device=DEV, dtype=torch.float64))
print(f"{nm:>16} {sa:>+10.2f} {corr:>+11.2f} {vc:>+9.2f} | {pys}")
combos.append((nm, corr, vc, pc))
# ---------- final BOOK: momentum + diversifiers that lift combined Sharpe and are low-corr ----------
base_sh = sharpe_t(sl)
keep = [(nm, pc) for nm, corr, vc, pc in combos if vc > base_sh and (math.isnan(corr) or corr < 0.5)]
print(f"\n===== BOOK = momentum + {[k for k,_ in keep]} (inverse-vol weighted, stress) =====")
sleeves = [np.asarray(sleeve)] + [np.asarray(pc) for _, pc in keep]
sds = [float(np.nanstd(s)) for s in sleeves]
ws = np.array([1 / s if s > 0 else 0 for s in sds]); ws /= ws.sum()
book = sum(w * s for w, s in zip(ws, sleeves))
vb = validate(book, days, 6)
py = peryear(book)
pys = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py)
print(f" momentum-only Sharpe (stress) = {base_sh:+.2f}")
print(f" BOOK: full {vb['full']:+.2f} IS {vb['is_']:+.2f} OOS {vb['oos']:+.2f} CPCVmed {vb['med']:+.2f} CPCV5% {vb['p5']:+.2f}")
print(f" BOOK per-year 2020..26: {pys}")
print("\nVERDICT: book Sharpe > momentum-only with every-year positive => diversification works (deployable book).")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Capstone research: (2) low-vol full validation battery + (1) per-coin slippage/capacity model.
Point-in-time universe (daily top-50 by trailing dollar-vol, dead coins in). Returns are
death-spiral-excluded (last 5 untradeable days zeroed) but NOT blanket-winsorized — the
slippage model replaces the crude cap with realistic, liquidity-aware cost:
one-way cost_i = half_spread(ADV_i) + η·σ_i·sqrt(trade_notional_i / ADV_i) (square-root impact)
Sweeping AUM gives the capacity curve (Sharpe vs size) for momentum, low-vol, and the book.
Low-vol gets the same battery momentum passed (window sweep, coin-bootstrap, per-year, IS/OOS).
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep as ps # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
TOPK = 50
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def tr(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, qv, fund = ps.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = roll(np.mean, qv, 30)
vol20 = np.nan_to_num(roll(np.std, R, 20))
year = (1970 + days / 365.25).astype(int)
yrs = list(range(2020, 2027))
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
univ[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff = np.where(tradeable, R - fund, 0.0) # death-excl, NO blanket winsor
def W(sig):
s = sig.copy(); s[~univ] = np.nan
return xs_weights(s)
def peryear(p):
return [sharpe_t(torch.tensor(p[year[1:] == y], device=DEV, dtype=torch.float64))
if (year[1:] == y).sum() > 30 else float("nan") for y in yrs]
# ---------- (2) LOW-VOL BATTERY ----------
print(f"\n===== (2) LOW-VOL VALIDATION BATTERY (PIT top-{TOPK}, death-excl, 10bp) =====")
print(f"{'vol-window':>10} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5} | per-year 2020..26")
for Wd in [10, 20, 30, 60]:
sig = -np.nan_to_num(roll(np.std, R, Wd))
pnl = pnl_w(W(sig), Reff, cost_bp=10)
v = validate(pnl, days, 8)
py = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in peryear(pnl))
print(f"{Wd:>10} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f} | {py}")
# coin-bootstrap on lowvol(20)
rng = np.random.default_rng(7)
lvsig = -vol20
sh = []
for _ in range(200):
cols = rng.choice(N, size=max(N // 2, 10), replace=False)
sub = np.full((T, N), np.nan); sub[:, cols] = lvsig[:, cols]
s = sharpe_t(torch.tensor(pnl_w(W(sub), Reff, cost_bp=10), device=DEV, dtype=torch.float64))
if not math.isnan(s):
sh.append(s)
sh = np.array(sh)
print(f" coin-bootstrap(200): mean {sh.mean():+.2f} median {np.median(sh):+.2f} 5th {np.percentile(sh,5):+.2f} frac>0 {np.mean(sh>0):.2f}")
# ---------- (1) SLIPPAGE / CAPACITY MODEL ----------
w_mom = W(tr(lc, 20)) # raw 20d momentum (diversifies w/ lowvol, corr -0.13)
w_lv = W(-vol20)
w_book = 0.5 * (w_mom + w_lv) # netting preserved (offsetting positions reduce turnover)
def net_sharpe(w, AUM, eta=1.0):
turn = np.abs(w[1:] - w[:-1]) # fraction of AUM traded per coin
adv = dv30[1:]; sg = vol20[1:]
hs = np.clip(30.0 / np.sqrt(np.maximum(adv, 1.0) / 1e6), 1.0, 30.0) / 1e4 # half-spread frac
part = np.where(adv > 0, turn * AUM / adv, 0.0)
impact = eta * sg * np.sqrt(np.clip(part, 0, None))
cost = np.sum(turn * (hs + impact), axis=1)
gross = np.sum(w[:-1] * Reff[1:], axis=1)
return sharpe_t(torch.tensor(gross - cost, device=DEV, dtype=torch.float64))
print(f"\n===== (1) SLIPPAGE / CAPACITY — Sharpe vs AUM (sqrt-impact η=1, liquidity-scaled spread) =====")
aums = [1e6, 5e6, 2e7, 5e7, 2e8, 5e8]
print(f"{'sleeve':>12} " + " ".join(f"{'$'+(f'{a/1e6:.0f}M' if a<1e9 else f'{a/1e9:.1f}B'):>8}" for a in aums))
for nm, w in [("momentum", w_mom), ("lowvol", w_lv), ("BOOK", w_book)]:
row = [net_sharpe(w, a) for a in aums]
print(f"{nm:>12} " + " ".join(f"{r:>+8.2f}" for r in row))
# frictionless reference (death-excl only, no cost)
print(f"{'(grossSR)':>12} " + " ".join(
f"{sharpe_t(torch.tensor(np.sum(w_book[:-1]*Reff[1:],axis=1),device=DEV,dtype=torch.float64)):>+8.2f}" for _ in [0]) +
" <- book gross (no cost)")
print("\nVERDICT: Sharpe at $5-50M AUM = realistic deployable number; where it decays = capacity ceiling.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Turnover / capacity optimization for crypto XS momentum (the one validated edge).
Daily-rebalancing a 20-day signal pays ~5x wasted turnover. Test rebalance frequency,
signal smoothing, and universe breadth (top-K) against the same square-root slippage model.
Trade-off: weekly/top-20 cuts cost but top-20 cuts breadth (momentum needs dispersion).
Report gross Sharpe, avg daily turnover, and net Sharpe-vs-AUM per config.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep as ps # noqa: E402
from signal_sweep import xs_weights, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def main():
syms, days, close, qv, fund = ps.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = roll(np.mean, qv, 30)
vol20 = np.nan_to_num(roll(np.std, R, 20))
Reff = R - fund
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff = np.where(tradeable, Reff, 0.0)
raw20 = np.full_like(lc, np.nan); raw20[20:] = lc[20:] - lc[:-20]
def univ_mask(TOPK):
u = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
u[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
return u
def held_weights(TOPK, K, smooth):
sig = raw20.copy(); sig[~univ_mask(TOPK)] = np.nan
wt = xs_weights(sig)
if smooth > 1: # EWMA the target weights over time
a = 2.0 / (smooth + 1)
for t in range(1, T):
wt[t] = a * wt[t] + (1 - a) * wt[t - 1]
wh = wt.copy() # step-rebalance every K days
if K > 1:
for t in range(T):
wh[t] = wt[t - (t % K)]
return wh
def net_sharpe(wh, AUM, eta=1.0):
turn = np.abs(wh[1:] - wh[:-1])
adv = dv30[1:]; sg = vol20[1:]
hs = np.clip(30.0 / np.sqrt(np.maximum(adv, 1.0) / 1e6), 1.0, 30.0) / 1e4
part = np.where(adv > 0, turn * AUM / adv, 0.0)
cost = np.sum(turn * (hs + eta * sg * np.sqrt(np.clip(part, 0, None))), axis=1)
return sharpe_t(torch.tensor(np.sum(wh[:-1] * Reff[1:], axis=1) - cost, device=DEV, dtype=torch.float64))
aums = [1e6, 5e6, 2e7, 5e7, 2e8, 5e8]
print(f"\n===== TURNOVER / CAPACITY OPTIMIZATION (XS momentum-20, sqrt-impact η=1) =====")
print(f"{'config':>26} {'grossSR':>7} {'turn%':>6} | " + " ".join(
f"{('$'+(f'{a/1e6:.0f}M' if a<1e9 else f'{a/1e9:.1f}B')):>7}" for a in aums))
cfgs = [
("top50 daily (baseline)", 50, 1, 1),
("top50 weekly", 50, 5, 1),
("top50 weekly+smooth5", 50, 5, 5),
("top30 weekly+smooth5", 30, 5, 5),
("top20 weekly+smooth5", 20, 5, 5),
("top20 biweekly+smooth10", 20, 10, 10),
("top30 biweekly+smooth10", 30, 10, 10),
]
for nm, TOPK, K, sm in cfgs:
wh = held_weights(TOPK, K, sm)
gross = sharpe_t(torch.tensor(np.sum(wh[:-1] * Reff[1:], axis=1), device=DEV, dtype=torch.float64))
turn = float(np.mean(np.sum(np.abs(wh[1:] - wh[:-1]), axis=1))) * 100
row = [net_sharpe(wh, a) for a in aums]
print(f"{nm:>26} {gross:>+7.2f} {turn:>5.1f}% | " + " ".join(f"{r:>+7.2f}" for r in row))
print("\nVERDICT: best config's $20-50M net Sharpe = realistic deployable number at meaningful scale.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Diversifier hunt: directional TIME-SERIES trend (CTA-style) vs market-neutral XS momentum.
XS momentum (the validated edge) is market-NEUTRAL relative-value. A proper TS-trend sleeve is
DIRECTIONAL — net-long when coins trend up, net-SHORT when they trend down — so it should profit
in sustained bears (2022) when XS momentum is weak: the classic crisis-alpha complement.
Tests TS-trend standalone battery (lookback, per-year, bootstrap), its CORRELATION to the
momentum sleeve, and the COMBINED book. PIT universe, death-excl, realistic.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep as ps # noqa: E402
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
TOPK = 30
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def tr(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, qv, fund = ps.load()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
qv = np.nan_to_num(qv)
dv30 = roll(np.mean, qv, 30)
vol20 = np.nan_to_num(roll(np.std, R, 20))
year = (1970 + days / 365.25).astype(int); yrs = list(range(2020, 2027))
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
univ[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff = np.where(tradeable, R - fund, 0.0)
invvol = np.where(vol20 > 0, 1.0 / vol20, 0.0)
def wn(sig): # market-neutral (demeaned) unit-gross
s = sig.copy(); s[~univ] = np.nan
return xs_weights(s)
def wdir(sig): # DIRECTIONAL unit-gross (NOT demeaned) — net long/short exposure
s = np.where(univ, sig, 0.0)
g = np.sum(np.abs(s), axis=1, keepdims=True); g[g == 0] = 1
return s / g
def peryear(p):
return [sharpe_t(torch.tensor(p[year[1:] == y], device=DEV, dtype=torch.float64))
if (year[1:] == y).sum() > 30 else float("nan") for y in yrs]
mom_pnl = pnl_w(wn(tr(lc, 20)), Reff, cost_bp=10)
print(f"\n===== DIVERSIFIER HUNT: directional TS-trend vs XS momentum (PIT top-{TOPK}) =====")
print("XS momentum sleeve per-year: " + " ".join(f"{p:>+5.2f}" for p in peryear(mom_pnl)) +
f" (full {sharpe_t(torch.tensor(mom_pnl,device=DEV,dtype=torch.float64)):+.2f})")
print(f"\n[TS-trend] DIRECTIONAL sign(trail_L)*invvol, net exposure")
print(f"{'lookback':>9} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'corr_mom':>8} {'combined':>9} | per-year 2020..26 (TS)")
for L in [20, 30, 60, 90]:
ts = wdir(np.sign(tr(lc, L)) * invvol)
ts_pnl = pnl_w(ts, Reff, cost_bp=10)
v = validate(ts_pnl, days, 8)
a, b = mom_pnl, ts_pnl
m = np.isfinite(a) & np.isfinite(b)
corr = float(np.corrcoef(a[m], b[m])[0, 1])
sm = float(np.nanstd(a)); st = float(np.nanstd(b))
comb = ((1 / sm) * a + (1 / st) * b) / (1 / sm + 1 / st)
vc = sharpe_t(torch.tensor(comb, device=DEV, dtype=torch.float64))
py = " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in peryear(ts_pnl))
print(f"{L:>9} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {corr:>+8.2f} {vc:>+9.2f} | {py}")
# bootstrap on TS-trend(30)
rng = np.random.default_rng(11)
base = np.sign(tr(lc, 30)) * invvol
sh = []
for _ in range(200):
cols = rng.choice(N, size=max(N // 2, 10), replace=False)
sub = np.zeros((T, N)); sub[:, cols] = base[:, cols]
s = sharpe_t(torch.tensor(pnl_w(wdir(np.where(univ, sub, 0)), Reff, cost_bp=10), device=DEV, dtype=torch.float64))
if not math.isnan(s):
sh.append(s)
sh = np.array(sh)
print(f"\nTS-trend(30) coin-bootstrap(200): mean {sh.mean():+.2f} median {np.median(sh):+.2f} 5th {np.percentile(sh,5):+.2f} frac>0 {np.mean(sh>0):.2f}")
# best combined book per-year
ts30 = pnl_w(wdir(np.sign(tr(lc, 30)) * invvol), Reff, cost_bp=10)
sm, st = float(np.nanstd(mom_pnl)), float(np.nanstd(ts30))
book = ((1 / sm) * mom_pnl + (1 / st) * ts30) / (1 / sm + 1 / st)
vb = validate(book, days, 8)
print(f"\nBOOK = XS-momentum + TS-trend(30) (inv-vol): full {vb['full']:+.2f} IS {vb['is_']:+.2f} OOS {vb['oos']:+.2f} CPCVmed {vb['med']:+.2f} 5% {vb['p5']:+.2f}")
print("BOOK per-year 2020..26: " + " ".join(f"{p:>+5.2f}" for p in peryear(book)))
print("\nVERDICT: TS-trend real (battery) + low corr + 2022 positive + lifts book => genuine diversifier.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Deflated signal sweep — a factor zoo on data in hand, judged against the FULL search.
Wisdom: breadth without multiple-testing control is the fastest way to fool yourself.
So every signal's Deflated Sharpe is deflated by N_trials = the number of signals tested.
Batch 1 (instant, free, data already downloaded): 22-futures daily panel — cross-sectional
& time-series momentum, short-term reversal, low-vol — plus structural SEASONALITY
(overnight-drift, intraday, day-of-week). Each signal → daily PnL → full/IS/OOS Sharpe,
CPCV(45-path) median+5th-pct, Deflated Sharpe. Ranked by OOS. Survivor = DSR>0.95 AND
OOS>0 AND CPCV-median>0 (consistent + multiple-testing-honest).
Validation on GPU (torch); panel/signal staging on CPU (small). Roll-zeroed close-close
returns (screening; survivors get proper roll handling in deeper validation).
"""
import glob
import math
import re
from itertools import combinations
from statistics import NormalDist
import numpy as np
import torch
ND = NormalDist()
DEV = "cuda" if torch.cuda.is_available() else "cpu"
MONTHS = "FGHJKMNQUVXZ"
SPLIT_DAY = 19723 # 2024-01-01 in days-since-epoch
def load_panel():
import databento as db
series = {}
for p in sorted(glob.glob("data/surfer/*.dbn")):
root = p.split("/")[-1][:-4]
pat = re.compile("^" + re.escape(root) + "[" + MONTHS + r"]\d{1,2}$")
try:
df = db.DBNStore.from_file(p).to_df().reset_index()
except Exception:
continue
if df.empty or "close" not in df.columns:
continue
df = df[df["close"] > 0]
df = df[df["symbol"].astype(str).str.match(pat)]
if df.empty:
continue
df["day"] = (df["ts_event"].astype("int64") // (86_400 * 10**9))
idx = df.groupby("day")["volume"].idxmax()
f = df.loc[idx, ["day", "instrument_id", "open", "close"]].sort_values("day")
series[root] = (f["day"].to_numpy(np.int64), f["instrument_id"].to_numpy(np.int64),
f["open"].to_numpy(np.float64), f["close"].to_numpy(np.float64))
roots = sorted(series)
days = np.array(sorted(set().union(*[set(series[r][0].tolist()) for r in roots])))
di = {d: i for i, d in enumerate(days.tolist())}
T, N = len(days), len(roots)
close = np.full((T, N), np.nan); op = np.full((T, N), np.nan); inst = np.full((T, N), -1, np.int64)
for j, r in enumerate(roots):
d, ii, oo, cc = series[r]
for k in range(len(d)):
row = di[int(d[k])]; close[row, j] = cc[k]; op[row, j] = oo[k]; inst[row, j] = ii[k]
weekday = (days + 3) % 7 # epoch day0 = Thursday → 0=Mon..6=Sun
return roots, days, close, op, inst, weekday
def build_returns(close, op, inst):
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]
same = np.zeros((T, N), bool); same[1:] = inst[1:] == inst[:-1]
R = np.where(same & np.isfinite(R), R, 0.0)
ov = np.zeros((T, N)); ov[1:] = np.log(op[1:] / close[:-1])
ov = np.where(same & np.isfinite(ov), ov, 0.0)
intr = np.log(close / op); intr = np.where(np.isfinite(intr), intr, 0.0)
return lc, R, ov, intr
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def xs_weights(sig): # cross-sectional z-score, market-neutral, unit gross
mu = np.nanmean(sig, axis=1, keepdims=True); sd = np.nanstd(sig, axis=1, keepdims=True)
z = np.nan_to_num((sig - mu) / np.where(sd > 0, sd, 1))
g = np.sum(np.abs(z), axis=1, keepdims=True); g[g == 0] = 1
return z / g
def pnl_w(w, R, cost_bp=2.0):
p = np.sum(w[:-1] * R[1:], axis=1)
turn = np.sum(np.abs(w[1:] - w[:-1]), axis=1)
return p - turn * (cost_bp / 1e4)
def sharpe_t(x):
x = x[torch.isfinite(x)]
if len(x) < 10 or float(x.std()) == 0:
return float("nan")
return float(x.mean() / x.std() * math.sqrt(252))
def validate(pnl, days, n_trials):
x = torch.tensor(np.asarray(pnl), device=DEV, dtype=torch.float64)
n = len(x); full = sharpe_t(x)
dd = days[-n:]
ism = torch.tensor(dd < SPLIT_DAY, device=DEV)
sis = sharpe_t(x[ism])
soos = sharpe_t(x[~ism]) if int((~ism).sum()) > 30 else float("nan")
nb, k = 10, 2
blk = [torch.arange(i * n // nb, (i + 1) * n // nb, device=DEV) for i in range(nb)]
oos = [sharpe_t(x[torch.cat([blk[b] for b in c])]) for c in combinations(range(nb), k)]
oos = np.array([o for o in oos if not math.isnan(o)])
med = float(np.median(oos)) if len(oos) else float("nan")
p5 = float(np.percentile(oos, 5)) if len(oos) else float("nan")
srd = full / math.sqrt(252)
if n_trials > 1:
sr0 = (1 / math.sqrt(n)) * ((1 - 0.5772) * ND.inv_cdf(1 - 1.0 / n_trials)
+ 0.5772 * ND.inv_cdf(1 - 1.0 / (n_trials * math.e)))
else:
sr0 = 0.0
dsr = ND.cdf((srd - sr0) * math.sqrt(n - 1)) if not math.isnan(full) else float("nan")
return dict(full=full, is_=sis, oos=soos, med=med, p5=p5, dsr=dsr)
def main():
roots, days, close, op, inst, weekday = load_panel()
lc, R, ov, intr = build_returns(close, op, inst)
T, N = close.shape
vol63 = np.full_like(lc, np.nan)
for t in range(63, T):
vol63[t] = np.nanstd(R[t - 63:t], axis=0)
invvol = 1.0 / np.where(vol63 > 0, vol63, np.nan)
sig = {}
for L in [21, 63, 126, 252]:
sig[f"XS_mom_{L}"] = pnl_w(xs_weights(trailing(lc, L)), R)
for L in [1, 5]:
sig[f"XS_rev_{L}"] = pnl_w(xs_weights(-trailing(lc, L)), R)
sig["XS_lowvol"] = pnl_w(xs_weights(-vol63), R)
for L in [21, 63, 252]:
w = np.nan_to_num(np.sign(trailing(lc, L)) * invvol)
g = np.sum(np.abs(w), axis=1, keepdims=True); g[g == 0] = 1
sig[f"TS_mom_{L}"] = pnl_w(w / g, R)
sig["SEAS_overnight"] = np.nanmean(ov[1:], axis=1) - 1.0 / 1e4 # EW long overnight, ~1bp cost
sig["SEAS_intraday"] = np.nanmean(intr[1:], axis=1) - 1.0 / 1e4 # EW long intraday
ewR = np.nanmean(R, axis=1)
for d, nm in enumerate(["Mon", "Tue", "Wed", "Thu", "Fri"]):
sig[f"SEAS_{nm}"] = np.where(weekday[1:] == d, ewR[1:], 0.0)
NT = len(sig)
rows = [(nm, validate(p, days, NT)) for nm, p in sig.items()]
rows.sort(key=lambda r: -(r[1]["oos"] if not math.isnan(r[1]["oos"]) else -9))
print(f"\n===== DEFLATED SIGNAL SWEEP — Batch 1 (22 futures daily + seasonality) =====")
print(f"device={DEV} roots={len(roots)} days={T} N_trials(deflation)={NT}")
print(f"{'signal':>16} {'full':>7} {'IS':>7} {'OOS':>7} {'CPCVmed':>8} {'CPCV5%':>8} {'DSR':>6}")
for nm, v in rows:
print(f"{nm:>16} {v['full']:>+7.2f} {v['is_']:>+7.2f} {v['oos']:>+7.2f} "
f"{v['med']:>+8.2f} {v['p5']:>+8.2f} {v['dsr']:>6.2f}")
surv = [nm for nm, v in rows if v["dsr"] > 0.95 and v["oos"] > 0 and v["med"] > 0]
print(f"\nSURVIVORS (DSR>0.95 & OOS>0 & CPCVmed>0): {surv if surv else 'NONE'}")
print("DSR is deflated by N_trials → multiple-testing-honest. Survivors get deeper validation + crypto/COT batches.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""sixtyforty — local CLI paper-forward test of a simple 60/40 ETF portfolio (the ~0.7-Sharpe
non-crypto baseline). No agents, no cloud, no capital. Free Yahoo daily adjusted closes.
60% SPY (S&P 500) + 40% IEF (7-10yr Treasuries) — the deployable ETF analog of the ES/ZN 60/40
backtest (+0.72 Sharpe). Always invested (no regime filter); the test just confirms the realized
forward Sharpe/return tracks the backtest. Books each real trading day exactly once (catch-up on
weekends / missed runs), using ADJUSTED closes (dividends + coupons = true total return).
python3 sixtyforty_paper.py snapshot latest prices + recent daily portfolio returns
python3 sixtyforty_paper.py run book new trading days, persist (cron-compatible)
python3 sixtyforty_paper.py status cumulative return + annualized Sharpe/vol/maxDD
(alias: 'paper' == 'run')
"""
import datetime
import json
import math
import os
import sys
import urllib.request
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
STATE = os.path.join(_REPO, "data/surfer/sixtyforty_state.json")
INSTR = [("SPY", 0.6), ("IEF", 0.4)]
def get(url, tries=4):
import time
for a in range(tries):
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
return json.loads(urllib.request.urlopen(req, timeout=30).read())
except Exception:
if a == tries - 1:
raise
time.sleep(2 * (a + 1))
def daily_adjclose(sym):
"""{ 'YYYY-MM-DD': adjclose } for ~3 months of trading days (Yahoo, free)."""
u = f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=3mo"
res = get(u)["chart"]["result"][0]
ts = res["timestamp"]
ind = res["indicators"]
adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"]
out = {}
for t, c in zip(ts, adj):
if c is not None:
d = datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d")
out[d] = float(c)
return out
def load_state():
if os.path.exists(STATE):
return json.load(open(STATE))
return {"last_date": "", "days": 0, "sum_r": 0.0, "sumsq_r": 0.0,
"equity": 1.0, "peak": 1.0, "max_dd": 0.0}
def metrics(st):
n = st["days"]
if n < 2:
return None
mean = st["sum_r"] / n
var = max(st["sumsq_r"] / n - mean * mean, 0.0)
std = math.sqrt(var)
apr = mean * 252
vol = std * math.sqrt(252)
return dict(apr=apr, vol=vol, sharpe=(apr / vol if vol > 0 else float("nan")),
total=st["equity"] - 1.0, maxdd=st["max_dd"])
def series():
data = {s: daily_adjclose(s) for s, _ in INSTR}
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
return data, dates
def cmd_run():
st = load_state()
data, dates = series()
if len(dates) < 2:
print("not enough data"); return
os.makedirs(os.path.dirname(STATE), exist_ok=True)
if not st["last_date"]: # fresh: start forward from latest close, no backfill
st["last_date"] = dates[-1]
json.dump(st, open(STATE, "w"))
print(f"initialized forward tracking from {dates[-1]}; first booked return on the next trading day.")
return
booked = 0
for i in range(1, len(dates)):
d, dprev = dates[i], dates[i - 1]
if d <= st["last_date"]:
continue
r = sum(w * (data[s][d] / data[s][dprev] - 1.0) for s, w in INSTR) # daily-rebalanced port return
st["days"] += 1
st["sum_r"] += r
st["sumsq_r"] += r * r
st["equity"] *= (1 + r)
st["peak"] = max(st["peak"], st["equity"])
st["max_dd"] = min(st["max_dd"], st["equity"] / st["peak"] - 1.0)
st["last_date"] = d
booked += 1
os.makedirs(os.path.dirname(STATE), exist_ok=True)
json.dump(st, open(STATE, "w"))
m = metrics(st)
tail = f" | Sharpe {m['sharpe']:+.2f} APR {100*m['apr']:+.1f}% maxDD {100*m['maxdd']:+.1f}%" if m else ""
print(f"{datetime.date.today()} booked {booked} new trading day(s) -> day {st['days']} "
f"(through {st['last_date']}); total {100*(st['equity']-1):+.2f}%{tail}")
def cmd_status():
st = load_state()
m = metrics(st)
print(f"60/40 (SPY 60% / IEF 40%) paper — day {st['days']} (through {st['last_date'] or 'n/a'})")
print(f" total return: {100*(st['equity']-1):+.2f}%")
if m:
print(f" annualized: Sharpe {m['sharpe']:+.2f} return {100*m['apr']:+.1f}% vol {100*m['vol']:.1f}% maxDD {100*m['maxdd']:+.1f}%")
print(f" backtest reference: ~0.72 Sharpe (ES/ZN 60/40, 2010-2026)")
else:
print(" (need >=2 booked trading days for annualized stats)")
def cmd_snapshot():
data, dates = series()
last = dates[-1]
print(f"60/40 snapshot {datetime.date.today()} (latest close {last})")
for s, w in INSTR:
print(f" {s} ({int(w*100)}%): {data[s][last]:.2f}")
print(" recent daily portfolio returns:")
for i in range(max(1, len(dates) - 5), len(dates)):
d, dprev = dates[i], dates[i - 1]
r = sum(w * (data[s][d] / data[s][dprev] - 1.0) for s, w in INSTR)
print(f" {d}: {100*r:+.2f}%")
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
if cmd in ("run", "paper"):
cmd_run()
elif cmd == "status":
cmd_status()
elif cmd == "snapshot":
cmd_snapshot()
else:
print(__doc__)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Side-by-side comparison of the wealth candidates (the CAGR reference plan, re-runnable).
Backtest (2006-2026, incl 2008) + the $360k+$8k/mo 20y trajectory for: adaptive multi-strat book,
SPY buy-hold, SPY 1.3x (cheap futures financing), 60/40, 70/30 SPY+book, SPY+adaptive overlay.
Honest: CAGR builds wealth; more CAGR = more drawdown. See docs/.../2026-06-08-cagr-reference-plan.md.
python3 strategy_compare.py # full table
python3 strategy_compare.py --p0 100000 --pmt 2000 --fin 0.03 --years 20
"""
import datetime
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from multistrat_paper import book_series # noqa: E402
def arg(flag, default, cast=float):
return cast(sys.argv[sys.argv.index(flag) + 1]) if flag in sys.argv else default
def yh(s):
r = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/{s}?interval=1d&range=25y",
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
a = r["indicators"].get("adjclose", [{}])[0].get("adjclose") or r["indicators"]["quote"][0]["close"]
ts = r["timestamp"]
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, a) if c is not None}
def stats(r):
r = np.nan_to_num(r)
eq = np.cumprod(1 + r); cagr = eq[-1] ** (252 / len(r)) - 1
vol = r.std() * math.sqrt(252)
dd = float((eq / np.maximum.accumulate(eq) - 1).min())
rf = 0.03; sharpe = (r.mean() * 252 - rf) / vol if vol > 0 else 0 # excess-over-financing Sharpe
return cagr, vol, sharpe, dd
def main():
P0 = arg("--p0", 360000.0); PMT = arg("--pmt", 8000.0); FIN = arg("--fin", 0.03); YEARS = int(arg("--years", 20))
syms = ["SPY", "IEF", "GLD", "DBC"]
data = {s: yh(s) for s in syms}
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
T = len(dates)
Pm = np.column_stack([np.array([data[s][d] for d in dates]) for s in syms])
R = np.zeros((T, 4)); R[1:] = Pm[1:] / Pm[:-1] - 1
spy, ief = R[:, 0], R[:, 1]
# multi-strat book (proxy: SPY/IEF/GLD/DBC + DIY trend sleeve)
lc = np.log(Pm); sg = np.zeros((T, 4)); sg[252:] = np.sign(lc[252:] - lc[:-252])
vol = np.full((T, 4), np.nan)
for t in range(63, T):
vol[t] = R[t - 63:t].std(0)
iv = 1 / np.where(vol > 0, vol, np.nan); w = np.nan_to_num(sg * iv)
g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
trend = np.zeros(T); trend[1:] = np.sum(w[:-1] * R[1:], axis=1)
book, _, _ = book_series(np.column_stack([R, trend]))
# SPY + adaptive overlay (vol-target 15% + 200d trend + drawdown de-lever)
lp = np.log(Pm[:, 0]); ma = np.array([lp[max(0, t - 200):t].mean() if t >= 200 else lp[t] for t in range(T)])
ovr = np.zeros(T); eqo = pk = 1.0
for t in range(63, T - 1):
rv = spy[t - 63:t].std() * math.sqrt(252); vs = min(1.0, 0.15 / (rv + 1e-9))
tr = 1.0 if lp[t] > ma[t] else 0.3
dd = eqo / pk - 1; dds = float(np.clip(1 + 3 * min(0, dd + 0.07), 0.4, 1.0))
e = float(np.clip(vs * tr * dds, 0, 1.0)); ovr[t + 1] = e * spy[t + 1]; eqo *= (1 + ovr[t + 1]); pk = max(pk, eqo)
cands = {
"SPY buy-hold (1.0x)": spy,
"SPY 1.3x (cheap fin)": 1.3 * spy - 0.3 * FIN / 252,
"60/40 (SPY/IEF)": 0.6 * spy + 0.4 * ief,
"70/30 SPY+book": 0.7 * spy + 0.3 * book,
"SPY+adaptive overlay": ovr,
"adaptive multi-strat book": book,
}
def fv(cagr):
rm = cagr / 12; N = YEARS * 12; gg = (1 + rm) ** N
return P0 * gg + PMT * ((gg - 1) / rm)
print(f"STRATEGY COMPARISON — {dates[0]}..{dates[-1]} | ${P0:,.0f} + ${PMT:,.0f}/mo, {YEARS}y, fin {100*FIN:.0f}%")
print(f" {'strategy':>26} | CAGR | vol | Sharpe* | maxDD | {YEARS}y wealth")
for nm, r in cands.items():
c, v, sh, dd = stats(r)
print(f" {nm:>26} | {100*c:>4.1f}% | {100*v:>3.0f}% | {sh:>+5.2f} | {100*dd:>+5.0f}% | ${fv(c):>13,.0f}")
print(" *Sharpe = excess over financing (the leverage-relevant one). More CAGR ALWAYS = deeper maxDD.")
print(" Reference plan: docs/superpowers/specs/2026-06-08-cagr-reference-plan.md")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""Surfer PoC — crypto cross-sectional momentum (the validated edge), end-to-end.
Modes:
backtest — run the strategy over the cached point-in-time history; print honest stats
(gross/net Sharpe at AUM, OOS, CPCV-median, DSR, per-year, max-DD, turnover, capacity curve)
regime — diagnostic: does any regime feature predict when momentum works? (overlay is OFF by default)
paper — forward paper-trade: fetch live data, compute today's target weights, log intended trades,
mark the paper book (price + funding), persist state. The only true out-of-sample test.
status — print the paper book + equity curve summary.
Design: ONE signal function (`compute_weights`) drives BOTH backtest and live (no backtest/live skew).
No lookahead (signal uses closed bars <= t; weights apply to t+1; rebalance keyed on calendar day%K).
Funding is P&L (longs pay, shorts earn). Realistic sqrt-impact cost. Validated edge only; regime overlay
stays a diagnostic until separately confirmed.
"""
import argparse
import json
import math
import os
import sys
import time
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import xs_weights, validate, sharpe_t # noqa: E402
import pit_sweep # noqa: E402 (load() of the cached PIT universe)
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_MS = 86_400_000
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # cwd-independent (cron-safe)
STATE = os.path.join(_REPO, "data/surfer/poc_state.json")
CFG = dict(
lookback=20, # XS momentum horizon (days)
beta_win=60, # rolling window for market-beta (residual momentum strips this)
topk=30, # universe = top-K liquid by trailing dollar-vol (breadth/liquidity optimum)
rebal_k=7, # rebalance every 7 calendar days (weekly)
smooth_span=5, # EWMA weight-smoothing span (cuts turnover + whipsaw)
vol_win=30, # trailing vol window (for sizing / regime)
cost_aum=2e7, # AUM the slippage model is priced at ($20M)
eta=1.0, # sqrt market-impact coefficient (conservative)
dd_breaker=-0.20, # cut gross exposure if rolling drawdown worse than this
vrp_risk_frac=0.30, # VRP diversifier sleeve risk budget (small — short-vol tail)
vrp_rise_k=5, # don't sell vol when DVOL rose over last K days (tail gate)
vrp_level_win=60, # sell full size only when DVOL < trailing-win median
)
# ---------------------------------------------------------------- helpers
def roll(fn, X, L):
out = np.full_like(X, np.nan)
for t in range(L, len(X)):
out[t] = fn(X[t - L:t], axis=0)
return out
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def ewma_rows(W, span):
if span <= 1:
return W
a = 2.0 / (span + 1)
out = W.copy()
for t in range(1, len(W)):
out[t] = a * W[t] + (1 - a) * out[t - 1]
return out
# ---------------------------------------------------------------- SHARED SIGNAL (backtest == live)
def compute_weights(close, qvol, days, cfg):
"""Panel -> held target weights [T,N] (market-neutral, weekly, smoothed). No lookahead.
Live takes the last row; backtest uses all rows. Identical code path."""
T, N = close.shape
lc = np.log(close)
dv = roll(np.mean, np.nan_to_num(qvol), 30)
univ = np.zeros((T, N), bool)
for t in range(T):
elig = np.where((dv[t] > 0) & np.isfinite(close[t]))[0]
if len(elig):
univ[t, elig[np.argsort(-dv[t, elig])[:cfg["topk"]]]] = True
# residual (beta-stripped) momentum: strip each coin's beta to the equal-weight market,
# rank the residual trend (Phase-1: +0.72 vs +0.57 raw — cleaner base edge). No lookahead.
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]; R = np.where(np.isfinite(R), R, 0.0)
mkt = np.array([R[t][univ[t]].mean() if univ[t].any() else 0.0 for t in range(T)])
Wb = cfg["beta_win"]; beta = np.zeros((T, N))
for t in range(Wb, T):
mw = mkt[t - Wb:t]; vb = mw.var() + 1e-12
beta[t] = ((R[t - Wb:t] * mw[:, None]).mean(0) - R[t - Wb:t].mean(0) * mw.mean()) / vb
rcum = np.cumsum(R - beta * mkt[:, None], axis=0)
L = cfg["lookback"]
sig = np.full((T, N), np.nan); sig[L:] = rcum[L:] - rcum[:-L]
sig[~univ] = np.nan
wt = xs_weights(sig) # daily target (market-neutral, unit gross)
wt = ewma_rows(wt, cfg["smooth_span"]) # smooth
held = wt.copy() # weekly rebalance on FIXED calendar phase (day%K==0)
K = cfg["rebal_k"] # epoch day0=Thu → rebalances every Thursday, live-consistent
last = 0
for t in range(T):
if days[t] % K == 0:
last = t
held[t] = wt[last]
return held, univ
def slippage_net(w, Reff, dv, vol, days, cfg):
"""Net daily PnL series under sqrt market-impact + liquidity-scaled spread at cfg['cost_aum']."""
AUM = cfg["cost_aum"]
turn = np.abs(w[1:] - w[:-1])
adv = np.nan_to_num(dv[1:]); sg = np.nan_to_num(vol[1:])
hs = np.clip(30.0 / np.sqrt(np.maximum(adv, 1.0) / 1e6), 1.0, 30.0) / 1e4
part = np.where(adv > 0, turn * AUM / adv, 0.0)
cost = np.sum(turn * (hs + cfg["eta"] * sg * np.sqrt(np.clip(part, 0, None))), axis=1)
return np.sum(w[:-1] * Reff[1:], axis=1) - cost
def max_drawdown(pnl):
eq = np.cumsum(np.nan_to_num(pnl)); peak = np.maximum.accumulate(eq)
return float((eq - peak).min())
# ---------------------------------------------------------------- VRP SLEEVE (tail-managed short-vol diversifier)
def _dvol(ccy, refresh):
cache = os.path.join(_REPO, "data/surfer/dvol", f"{ccy}.json")
d = {int(k): v for k, v in json.load(open(cache)).items()} if os.path.exists(cache) else {}
if refresh:
end = int(time.time() * 1000); start = end - 200 * DAY_MS
try:
rows = _get(f"https://www.deribit.com/api/v2/public/get_volatility_index_data?currency={ccy}"
f"&start_timestamp={start}&end_timestamp={end}&resolution=86400").get("result", {}).get("data", [])
for r in rows:
d[int(r[0]) // DAY_MS] = float(r[4])
os.makedirs(os.path.dirname(cache), exist_ok=True)
json.dump({str(k): v for k, v in d.items()}, open(cache, "w"))
except Exception:
pass
return d
def vrp_ccy(iv, close, cfg):
"""Tail-managed short-variance daily P&L per ccy. Gate uses DVOL up to t-1 (no lookahead)."""
days = sorted(set(iv) & set(close)); pnl = {}
w0 = max(cfg["vrp_level_win"], cfg["vrp_rise_k"]) + 1
for i in range(w0, len(days)):
d0, d1 = days[i - 1], days[i]
if close[d0] <= 0 or close[d1] <= 0:
continue
r = math.log(close[d1] / close[d0]); ivl = iv[d0] / 100.0
raw = ivl * ivl / 365.0 - r * r # collect implied var, pay realized
rising = iv[d0] > iv[days[i - 1 - cfg["vrp_rise_k"]]] # don't sell into rising vol
win = [iv[days[j]] for j in range(i - 1 - cfg["vrp_level_win"], i - 1)]
level = 1.0 if iv[d0] < float(np.median(win)) else 0.5 # full in calm, half elevated
pnl[d1] = (0.0 if rising else 1.0) * level * raw
return pnl
def vrp_book_series(cfg, btc_close, eth_close, refresh):
b = vrp_ccy(_dvol("BTC", refresh), btc_close, cfg)
e = vrp_ccy(_dvol("ETH", refresh), eth_close, cfg)
days = sorted(set(b) | set(e))
return {d: float(np.nanmean([b.get(d, np.nan), e.get(d, np.nan)])) for d in days}
def _closes_from(syms, days, close, sym):
if sym not in syms:
return {}
j = syms.index(sym)
return {int(days[t]): float(close[t, j]) for t in range(len(days)) if np.isfinite(close[t, j])}
def _vrp_calib(cfg):
"""From cached history: (momentum daily vol, VRP daily vol) — to size VRP to its risk budget."""
syms, days, close, qv, fund = pit_sweep.load()
R = np.zeros_like(close); R[1:] = np.log(close)[1:] - np.log(close)[:-1]; R = np.where(np.isfinite(R), R, 0.0)
cf = np.where(np.isfinite(fund), fund, 0.0)
w, _ = compute_weights(close, qv, days, cfg)
mom = np.sum(w[:-1] * (R - cf)[1:], axis=1)
vrpb = vrp_book_series(cfg, _closes_from(syms, days, close, "BTCUSDT"),
_closes_from(syms, days, close, "ETHUSDT"), refresh=False)
return float(np.nanstd(mom)), float(np.nanstd(np.array(list(vrpb.values()))))
# ---------------------------------------------------------------- BACKTEST
def backtest(cfg):
syms, days, close, qv, fund = pit_sweep.load()
T, N = close.shape
R = np.zeros((T, N)); R[1:] = np.log(close)[1:] - np.log(close)[:-1]
R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
tradeable = np.ones((T, N), bool)
for j in range(N):
idx = np.where(np.isfinite(close[:, j]))[0]
if len(idx):
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
Reff = np.where(tradeable, R - fund, 0.0)
dv = roll(np.mean, np.nan_to_num(qv), 30)
vol = roll(np.std, R, cfg["vol_win"])
w, univ = compute_weights(close, qv, days, cfg)
gross = np.sum(w[:-1] * Reff[1:], axis=1)
net = slippage_net(w, Reff, dv, vol, days, cfg)
year = (1970 + days / 365.25).astype(int)
v = validate(net, days, 30)
gsr = sharpe_t(torch.tensor(gross, device=DEV, dtype=torch.float64))
turn = float(np.mean(np.sum(np.abs(w[1:] - w[:-1]), axis=1))) * 100
print(f"\n===== SURFER PoC BACKTEST — XS momentum (top{cfg['topk']}, {cfg['lookback']}d, weekly, smooth{cfg['smooth_span']}) =====")
print(f"coins(ever)={N} days={T} universe/day~{int(univ.sum(1).mean())} cost-AUM=${cfg['cost_aum']/1e6:.0f}M turnover/day={turn:.1f}%")
print(f"gross Sharpe {gsr:+.2f} | NET Sharpe {v['full']:+.2f} IS {v['is_']:+.2f} OOS {v['oos']:+.2f} CPCVmed {v['med']:+.2f} DSR {v['dsr']:.2f}")
print(f"max-DD (sum-rets) {max_drawdown(net):+.2f}")
py = [sharpe_t(torch.tensor(net[year[1:] == y], device=DEV, dtype=torch.float64)) if (year[1:] == y).sum() > 30 else float('nan') for y in range(2020, 2027)]
print("per-year 2020..26: " + " ".join(f"{p:+.2f}" if not math.isnan(p) else " n/a" for p in py))
print("capacity: " + " ".join(
f"${a/1e6:.0f}M={sharpe_t(torch.tensor(slippage_net(w,Reff,dv,vol,days,{**cfg,'cost_aum':a}),device=DEV,dtype=torch.float64)):+.2f}"
for a in [1e6, 5e6, 2e7, 5e7, 2e8]))
# ---- VRP diversifier sleeve + combined two-sleeve book ----
vrpb = vrp_book_series(cfg, _closes_from(syms, days, close, "BTCUSDT"),
_closes_from(syms, days, close, "ETHUSDT"), refresh=False)
mom_by = {int(days[1:][i]): net[i] for i in range(len(net))}
common = sorted(set(vrpb) & set(mom_by))
if common:
mo = np.array([mom_by[d] for d in common]); vr = np.array([vrpb[d] for d in common])
mu, vu, f = np.nanstd(mo), np.nanstd(vr), cfg["vrp_risk_frac"]
comb = (mo / mu) * (1 - f) + (vr / vu) * f
cy = (1970 + np.array(common) / 365.25).astype(int)
T = lambda x: torch.tensor(x, device=DEV, dtype=torch.float64)
print(f"VRP sleeve (tail-managed) Sharpe {sharpe_t(T(vr)):+.2f} corr-to-momentum {np.corrcoef(mo,vr)[0,1]:+.2f}")
print(f"COMBINED BOOK (momentum {int((1-f)*100)}% + VRP {int(f*100)}% risk) Sharpe {sharpe_t(T(comb)):+.2f}")
print("combined per-year: " + " ".join(
f"{y}:{sharpe_t(T(comb[cy==y])):+.2f}" for y in range(2021, 2027) if (cy == y).sum() > 30))
# ---------------------------------------------------------------- REGIME DIAGNOSTIC (overlay default OFF)
def regime(cfg):
syms, days, close, qv, fund = pit_sweep.load()
T, N = close.shape
R = np.zeros((T, N)); R[1:] = np.log(close)[1:] - np.log(close)[:-1]; R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
w, univ = compute_weights(close, qv, days, cfg)
book = np.sum(w[:-1] * (np.where(np.isfinite(R), R, 0.0) - fund)[1:], axis=1) # daily book return
lc = np.log(close)
disp = np.array([np.nanstd(trailing(lc, cfg["lookback"])[t][univ[t]]) if univ[t].any() else np.nan for t in range(T)])
mvol = np.nanmedian(roll(np.std, R, cfg["vol_win"]), axis=1)
mtrend = np.array([np.nanmedian(np.abs(trailing(lc, cfg["lookback"])[t][univ[t]])) if univ[t].any() else np.nan for t in range(T)])
feats = {"xs_dispersion": disp[:-1], "median_vol": mvol[:-1], "abs_trend": mtrend[:-1]}
split = int(0.7 * len(book))
print("\n===== REGIME DIAGNOSTIC — does a feature predict next-day book return? (overlay OFF until confirmed) =====")
print(f"{'feature':>16} {'IC_all':>7} {'IC_IS':>7} {'IC_OOS':>7}")
for nm, f in feats.items():
f = f[1:]; b = book[1:] # feature_t -> book return_{t+1}
m = np.isfinite(f) & np.isfinite(b)
def ic(mask):
mm = m & mask
return float(np.corrcoef(f[mm], b[mm])[0, 1]) if mm.sum() > 50 else float("nan")
idx = np.arange(len(f))
print(f"{nm:>16} {ic(np.ones_like(m)):>+7.3f} {ic(idx < split):>+7.3f} {ic(idx >= split):>+7.3f}")
print("Significant + IS/OOS-consistent IC => regime sizing worth building. Otherwise momentum runs flat-sized.")
# ---------------------------------------------------------------- LIVE (paper)
def _get(u):
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
def _live_panel(cfg):
info = _get("https://fapi.binance.com/fapi/v1/exchangeInfo")
perps = {s["symbol"] for s in info["symbols"] if s.get("contractType") == "PERPETUAL"
and s.get("quoteAsset") == "USDT" and s.get("status") == "TRADING"}
tick = _get("https://fapi.binance.com/fapi/v1/ticker/24hr")
vol = {t["symbol"]: float(t["quoteVolume"]) for t in tick if t["symbol"] in perps and t["symbol"].isascii()}
syms = sorted(vol, key=lambda s: -vol[s])[:max(cfg["topk"] * 2, 60)] # wide net; signal picks top-K
need = max(cfg["lookback"] + cfg["vol_win"], cfg["vrp_level_win"] + cfg["vrp_rise_k"],
cfg["beta_win"] + cfg["lookback"]) + 12 # cover VRP gate + residual-momentum beta window
closes, qvols, funds, keep = {}, {}, {}, []
for s in syms:
k = _get(f"https://fapi.binance.com/fapi/v1/klines?symbol={s}&interval=1d&limit={need}")
if len(k) < need - 2:
continue
d = np.array([int(r[0]) // DAY_MS for r in k])
closes[s] = (d, np.array([float(r[4]) for r in k]), np.array([float(r[7]) for r in k]))
fr = _get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={s}&limit=10")
funds[s] = float(fr[-1]["fundingRate"]) * 3 if fr else 0.0 # ~daily funding (3x 8h)
keep.append(s)
time.sleep(0.05)
days = np.array(sorted(set().union(*[set(closes[s][0].tolist()) for s in keep])))
di = {int(v): i for i, v in enumerate(days.tolist())}
T, N = len(days), len(keep)
close = np.full((T, N), np.nan); qv = np.full((T, N), np.nan)
for j, s in enumerate(keep):
d, c, q = closes[s]
for i in range(len(d)):
close[di[int(d[i])], j] = c[i]; qv[di[int(d[i])], j] = q[i]
return keep, days, close, qv, np.array([funds[s] for s in keep])
def _load_state():
if os.path.exists(STATE):
return json.load(open(STATE))
return dict(inception=None, last_mark_day=None, positions={}, prices={}, equity=1.0, log=[])
def _save_state(st):
tmp = STATE + ".tmp"
json.dump(st, open(tmp, "w"), indent=1)
os.replace(tmp, STATE)
def paper(cfg):
keep, days, close, qv, fund_now = _live_panel(cfg)
w, univ = compute_weights(close, qv, days, cfg)
target = {keep[j]: float(w[-1, j]) for j in range(len(keep)) if abs(w[-1, j]) > 1e-6}
last_close = {keep[j]: float(close[-1, j]) for j in range(len(keep)) if np.isfinite(close[-1, j])}
today = int(days[-1])
st = _load_state()
if st["inception"] is None:
st["inception"] = today
st.setdefault("vrp_equity", 1.0); st.setdefault("combined_equity", 1.0)
if "vrp_vu" not in st:
try:
st["vrp_mu"], st["vrp_vu"] = _vrp_calib(cfg)
except Exception:
st["vrp_mu"], st["vrp_vu"] = 0.0, 1.0
vrpb = vrp_book_series(cfg, _closes_from(keep, days, close, "BTCUSDT"),
_closes_from(keep, days, close, "ETHUSDT"), refresh=True)
vrp_today = vrpb.get(today, None)
# 1) MARK both sleeves to latest prices (+ funding) since last mark
if st["positions"] and st["last_mark_day"] != today:
ret = 0.0
for s, pos in st["positions"].items():
p0 = st["prices"].get(s); p1 = last_close.get(s)
if p0 and p1 and p0 > 0:
ret += pos * (p1 / p0 - 1.0)
ret -= pos * fund_now[keep.index(s)] if s in keep else 0.0 # long pays funding
f = cfg["vrp_risk_frac"]
scaled = (vrp_today / st["vrp_vu"]) * st["vrp_mu"] if (vrp_today is not None and st["vrp_vu"] > 0) else 0.0
comb_ret = (1 - f) * ret + f * scaled # VRP scaled to momentum vol, f risk
st["equity"] *= (1.0 + ret)
st["vrp_equity"] *= (1.0 + scaled)
st["combined_equity"] *= (1.0 + comb_ret)
st["log"].append({"day": today, "mom_ret": round(ret, 6), "vrp_ret": round(scaled, 6),
"comb_ret": round(comb_ret, 6), "combined": round(st["combined_equity"], 5)})
# 2) REBALANCE on the weekly boundary
rebal = (st["last_mark_day"] is None) or (today % cfg["rebal_k"] == 0) # fixed weekly phase (Thursdays)
trades = []
if rebal:
cur = st["positions"]
allk = set(cur) | set(target)
for s in sorted(allk):
d = target.get(s, 0.0) - cur.get(s, 0.0)
if abs(d) > 1e-4:
trades.append({"sym": s, "side": "BUY" if d > 0 else "SELL", "dweight": round(d, 4)})
st["positions"] = target
st["prices"] = last_close
st["last_mark_day"] = today
_save_state(st)
print(f"\n===== SURFER PoC PAPER — day {today} (universe {len(keep)} perps) =====")
print(f"momentum {st['equity']:.4f} | VRP {st['vrp_equity']:.4f} | COMBINED BOOK {st['combined_equity']:.4f} "
f"(inception day {st['inception']}, {len(st['log'])} marks)")
print(f"VRP today: {'flat (gated)' if (vrp_today is not None and abs(vrp_today)<1e-9) else ('%.5f'%vrp_today if vrp_today is not None else 'n/a')} "
f"rebalance: {rebal} intended trades: {len(trades)}")
longs = sorted([(s, x) for s, x in target.items() if x > 0], key=lambda z: -z[1])[:8]
shorts = sorted([(s, x) for s, x in target.items() if x < 0], key=lambda z: z[1])[:8]
print("top longs : " + ", ".join(f"{s} {x:+.3f}" for s, x in longs))
print("top shorts: " + ", ".join(f"{s} {x:+.3f}" for s, x in shorts))
for t in trades[:12]:
print(f" {t['side']:>4} {t['sym']:>12} dW {t['dweight']:+.4f}")
print("(paper only — no real orders. Re-run weekly to forward-test edge persistence.)")
def status(cfg):
st = _load_state()
print(f"\n===== SURFER PoC STATUS =====")
if st["inception"] is None:
print("no paper state yet — run: python scripts/surfer/surfer_poc.py paper"); return
print(f"inception day {st['inception']} last mark {st['last_mark_day']} marks {len(st['log'])}")
print(f"equity: momentum {st['equity']:.4f} | VRP {st.get('vrp_equity',1.0):.4f} | COMBINED {st.get('combined_equity',1.0):.4f}")
if st["log"]:
def srp(key):
r = np.array([m[key] for m in st["log"] if key in m])
return r.mean() / (r.std() + 1e-9) * math.sqrt(252) if len(r) > 5 else float("nan")
print(f"annual-ish Sharpe (live): momentum {srp('mom_ret'):+.2f} | VRP {srp('vrp_ret'):+.2f} | COMBINED {srp('comb_ret'):+.2f}")
print(f"current book: {len([p for p in st['positions'].values() if abs(p)>1e-6])} positions")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("mode", choices=["backtest", "regime", "paper", "status"])
a = ap.parse_args()
{"backtest": backtest, "regime": regime, "paper": paper, "status": status}[a.mode](CFG)

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Crypto volatility-risk-premium (VRP) as a diversifier to the momentum book.
VRP = implied vol (Deribit DVOL, BTC+ETH) systematically exceeds realized vol; a vol-seller
harvests it. Short-variance daily P&L proxy: vrp_t = (DVOL_{t-1}/100)^2/365 - r_t^2
(collect implied daily variance, pay realized squared return). Approximate (no Greeks) but
captures the premium's edge + correlation — enough for a diversifier CHECK.
Then the prize: Sharpe of VRP, per-year (incl. crash years), worst day, CORRELATION to the
crypto momentum book, and the combined two-premium book. A real diversifier needs Sharpe>=~0.4
AND low correlation. Honest: short-vol has negative skew -> Sharpe FLATTERS it; watch the tails.
"""
import json
import math
import os
import sys
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
from surfer_poc import compute_weights, CFG # noqa: E402
from signal_sweep import sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_MS = 86_400_000
DVOL_DIR = "data/surfer/dvol"
def _get(u):
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
def dvol(ccy):
os.makedirs(DVOL_DIR, exist_ok=True)
cache = f"{DVOL_DIR}/{ccy}.json"
if os.path.exists(cache):
return {int(k): v for k, v in json.load(open(cache)).items()}
out, end, start = {}, 1780704000000, 1577836800000
for _ in range(8):
u = f"https://www.deribit.com/api/v2/public/get_volatility_index_data?currency={ccy}&start_timestamp={start}&end_timestamp={end}&resolution=86400"
d = _get(u).get("result", {}).get("data", [])
if not d:
break
for row in d:
out[int(row[0]) // DAY_MS] = float(row[4])
end = d[0][0] - 1
if len(d) < 1000:
break
json.dump({str(k): v for k, v in out.items()}, open(cache, "w"))
return out
def main():
syms, cdays, cclose, cqv, cfund = pit_sweep.load()
idx = {s: j for j, s in enumerate(syms)}
# realized daily returns for BTC/ETH
def closes(sym):
j = idx[sym]; c = cclose[:, j]
return {int(cdays[t]): c[t] for t in range(len(cdays)) if np.isfinite(c[t])}
btc, eth = closes("BTCUSDT"), closes("ETHUSDT")
dv_btc, dv_eth = dvol("BTC"), dvol("ETH")
print(f"DVOL coverage: BTC {len(dv_btc)}d ({min(dv_btc)}..{max(dv_btc)}), ETH {len(dv_eth)}d")
def vrp_series(px, dv):
days = sorted(set(px) & set(dv))
pnl = {}
for i in range(1, len(days)):
d0, d1 = days[i - 1], days[i]
if px[d0] > 0 and px[d1] > 0:
r = math.log(px[d1] / px[d0])
iv = dv[d0] / 100.0
pnl[d1] = (iv * iv) / 365.0 - r * r # short-variance daily P&L
return pnl
vb, ve = vrp_series(btc, dv_btc), vrp_series(eth, dv_eth)
alld = sorted(set(vb) | set(ve))
vrp = np.array([np.nanmean([vb.get(d, np.nan), ve.get(d, np.nan)]) for d in alld])
alld = np.array(alld)
year = (1970 + alld / 365.25).astype(int)
sr = sharpe_t(torch.tensor(vrp, device=DEV, dtype=torch.float64))
print(f"\n===== CRYPTO VRP (short-vol BTC+ETH) =====")
print(f"days={len(vrp)} Sharpe {sr:+.2f} worst-day {np.nanmin(vrp)/np.nanstd(vrp):+.1f}σ skew {float(((vrp-np.nanmean(vrp))**3).mean()/np.nanstd(vrp)**3):+.2f}")
print("per-year: " + " ".join(f"{y}:{sharpe_t(torch.tensor(vrp[year==y],device=DEV,dtype=torch.float64)):+.2f}" for y in range(2021, 2027) if (year == y).sum() > 30))
# crypto momentum book
cR = np.zeros_like(cclose); cR[1:] = np.log(cclose)[1:] - np.log(cclose)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0)
cf = np.where(np.isfinite(cfund), cfund, 0.0)
cw, _ = compute_weights(cclose, cqv, cdays, CFG)
mom = np.sum(cw[:-1] * (cR - cf)[1:], axis=1)
mom_by_day = {int(cdays[1:][i]): mom[i] for i in range(len(mom))}
common = sorted(set(alld.tolist()) & set(mom_by_day))
va = np.array([vrp[np.where(alld == d)[0][0]] for d in common])
ma = np.array([mom_by_day[d] for d in common])
m = np.isfinite(va) & np.isfinite(ma)
corr = float(np.corrcoef(va[m], ma[m])[0, 1])
sv, sm = np.std(va[m]), np.std(ma[m])
comb = ((1 / sv) * va[m] + (1 / sm) * ma[m]) / (1 / sv + 1 / sm)
svr = sharpe_t(torch.tensor(va[m], device=DEV, dtype=torch.float64))
smr = sharpe_t(torch.tensor(ma[m], device=DEV, dtype=torch.float64))
scr = sharpe_t(torch.tensor(comb, device=DEV, dtype=torch.float64))
print(f"\n===== DIVERSIFIER CHECK — VRP vs crypto-momentum (overlap {m.sum()} days) =====")
print(f"VRP Sharpe {svr:+.2f} momentum Sharpe {smr:+.2f} CORRELATION {corr:+.2f} combined {scr:+.2f}")
opt = math.sqrt(max(svr, 0)**2 + max(smr, 0)**2)
print(f"optimal-weight combined (uncorrelated approx) ~ {opt:.2f} (vs momentum alone {smr:+.2f})")
print("\nVERDICT: VRP Sharpe>=~0.4 + low |corr| + combined>momentum-alone = genuine diversifier (mind the short-vol tail).")
if __name__ == "__main__":
main()

118
scripts/surfer/vrp_tail.py Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Tail-managed crypto VRP — does 'don't sell vol into a storm' fix the 21σ skew AND the 2025-26 decay?
Pre-registered tail rules (point-in-time, gates use DVOL/return info up to t-1 only):
baseline — full short-vol always (the +1.03 / -21σ version)
dvol_rising — flat when implied vol is rising over 5d (don't sell into a spike)
dvol_level — full size when DVOL < trailing-60d median (calm), half when elevated
combo — dvol_level AND not rising
loss_cap — idealized: truncate each day's loss at -3σ (stop/protection reference)
vol_target — scale exposure to constant risk (inverse trailing P&L vol)
Judge: Sharpe, per-year (esp 2025-26), skew, worst-day. Best -> diversifier check vs momentum.
"""
import json
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pit_sweep # noqa: E402
from surfer_poc import compute_weights, CFG # noqa: E402
from signal_sweep import sharpe_t # noqa: E402
from vrp_diversifier import dvol # noqa: E402 (cached DVOL fetch)
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
def per_ccy(px, dv):
days = np.array(sorted(set(px) & set(dv)))
iv = np.array([dv[int(d)] / 100.0 for d in days])
r = np.full(len(days), np.nan)
for i in range(1, len(days)):
if px[int(days[i])] > 0 and px[int(days[i - 1])] > 0:
r[i] = math.log(px[int(days[i])] / px[int(days[i - 1])])
raw = np.full(len(days), np.nan)
raw[1:] = (iv[:-1] ** 2) / 365.0 - r[1:] ** 2 # short-var daily P&L (iv lagged)
return days, iv, r, raw
def gates(iv, raw):
n = len(iv)
rising = np.zeros(n) # 1 if NOT rising (ok to sell)
for i in range(1, n):
rising[i] = 0.0 if (i >= 6 and iv[i - 1] > iv[i - 6]) else 1.0
level = np.zeros(n) # 1 calm, 0.5 elevated (vs trailing median)
for i in range(1, n):
win = iv[max(0, i - 61):i - 1]
level[i] = 1.0 if (len(win) > 10 and iv[i - 1] < np.median(win)) else 0.5
vt = np.ones(n) # vol-target on raw P&L
for i in range(31, n):
s = np.nanstd(raw[i - 30:i])
vt[i] = min(1.0, (np.nanstd(raw[~np.isnan(raw)]) / s)) if s > 0 else 1.0
return {"baseline": np.ones(n), "dvol_rising": rising, "dvol_level": level,
"combo": rising * level, "vol_target": vt}
def main():
syms, cdays, cclose, cqv, cfund = pit_sweep.load()
idx = {s: j for j, s in enumerate(syms)}
def closes(sym):
j = idx[sym]; c = cclose[:, j]
return {int(cdays[t]): c[t] for t in range(len(cdays)) if np.isfinite(c[t])}
series = {c: per_ccy(closes(c + "USDT"), dvol(c)) for c in ["BTC", "ETH"]}
# build per-rule pooled daily P&L across BTC+ETH (aligned by day)
def rule_pnl(rule, cap=False):
bag = {}
for c, (days, iv, r, raw) in series.items():
g = gates(iv, raw)[rule]
p = g * raw
if cap:
sd = np.nanstd(raw)
p = np.maximum(p, -3 * sd)
for i in range(len(days)):
bag.setdefault(int(days[i]), []).append(p[i])
dd = np.array(sorted(bag))
return dd, np.array([np.nanmean(bag[int(d)]) for d in dd])
rules = ["baseline", "dvol_rising", "dvol_level", "combo", "vol_target"]
print("\n===== TAIL-MANAGED CRYPTO VRP =====")
print(f"{'rule':>14} {'Sharpe':>7} {'skew':>6} {'worstσ':>7} | per-year 2021..26")
results = {}
for rule in rules + ["loss_cap"]:
dd, p = rule_pnl("baseline" if rule == "loss_cap" else rule, cap=(rule == "loss_cap"))
pf = p[np.isfinite(p)]
sr = sharpe_t(torch.tensor(p, device=DEV, dtype=torch.float64))
sk = float(((pf - pf.mean()) ** 3).mean() / (pf.std() ** 3 + 1e-12))
worst = float(np.nanmin(p) / (np.nanstd(p) + 1e-12))
yr = (1970 + dd / 365.25).astype(int)
py = " ".join(f"{y}:{sharpe_t(torch.tensor(p[yr==y],device=DEV,dtype=torch.float64)):+.2f}" for y in range(2021, 2027) if (yr == y).sum() > 30)
print(f"{rule:>14} {sr:>+7.2f} {sk:>+6.2f} {worst:>+7.1f} | {py}")
results[rule] = (dd, p, sr, sk)
# diversifier check for combo (the principled don't-sell-into-storm rule)
dd, p, _, _ = results["combo"]
cR = np.zeros_like(cclose); cR[1:] = np.log(cclose)[1:] - np.log(cclose)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0)
cf = np.where(np.isfinite(cfund), cfund, 0.0)
cw, _ = compute_weights(cclose, cqv, cdays, CFG)
mom = np.sum(cw[:-1] * (cR - cf)[1:], axis=1)
mbd = {int(cdays[1:][i]): mom[i] for i in range(len(mom))}
common = sorted(set(dd.tolist()) & set(mbd))
va = np.array([p[np.where(dd == d)[0][0]] for d in common]); ma = np.array([mbd[d] for d in common])
m = np.isfinite(va) & np.isfinite(ma)
corr = float(np.corrcoef(va[m], ma[m])[0, 1])
sv, sm = np.std(va[m]), np.std(ma[m])
comb = ((1 / sv) * va[m] + (1 / sm) * ma[m]) / (1 / sv + 1 / sm)
cyr = (1970 + np.array(common) / 365.25).astype(int)[m]
print(f"\n===== DIVERSIFIER CHECK (combo tail-managed VRP vs momentum, {m.sum()}d) =====")
print(f"VRP {sharpe_t(torch.tensor(va[m],device=DEV,dtype=torch.float64)):+.2f} momentum {sharpe_t(torch.tensor(ma[m],device=DEV,dtype=torch.float64)):+.2f} CORR {corr:+.2f} combined {sharpe_t(torch.tensor(comb,device=DEV,dtype=torch.float64)):+.2f}")
print("combined per-year: " + " ".join(f"{y}:{sharpe_t(torch.tensor(comb[cyr==y],device=DEV,dtype=torch.float64)):+.2f}" for y in range(2021, 2027) if (cyr == y).sum() > 30))
print("\nVERDICT: tail rule restores 2025-26 to positive + skew less extreme + combined>momentum = deployable diversifier.")
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More