Strategies
Hong Kong Large-Cap Cross-Sectional Monthly REVERSAL, Long-Only Equal-Weight Basket (HKEQ — buy the 6 biggest RELATIVE losers of the last 21 sessions out of a fixed 24-name HSI-blue-chip universe, hold 21 sessions, roll; a brand-new venue and price estate with ZERO prior experiments, 3-parameter)
Outcome: Abandoned
HkBlueChipMonthlyCrossSectionalReversalBasketHkeq
Outcome Summary
Hypothesis
Negative expectancy on a fully reliable 445-trade / 8.6-year sample, with the premise measurably absent. PF 0.933 (<1), total return -12.02%, Sharpe -0.098, and per-trade return 0.109% versus the ~0.30% HKEQ round trip the hypothesis was designed around. Those costs were never actually charged (HKEQ Equity instruments bypass `_apply_fees`), so a manual ~2%/yr haircut on ~4 rotations/month puts the true result near -2.5 to -3%/yr. The hypothesized Asia-ex-Japan 1-month contrarian premium is simply not present at the 21-session horizon in HSI blue chips — confirmed independently by the developer's own offline replication (loser basket 0.18%/month gross vs 0.35%/month for the equal-weight universe). Optimization cannot lift an edge that is below cost and gross-negative in sign; the three tunables only reshuffle which losers get bought. Not `revise_hypothesis`: the reversal premise itself is falsified on the exact universe it was specified for, and the venue's stamp duty leaves no room for a marginal variant.
Implementation
Long-only, always-invested cross-sectional MONTHLY REVERSAL basket on a fixed 24-name HKEX blue-chip universe (HKEQ, 1-DAY bars, CASH account, leverage 1). Every session each name's trailing 21-session return is computed and the universe MEDIAN subtracted, giving a purely RELATIVE score with no market-timing component; calculate_signal returns the primary name's (0700) demeaned score every bar. On each calendar rebalance boundary (month grid = ~21 HK sessions) the 6 most negative relative performers become the target basket: names dropping out are closed, new names are bought equal-weight (95% gross / 6 names) sized against NAV and hard-capped by the cash the CASH account actually holds, whole shares. Names that stay in the bottom-6 are NOT traded, so the ~0.30% HK round trip (0.11%/side stamp duty + ~4bp half spread) is paid only on the ~70% of the book that actually rotates, once a month. A target that could not be funded on the boundary session (sale proceeds not yet credited — HK is T+2) is retried on the following sessions until the next boundary.
Verification Results
Not a developer fix. Factory-level: narrow the `Equity` early-return in `_apply_fees` to venues whose VenueConfig fees are actually 0.0 (USEQ), and rebuild non-zero-fee Equity instruments with maker/taker set so MakerTakerFeeModel charges them; and raise `ROUND_TRIP_COST_PCT['HKEQ']` from 0.10 to ~0.30 (0.22% statutory + ~0.08% spread). Until then the analyst must haircut HKEQ/CNEQ results by hand.
Venue cost model gap (infrastructure, NOT fixable in this strategy file): HKEQ instruments are NT `Equity`, and `src/backtesting/runner.py::_apply_fees` short-circuits on `isinstance(instrument, Equity)` before it ever reads `venue_config.maker_fee/taker_fee`. That early return was written for USEQ (genuinely commission-free) but now also swallows HKEQ's declared 0.11%/side and CNEQ's 0.05%/side. Sandbox evidence: total_commission=0.0 and commission_pct_of_gross=0.0 on 336 closed positions at HKEQ. The strategy's fee arithmetic (0.95%/month capture vs 0.30% round trip) therefore cannot be validated by this backtest — the cost side is zero.
Either drop the `signal <= 0.0` condition (membership in `self._targets` is already the entry decision, and `_deploy` has already applied the ranking) or move the cash debit for the primary so it only happens once the entry is actually going to be submitted — e.g. set `_primary_enter`/`_primary_qty` and debit `cash_left` inside the same guarded branch, or apply the `<= 0` filter when building `missing` so the primary is excluded from the loop rather than reserved-then-vetoed.
The PRIMARY leg (0700) is gated differently from the other 23 legs. `_deploy()` decides the basket for all names identically and debits `cash_left` for the primary at line 224, but the primary's order is only actually submitted if `should_enter()` also sees `signal <= 0.0` (line 269). On a rebalance boundary the primary is by construction in the bottom-6 of 24 so its demeaned return is strictly negative and the gate is a no-op. On a RETRY session, however (a target that could not be funded on the boundary day is re-attempted every session until the next boundary), `rel` has been recomputed on a fresh 21-session window and the primary's demeaned score can have flipped positive while it is still in `self._targets`. The primary is then silently skipped, yet `cash_left` has already been reduced by its full `per_name` cost inside the same `_deploy()` call, so a later name in the loop can be starved of cash that was never spent. The 23 extra legs have no such veto.
No change required. If exact 21-session windows matter for parity with an offline replication, record the primary's close in `on_bar`-independent state (e.g. also record the primary inside a bar-agnostic hook) so its history has the same gap structure as the extras.
The cross-leg alignment barrier in the base template (`_maybe_process_primary` waits for all 23 same-timeframe extra legs to carry a bar at the primary's timestamp) means a single missing/halted session on ANY one of the 24 names skips the entire session for the whole strategy. Because the primary's close is recorded in `calculate_signal` (which only runs when the barrier passes) while extras are recorded in `on_extra_bar` (always), the primary's `_hist` develops gaps the extras do not have, so `ref_ts = prim_ts[-1 - lookback]` can be 22+ calendar sessions back rather than exactly 21. Impact is bounded and benign here (the `_close_at` bisect is timestamp-keyed, so all 24 legs still measure the SAME window, and `_tol_ns` = 10 days covers Lunar New Year), and a skipped boundary session merely defers the rebalance to the first processed session of the new month. Flagged so it is not mistaken for a lookback bug when comparing against an offline replication that uses an ungapped session index.
Backtest Review
- Sharpe
- -0.10
- Total return
- -12.02%
- Max drawdown
- 32.34%
- Trades
- 445
- Win rate
- 50.6%
- Profit factor
- 0.93
Genuinely novel venue/price estate (HKEQ, zero prior experiments) with clean 2018-2026 daily history across four distinct macro regimes
Large, reliable sample: 445 single-name trades over 8.6 years, ~104 monthly rebalances — not the low-trade-count trap that kills ETF rotations
Mechanism implemented as described: long-only, always-invested, 6-name demeaned-loser basket, turnover suppressed on names that stay in the bottom-k (avg holding 40d, no shorts)
Negative expectancy on a reliable sample: PF 0.933, total_return -12.02%, CAGR -1.52%, Sharpe -0.098, expectancy -$27.02/trade
avg_trade_return_pct 0.109% is ~1/3 of the 0.30% HKEQ round-trip floor — and the loss is BEFORE costs
Costs never charged: total_commission 0.0, commission_pct_of_gross 0.0, impact_folded false — the ~0.22% statutory round trip + spread (~2%/yr of NAV) is absent from the reported PnL
Negative in 6 of 9 calendar years; loses worst in the CALM vol tercile (-15.1%), so the failure is the base case, not a stress artifact
Max drawdown 32.3% with 2833 days underwater
QA's edge concern is borne out: the developer's own offline replication measured the demeaned-loser basket at 0.18%/month gross vs 0.35%/month for the equal-weight universe — negative relative alpha before any cost, which the full backtest reproduces
Outcome Summary
Before committing a multi-instrument basket to the pipeline, confirm the cross-sectional premium exists gross-of-cost on the exact universe — the developer's own offline replication already showed the loser basket at 0.18%/month versus 0.35%/month for the equal-weight universe, i.e. negative relative alpha that no parameter search could repair — and check that the venue's fees are actually charged by the engine before reading per-trade returns.
The analyst's backtest-review gate returned `abandon` before optimization: negative expectancy on a reliable 445-trade sample with the hypothesized edge measurably absent, losing in 6 of 9 calendar years and worst in the calm vol tercile (-15.1%). Optimization, analysis, risk review and promotion stages were never reached.
A long-only HKEQ basket that every 21 sessions bought the 6 worst relative performers out of a fixed 24-name HSI blue-chip universe (returns demeaned against the universe median) and held them a month, betting on a 1-month cross-sectional reversal premium in Hong Kong large caps.
Over 2018-01-02 to 2026-09-10 (2,058 data days, 2,146 bars) it made 445 long trades with a 50.6% win rate but lost money: total return -12.02%, CAGR -1.52%, Sharpe -0.098, profit factor 0.933, expectancy -$27.02/trade, max drawdown 32.3% over 2,833 days underwater. Avg trade return was 0.109% of notional, and commissions were recorded as 0.0 (total_commission 0.0, commission_pct_of_gross 0.0, impact_folded false), so even that figure is before the ~0.30% HKEQ round trip.
Backtest and paper results are hypothetical. Trading involves risk of loss.