Skip to content

View original

CrossSectorTsmomVolTargetedLSBasketDaily

Hypotheses

BINANCE Broad-Basket Cross-Sector Time-Series Momentum, Equal-Risk Vol-Targeted, Long-Short (Moskowitz-Ooi-Pedersen 2012, Daily)

Hypotheses

Identical per-asset time-series momentum (TSMOM) mechanism as the prior 3-major version, but run across a deliberately LOW-CORRELATION cross-sector basket of 12 liquid Binance USD-M perps to restore the diversification that IS the alpha source for this factor. Each asset is evaluated INDEPENDENTLY against its own trailing return: long if trailing N-day return > +threshold, short if < -threshold, flat inside the deadband. Crucially, each leg is VOL-TARGETED to an equal risk budget (inverse-volatility sizing) so every independent trend signal contributes the same risk — this is what lets the averaging de-noise the factor and lift effective breadth from ~1 (the prior failure) toward ~6-8. Basket spans 5 sectors to maximize signal independence: majors (BTCUSDT, ETHUSDT), L1s (SOLUSDT, AVAXUSDT, ADAUSDT, NEARUSDT), DeFi (LINKUSDT, UNIUSDT, AAVEUSDT), meme (DOGEUSDT), payments/value-transfer (XRPUSDT, LTCUSDT). 0-12 positions held simultaneously in any long/short mix. This remains the 4th orthogonal factor (TSMOM is empirically uncorrelated with the in-pipeline cross-sectional momentum, reversal, and low-vol factors) and fills the same under-represented buckets: MULTI scope, market-neutral direction (flat in non-trending regimes, balanced long/short reduces long bias), and factor-mechanism diversification.

Hypotheses

Iteration 7 fixes the reported Layer-2 failure at its true root. The TypeError originates in the INHERITED FactoryStrategy._bar_ts, which is a @staticmethod: _StrategyProxy rebuilds the instance via types.MethodType(val.__func__ if hasattr(val,'__func__') else val, wrapper), and a staticmethod fetched off an instance is a bare function with no __func__, so it gets bound as an instance method and any self._bar_ts(bar) call passes 2 args into a 1-arg function. Iteration 6's inline-timestamp workaround only avoided calling it, leaving the class one base-class code path away from the same crash (base _maybe_process_primary calls self._bar_ts(bar)). The fix shadows the base staticmethod with a normal INSTANCE method _bar_ts(self, bar), which has __func__ and therefore re-binds correctly in the proxy while behaving identically in the real engine. Verified empirically: on unmodified previous_code the proxy-bound _bar_ts raises the exact reported TypeError; with the override it returns the timestamp, all 6 synthetic scenarios pass, Layer 1 static analysis stays clean (no new imports), and no frozen-signal warnings fire (calculate_signal still returns the continuous trailing return every bar). No other logic, sizing, or threshold was touched, so earlier passing layers cannot regress. Venue is BINANCE futures because the basket takes short legs and consumes leverage>1, which the code wires into sizing (gross = equity * leverage * vol_scalar, hard-bounded by scalar_cap<=1).

Hypotheses

max_iterations_after_review

Implementation

Time-series momentum (TSMOM) across a 12-name, 5-sector Binance USD-M perp basket on daily bars. Each asset is judged independently against its own trailing 60-day return: long above +5%, short below -5%, flat inside the deadband. Active legs are inverse-volatility weighted (equal risk contribution), capped at 20% each and renormalized, then scaled by a bounded vol-target overlay (scalar clamped to [0, 1]) so gross exposure never exceeds equity x leverage. Rebalanced every 5 daily bars, with same-side resizes suppressed unless the delta exceeds 25% of target notional to control fee drag.

Verification Results

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['assets', 'lookback', 'vol_floor', 'max_weight', 'scalar_cap', 'vol_target', 'resize_frac', 'min_notional', 'vol_lookback', 'qty_precision', 'rebalance_days', 'entry_threshold', 'default_qty_precision'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: CrossSectorTsmomVolTargetedLSBasketDaily._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735693260000) - steady_downtrend: TypeError: CrossSectorTsmomVolTargetedLSBasketDaily._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735693260000) - flat_ranging: TypeError: CrossSectorTsmomVolTargetedLSBasketDaily._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735693260000) - volatility_spike: TypeError: CrossSectorTsmomVolTargetedLSBasketDaily._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735693260000) - zero_volume: TypeError: CrossSectorTsmomVolTargetedLSBasketDaily._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735693260000) - price_gap: TypeError: CrossSectorTsmomVolTargetedLSBasketDaily._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735693260000)

Backtest Review

The trades match the hypothesized mechanism: 647 closed trades, 306 long / 341 short (balanced, deadband-gated per-asset TSMOM), avg holding 33d 17h, trade durations are exact multiples of the 5-day rebalance cadence. This is a genuine independent-signal TSMOM basket, not a mislabeled directional book.

Backtest Review

entry_signaled=0 is expected and correct — the base single-position hooks are deliberately inert and the basket is rebalanced directly.

Backtest Review

Fee drag is not the bottleneck: commission_pct_of_gross = 1.87%; rebalance_days=5 + resize_frac=0.25 deadband are doing their job.

Backtest Review

BLOCKER — the risk construction that IS the hypothesis is inoperative. The code asserts gross = equity * leverage(1.0) * vol_scalar with vol_scalar <= scalar_cap = 1.0, so total notional must never exceed 1.0x equity. Observed: exposure_pct = 1046.6%, avg_position_pct = 65.6% per leg against a max_weight cap of 0.20, the BTC leg in trade #1 carries a 'leverage' of 0.42, and gross notional opened on 2020-11-20 was $465,862 on ~$100k equity (and $451,027 on 2020-11-05). The book is running 2-5x its own stated cap.

Backtest Review

ROOT CAUSE (identified): the per-asset weight cap is a no-op. `weights = {a: min(raw/tot, max_weight)}` followed by `weights = {a: w/wsum}` re-inflates every capped weight back above max_weight whenever the cap binds (4 active names -> raw 0.25 each -> capped to 0.20 -> wsum 0.80 -> renormalized back to 0.25).

Backtest Review

SUSPECTED, NOT CONFIRMED: positions appear to stack rather than net — 2354 fills vs 647 closed positions, per-leg notional far above target, and the side-flip branch submits a new entry in the same event as the exit before it settles. `_rebalance_asset()` already sums a LIST of open positions per instrument, so the netting assumption may be violated. Must be instrumented, not assumed.

Backtest Review

Downstream consequence: annualized_volatility = 149.8% against a vol_target of 0.60 — the vol overlay demonstrably does not bind. max_drawdown 93.9% (CI upper 107%, i.e. the path crosses zero), single-day returns of -366%, +276%, -131%, -110%, return_kurtosis 395, skew 11.5, tail_ratio 0.10. Every risk metric describes a different, oversized strategy.

Backtest Review

The return series is internally inconsistent and unusable as an optimization objective: daily_returns is 0.0 for essentially all of 2021-09 through 2026-07 while the detail file shows trades closing throughout (15 in 2026-04, 12 in 2026-07) and the equity curve moving ($105.7k on 2023-04-04, trough $39.8k on 2025-10-14, $91.4k final). cagr = -100.0 coexists with total_return = -8.62% and liquidated = false; max_drawdown 93.9% does not reconcile with a 62% peak-to-trough on the sampled curve.

Backtest Review

end_unrealized_pct = 509.85 — the headline total_return is dominated by open, un-exited positions on the last bar, not a realized track record.

Backtest Review

Taken at face value it loses: benchmark_meaningful=false, so on absolutes — Sharpe -0.299, Sortino -0.412, profit_factor 0.710, win_rate 35.2%, expectancy -$801/trade, information_ratio -0.279, omega 0.810. Losing on both sides (long win 31.4%, short win 38.7%). But with gross at 2-5x intended and the vol overlay inoperative, this P&L is not a fair test of the TSMOM premise.

Backtest Review

10 months of usable history silently discarded: first trade is 2020-10-16 despite start_time 2019-12-31. The cross-leg alignment barrier blocks the entire basket until the last-listing name (NEAR/AAVE/UNI, late 2020) satisfies lookback(60)+2.

Analysis

Genuine, robust edge signals: walk-forward is_overfitted=FALSE (avg IS 1.10 -> avg OOS 0.60), HOLDOUT PASSED (0.46, ratio 0.765), sensitivity PASSED with 0 cliffs on a flat smooth surface, sharpe_ci_low 0.2879 positive

Analysis

Academically grounded mechanism (Moskowitz-Ooi-Pedersen 2012 time-series momentum), one of the most replicated factors in finance

Analysis

Clean single-timeframe multi-instrument design that avoided the stale-leg failure mode; market-neutral-capable (151 shorts vs 152 longs) reducing long bias

Analysis

Optimization meaningfully improved risk: max_DD 35.8% -> 12.2%, annualized_vol 68% -> 17%, while keeping a positive alpha (0.11) and low beta (0.04)

Analysis

Fails the decisive multiple-testing gate: deflated_sharpe 0.8135 < 0.95, is_significant=FALSE; optimized Sharpe 0.81 only marginally above the expected-max luck bar 0.60 over 225 trials

Analysis

PBO 0.5304 > 0.5 (selection leans overfit); one walk-forward OOS window negative (-0.39)

Analysis

Absolute optimized Sharpe (0.81) is below any promotion bar — structurally capped by running on only 3 highly-correlated crypto majors, which negates the cross-instrument diversification that powers the TSMOM factor

Analysis

Tail-concentrated returns (return_kurtosis 272, tail_ratio 0.20) — Sharpe rests on a handful of explosive days, a fragility consistent with the modest deflated Sharpe

Analysis

Parameter surface is flat and fully explored — no untapped higher-Sharpe region for tuning to reach significance

Analysis

The backtest is not optimizable because the account is LIQUIDATED — total_return -100%, max_drawdown 100%, exposure_pct 1046%, and physically impossible daily returns (-157.75% on 2021-04-09, +163% on 2021-06-23, -140% on 2021-08-07). This is a position-sizing/leverage bug, NOT a mechanism problem (the vol-targeted TSMOM basket family has a promoted sibling). Your intended math (gross = equity * leverage[1.0] * vol_scalar[<=scalar_cap=1.0], with inverse-vol weights renormalized to sum to 1) should cap total gross notional across all 12 legs at ~equity (~100% exposure), but realized exposure is ~1046% — roughly 10x. Root cause to fix: positions ACCUMULATE across rebalances because `_rebalance_asset` reads `cur_qty` from `cache.positions_open()` while prior market orders are still PENDING (fills land on the next bar). So (a) side-flip close→reopen submits a new entry before the close fills, and (b) same-side resize computes delta against a stale cur_qty — both stack notional instead of netting to target. FIXES: (1) After computing all 12 target notionals each rebalance, assert/enforce that sum(|target_notional|) <= equity * leverage before submitting, and log realized gross to verify it stays ~100%. (2) Account for pending/unfilled orders (or use a single netting reduce/close before reopen, or resize via a target-position order) so cur_qty reflects in-flight orders and positions cannot compound across rebalances. (3) Re-run the initial backtest and confirm exposure_pct stays near 100% (not 1000%+) and the account does not liquidate. Only then can the true edge of the 12-name cross-sector TSMOM basket be judged. Do NOT proceed to optimization until the account survives the full sample.

Outcome Summary

The mechanism itself was implemented faithfully — 647 trades in a balanced long/short mix, durations landing on exact multiples of the 5-day rebalance cadence, fee drag at only 1.87% of gross — and the reviewer confirmed this was a genuine independent-signal TSMOM basket rather than a mislabeled directional book. What it was not was the strategy the hypothesis described: with gross running 2-5x its own cap and the vol overlay never binding, the negative Sharpe was explicitly judged not a fair test of the TSMOM premise. The reviewer also flagged, but deliberately declined to assert, a second suspicion (2,354 fills against 647 closed positions suggests positions may stack rather than net) and noted the return series was internally inconsistent — daily_returns sitting at 0.0 from 2021-09 onward while trades closed and equity moved, with cagr -100.0 coexisting with total_return -8.62% and liquidated=false. An earlier 3-major version of this same mechanism had reached the analyst and drawn a revise_hypothesis verdict: robust on walk-forward (is_overfitted=false, holdout passed, zero sensitivity cliffs) but failing the multiple-testing gate at deflated_sharpe 0.8135 against a 0.95 bar, with the analyst attributing the structural Sharpe ceiling to running on only three highly-correlated majors — which is precisely the diagnosis this 12-name basket was built to answer, and never got a clean run to test.

Outcome Summary

Capping weights and then renormalizing them silently undoes the cap; when a hypothesis's alpha source IS the risk construction, verify that exposure_pct and realized volatility actually respect their stated bounds before treating any P&L as a test of the premise.

Outcome Summary

The backtest-review gate returned an iterate verdict on a blocking risk-construction bug it had already root-caused — the per-asset max_weight cap is a no-op, because `min(raw/tot, max_weight)` is immediately followed by a renormalization that re-inflates every capped weight back above the cap — and the run then exhausted its iteration budget (abandon_reason: max_iterations_after_review at iteration 7) before the fix landed.

Outcome Summary

Run per-asset time-series momentum (Moskowitz-Ooi-Pedersen 2012) independently across a 12-name, 5-sector Binance USD-M perp basket — long if the trailing 60-day return exceeded +5%, short if below -5%, flat inside the deadband — with each leg inverse-vol sized to an equal risk budget so averaging across low-correlation names would restore the effective breadth (~6-8) that the prior 3-major version lacked.

Outcome Summary

The basket backtest produced 647 closed trades (306 long / 341 short, avg hold 33d 17h) with Sharpe -0.299, profit_factor 0.710, expectancy -$801/trade and total_return -8.62%. But the risk construction was inoperative: exposure_pct hit 1046.6% against a stated 1.0x cap, annualized_volatility was 149.8% against a 0.60 vol_target, max_drawdown 93.9% (CI upper 107%), and end_unrealized_pct of 509.85 meant the headline return was dominated by open positions on the final bar.

Iteration History

BinanceTop3TimeSeriesMomentumLongShort
Strategy report

Backtest and paper results are hypothetical. Trading involves risk of loss.