M9d = M8 + SKIP SHORT if excess_gap_spy < -3% (bounce filter). Reported Sharpe 8.84, Train→Test degradation 0.6%.
Аудит обнаружил 3 критических, 4 высоких, 5 средних и 3 низких проблемы.
| # | Finding | Severity | Persona | Impact on Sharpe |
|---|---|---|---|---|
| 1 | Entry = close_930 vs open_930 | CRITICAL | Devil / Trader | -1.0 to -2.5 |
| 2 | Non-monotonic grading (U-shape) | CRITICAL | Quant | Overfit flag |
| 3 | April exclusion = data snooping | CRITICAL | Quant / Devil | -0.5 to -1.0 |
| 4 | Sharpe 8.84 physically implausible | CRITICAL | Quant | True ~2-4 |
| 5 | No slippage/spread in build_m9d.py | HIGH | Trader | -0.8 to -1.5 |
| 6 | 37 trades/day market impact | HIGH | Trader | -0.3 to -0.8 |
| 7 | Survivorship bias in ADV filter | HIGH | Devil | -0.2 to -0.5 |
| 8 | gap_pm vs gap_pct direction mismatch | HIGH | Devil | Unknown |
| 9 | Monday SHORT skip = snooping | MEDIUM | Quant | -0.1 to -0.3 |
| 10 | Hardcoded beta table (not rolling) | MEDIUM | Quant / Devil | -0.1 to -0.3 |
| 11 | Sharpe * 16 assumes 256 trading days | MEDIUM | Quant | Inflated ~3% |
| 12 | PM volume may include FINRA prints | MEDIUM | Trader | Unknown |
| 13 | Compound 3x cap amplifies variance | MEDIUM | Quant | Inflated compound |
| 14 | OOS period too short (3.5 months) | LOW | Quant | Need more data |
| 15 | Consec gap DN filter = overfit risk | LOW | Devil | -0.05 |
Функция new_grade() в build_m9d.py определяет грейды так:
def new_grade(s):
if 5.5 <= s <= 11.5: return 'A+' # medium scores = BEST
elif 3.0 <= s < 5.5 or s >= 24: return 'A' # low OR very high = second best
elif 11.5 <= s < 24: return 'B' # high scores = WORSE than A+!
elif 1.0 <= s < 3.0: return 'B+' # low = third best
else: return 'SKIP'
Проблема: Скор 6.0 -> A+ (best), скор 15.0 -> B (worse), скор 25.0 -> A (second). Это U-образная зависимость. В здоровой модели БОЛЬШЕ скор = ЛУЧШЕ грейд. Немонотонная зависимость = классический признак переобучения на шум.
Что это значит: Модель "нашла", что средние скоры лучше высоких. Но это скорее артефакт данных — высокие скоры часто приходятся на extreme дни (earnings, news), где конкуренция за ликвидность и crowded trades убивают edge. Вместо понимания ПРИЧИНЫ, модель просто режет интервал.
Тест: Возьмите только score >= 0 и монотонный грейдинг. Если Sharpe падает значительно — overfit confirmed.
keep &= u.month != 4 — полное исключение апреля.
Проблема: Если апрель был худшим месяцем в training set, его исключение = подгонка под прошлое. Это классический "worst period exclusion". Можно так же исключить любой плохой квартал и получить лучший Sharpe.
Реальный вопрос: Есть ли structural reason почему апрель плох? (Tax-loss selling deadline? Earnings season start?). Если да — документируйте ПРИЧИНУ. Если нет — это data snooping.
Impact: ~21 торговых дней в году × 37 trades/day = ~777 trades. Исключая худший месяц, вы убираете ~3% самых плохих данных и Sharpe прыгает вверх.
Sharpe Ratio 8.84 означает, что стратегия зарабатывает 8.84 стандартных отклонений в год. Для сравнения:
| Strategy | Sharpe (ann.) | Кто |
|---|---|---|
| Renaissance Medallion Fund | ~3.0-4.0 | Best hedge fund in history |
| HFT market making | ~4-8 | Citadel/Jump (nano-second execution) |
| Typical stat arb | ~1.5-2.5 | Институционалы |
| M9d заявленный | 8.84 | Одиночный retail |
Причины инфляции:
Реалистичная оценка: После корректировки на slippage (+5bps entry + 5bps exit = 10bps round-trip), market impact (еще 5bps на crowded names), и honest entry price → Sharpe ≈ 2-4. Это все еще отличная стратегия, но не "8.84".
keep &= ~((u.dow == 'Monday') & (u.direction == 'SHORT'))
Calendar-based rule без structural justification. Понедельники SHORT плохи потому что weekend risk premium? Тогда Friday close уже должен это учитывать. Или это артефакт конкретного sample period?
mult = min(eq / 100000, 3.0) # up to 3x leverage on compounding eq += p * mult
Compound $486K при стартовых $100K = +386%. Но cap 3.0 означает, что после $300K вы торгуете с 3x leverage. Это маскирует реальный drawdown risk. Линейный PnL ($172K) — более честная метрика.
Test (2025-01 to 2026-04) = ~15 месяцев, не 3.5. Это нормально. Но Train→Test "degradation 0.6%" подозрительно мало — обычно 10-30% деградация для honest OOS. Либо модель genuinely robust, либо OOS не полностью independent (например, ticker bias "past-only" но computed на всем TRAIN который частично overlaps).
В 01_data_builder.py:327:
bars_930.columns = ["open_930", "high_930", "low_930", "entry_930", "vol_930"] # ^^^^^^^^^^ # entry_930 = CLOSE of 9:30 bar (т.е. цена в 9:30:59) # open_930 = OPEN of 9:30 bar (т.е. цена в 9:30:00 = реальный MOO)
Есть fix_master_preopen.py который пытается исправить: m["entry_930"] = m["entry_open"]. Но:
Magnitude: В первую минуту после MOO средний move = 5-15bps. На 30K trades это $2K-$7K бесплатного P&L в бэктесте. Может объяснять 10-20% общего P&L.
Как проверить:
import pandas as pd
u = pd.read_parquet('moo955_v2/universe_scored_v4.parquet')
# Если entry_930 == close of 930 bar:
diff = (u['entry_930'] - u['open_930']).abs() / u['open_930'] * 100
print(f"Median |entry-open|: {diff.median():.4f}%")
print(f"Mean |entry-open|: {diff.mean():.4f}%")
# Если median > 0.02% → fix NOT applied, entry = close
Cost model в build_m9d.py:
BROKER_PS = 0.004 # broker per share ECN_PS = 0.003 # ECN per share FINRA_PS = 0.000166 # FINRA TAF SEC_RATE = 0.0000278 # SEC fee t['cost'] = t.shares * (BROKER_PS+ECN_PS+FINRA_PS)*2 + t['size']*SEC_RATE
Сравнение с config.py (используется в реалистичных бэктестах):
| Cost | build_m9d.py | config.py (realistic) |
|---|---|---|
| Slippage | 0 | 5 bps |
| Spread | 0 | 10 bps |
| Locate (short) | 0 | $0.01-0.03/sh |
| Broker+ECN+FINRA | $0.0072/sh | $0.0072/sh |
Missing cost per trade:
$1000 position × 15bps slippage+spread = $1.50 per trade. На 30K trades = $45,000 недоучтенных costs. Это 26% от заявленного PnL ($172K).
Для SHORT trades добавьте locate fee: ~$0.02/share × ~100 shares = $2/trade × 10K short trades = $20,000.
Итого недоучтено: ~$65K = 38% от PnL.
30,182 trades / 800 days ≈ 37.7 trades/day. При $1000 avg position size = $37,700 daily notional. Это мало для рынка, НО:
Фильтр pm_vol >= 10000 может пропускать тикеры с "ложной" PM ликвидностью из FINRA ADF. По вашему же исследованию, FINRA prints = exclude. В Datum 1-min bars нет ex_finr варианта. Это может добавлять 5-15% "phantom" PM volume тикеров.
Бэктест использует exit_955 = close of 9:55 bar. В реальности:
Самая опасная находка.
В коде есть ДВЕ версии entry price:
# Version 1 (01_data_builder.py:54): entry = bar_930.iloc[0]["close"] # = price at 9:30:59 # Version 2 (01_data_builder.py:327, vectorized path): bars_930.columns = [..., "entry_930", ...] # maps to "close" column
Обе версии используют CLOSE of 9:30 bar. fix_master_preopen.py существует для исправления, но:
Даже если fix applied для ret_955 расчета, в build_m9d.py есть второй lookahead:
u['gap_pm'] = (u.pm_close - u.prev_close) / u.prev_close * 100
Но direction в universe_scored_v4 был назначен по gap_pct из data builder:
# 01_data_builder.py:432-434 gap_pct = (entry_930 - prev_close) / prev_close * 100 # 01_data_builder.py:440 direction = "LONG" if gap_pct > 0 else "SHORT"
Если entry_930 = close_930, то direction определяется по цене 9:30:59, а не по pre-open gap! Stock может gap UP в PM, но close of 9:30 bar ниже prev_close → direction = SHORT. Это forward-looking direction assignment.
Universe фильтр: avg_vol_20d >= 2,000,000 + price >= $5 + mcap >= $300M
Проблема: Эти фильтры применяются к ТЕКУЩИМ данным. Но в 2023 году:
Направление bias: Survivorship bias ВСЕГДА завышает returns. Delisted stocks — это обычно ЛОСЕРЫ. Их исключение убирает худшие trades.
Estimated impact: ~5-10% universe affected. WR inflation ~0.5-1pp. Sharpe inflation ~0.2-0.5.
В build_m9d.py: gap_pm = (pm_close - prev_close) / prev_close
В data builder: gap_pct = (entry_930 - prev_close) / prev_close
Эти могут иметь РАЗНЫЙ ЗНАК! Пример:
| Scenario | prev_close | pm_close (9:25) | entry_930 (9:30) | gap_pm | gap_pct | direction |
|---|---|---|---|---|---|---|
| MOO sell-off | $100 | $101.50 | $99.80 | +1.50% | -0.20% | SHORT |
| MOO spike | $100 | $98.50 | $100.30 | -1.50% | +0.30% | LONG |
Когда gap_pm и direction disagreement → фильтры в build_m9d.py (которые используют gap_pm) работают на ДРУГИХ данных, чем scoring (который использует direction). Это тихий data leak.
Beta table в M9d:
sector_beta_spy = {
'Technology': 1.25, 'Communications': 1.05, ...
}
high_beta_names = ['NVDA','TSLA','AMD',...] # multiply by 1.4
low_beta_names = ['KO','PEP','WMT',...] # multiply by 0.6
Проблемы:
M9d содержит 8 фильтрующих правил:
10 правил, каждое "улучшает" Sharpe. Но каждое добавление = одна степень свободы overfit. С 10 правилами, шансы что ВСЕ genuinely predictive ≈ 0.5^10 = 0.1%.
Proper test: Permutation test — рандомизируйте даты/тикеры и применяйте те же 10 правил. Если Sharpe improvement ~similar → rules = noise.
u['prev_gap_pm'] = u.groupby('ticker')['gap_pm'].shift(1)
u['consec_gap_dn'] = (u.gap_pm < -1) & (u.prev_gap_pm < -1)
Технически OK (используется shift(1) = вчерашний gap). Но gap_pm рассчитан из pm_close текущего дня. Если gap_pm использует entry_930 → опять 1min lookahead.
5 реальных сценариев, показывающих где M9d ломается.
| Adjustment | Sharpe Impact | Cumulative |
|---|---|---|
| Reported M9d | 8.84 | |
| Add slippage + spread (15bps RT) | -1.5 to -2.5 | 6.3 - 7.3 |
| Fix entry price (if close→open) | -0.5 to -1.5 | 5.0 - 6.8 |
| Remove April exclusion | -0.3 to -0.8 | 4.5 - 6.5 |
| Add locate fees (SHORT) | -0.2 to -0.5 | 4.0 - 6.3 |
| Survivorship bias correction | -0.2 to -0.5 | 3.5 - 6.1 |
| Market impact on MOO fills | -0.1 to -0.3 | 3.2 - 6.0 |
| Direction mismatch (gap_pm vs gap_pct) | -0.2 to -0.5 | 2.7 - 5.8 |
| Estimated Real Sharpe | 3.0 - 5.0 |
YES, but with corrections.
Even at Sharpe 3.0-5.0 (after all haircuts), this is a strong strategy. The core edge — gap continuation/reversal in the first 25 minutes — is real and backed by market microstructure theory (opening auction price discovery + mean reversion).
The M9d rule (skip SHORT if excess_gap < -3%) is intuitively sound — capitulation bounces are real. But the threshold, beta table, and surrounding calendar rules need denoising.
Bottom line: Fix the entry price bug, add realistic costs, remove calendar snooping → you likely have a Sharpe 3-4 strategy generating $50K-$100K/year on $100K capital. That's excellent.
| Component | Confidence | Reason |
|---|---|---|
| Core edge (gap + 25min mean-reversion) | HIGH 85% | Microstructure theory + massive N |
| Scoring v4 OOS methodology | HIGH 80% | Honest train/test, FDR, past-only bias |
| M9d excess_gap rule | MEDIUM 60% | Sound logic, but hardcoded beta + threshold |
| Calendar rules (April, Monday) | LOW 30% | No structural justification documented |
| Reported Sharpe 8.84 | LOW 10% | Physically implausible without HFT infra |
| Real profitability after corrections | HIGH 75% | Even with 50% haircut, still profitable |