Live и walk-forward

Главное правило v42: offline replay идет тем же путем, что live.

Если replay использует другой press, другой MTF, другую session reset policy или другой director, результат нельзя напрямую сравнивать с live.

Live entrypoint

Текущий live runner:

store/market/tools/whaler_s300_stage_live.py

Он по умолчанию использует текущую закрытую Whaler v42 personality и соответствующий labels-пакет. Конкретные bake-артефакты и рецепт сборки относятся к закрытой ИС проекта.

Live пишет run-дир вида:

store/market/runs/live/s300_stage_BTCUSDT_YYYYMMDD_HHMMSS/

Внутри появляются рабочие артефакты:

frames.mtf.s300.jsonl
tape.mtf.s300.raw8.vsb
mtf.s300.html

frames.mtf.s1800.jsonl
tape.mtf.s1800.raw8.vsb
mtf.s1800.html

director.json
director.tsv
director.html

Offline live-equivalent replay

Текущий offline runner:

store/market/tools/whaler_live_replay_walkforward.py

Он делает:

historical frames
  -> closed MTF s300/s1800
  -> d8_agent_cli v42
  -> phase director
  -> optional catalog filter
  -> optional inventory journal

Это не старый research replay. Это parity runner: он повторяет live-связку на истории.

Откуда взялся s1800

В диагностическом all-history отчете Memory 432:

store/market/runs/v42_reset_frame432_BTCUSDT_all_history_2026-01-01_2026-06-13/tf_signal_compare_432.html

сравнивались фазовые слои:

s300
s900
s1800
s3600

на моментах sparse s300-сигналов. Сводка отчета:

signals=170
s300_frames=47232
s900_frames=14400
s1800_frames=7872
s3600_frames=3600
s300_long_at_signals=86
s300_short_at_signals=84
s900_long_at_signals=90
s900_short_at_signals=80
s1800_long_at_signals=83
s1800_short_at_signals=87
s3600_long_at_signals=81
s3600_short_at_signals=89

Визуально и по phase-sim sweep лучшим практическим слоем стал s1800. Например phase_s1800_lb224_th0.json на том же all-history прогоне:

frames=43200
phase_frames=7200
signals=150
trades=150
return=+25.63%
win=51.33%
profit_factor=1.45
max_drawdown=13.74%

Для сравнения, s900 с тем же lb224/th0 дал отрицательный результат:

return=-11.02%
win=38.67%
profit_factor=0.83
max_drawdown=29.81%

А s3600 был положительным, но хуже s1800:

return=+10.98%
win=50.0%
profit_factor=1.19
max_drawdown=19.95%

Вывод: s1800 не является отдельным swarm в текущем live, но его роль как HTF-фазы подтверждена экспериментом.

Почему закрытые bucket важны

В live нельзя знать будущее незакрытого 5-минутного окна.

Поэтому offline тоже не использует неполный bucket так, будто он был закрыт.

Правило:

Decima получает только закрытый s300 bucket
директор получает только закрытый s1800 bucket

Если это нарушить, walk-forward станет оптимистичным и перестанет быть проверкой live.

Reset policy

Базовая v42 / UTC daily Memory 288 использует дневной UTC-сброс и ограниченную по числу s300-фреймов память Decima:

--swarm-session-reset utc-day
--swarm-session-frames 288
--swarm-session-anchor utc

Смысл:

  • Decima слушает поток s300 дневными блоками по 288 закрытых s300-фреймов;
  • 288 * 300 секунд = 24 часа;
  • UTC-день задает воспроизводимый якорь для replay/live;
  • после любого fired/winner frame агент делает RESET_DOMAIN(0xFFFF);
  • следующий сигнал ищется уже из чистого состояния;
  • календарная граница UTC-day не считается рыночным событием.

Важно: 288 не является универсальной константой Decima. Это текущий market session profile для BTCUSDT, потому что крипторынок торгуется 24/7, а s300 дает 288 закрытых 5-минутных bucket за UTC-сутки.

Для рынков с ночным закрытием память должна считаться от активной торговой сессии, а не от календарных суток. Если основная сессия длится 12 часов, профиль может быть около 144 s300-фреймов; если 8 часов - около 96; если есть premarket/afterhours, нужен отдельный профиль ликвидности. Для таких рынков reset должен быть привязан к биржевой сессии и ее timezone, а не механически копировать BTCUSDT utc-day=288.

Старый совместимый режим Memory 432 считал границы от первого s300-фрейма текущего replay/live run:

--swarm-session-reset frame-count
--swarm-session-frames 432
--swarm-session-anchor local

Такой режим зависит от точки старта окна. Если replay начинается на несколько часов раньше или позже, границы сдвигаются, а Decima получает другую фазу памяти.

Для воспроизводимого live/replay parity используется абсолютная сетка:

--swarm-session-reset utc-day
--swarm-session-frames 288
--swarm-session-anchor utc
--swarm-session-offset-frames 0

В этом режиме границы считаются от UTC-дня s300 bucket, поэтому restart live и сдвиг начала walk-forward не меняют саму сетку памяти.

Но якорь решает только границы. При запуске live внутри уже идущего 288-блока Decima все равно стартует холодной, если не дать ей предыдущие s300-фреймы этой же session. Для этого добавлен swarm warmup:

--swarm-warmup-frames-glob 'store/market/runs/BTCUSDT-*/frames.mtf.s300.jsonl'

Warmup-фреймы участвуют в состоянии d8_agent, но их fired/winner не попадают в short.signals.jsonl. Директор видит только live-сигналы после старта.

Правильная VPS-схема для tenant:

swarm_session_reset=utc-day
swarm_session_frames=288
swarm_session_anchor=utc
swarm_session_offset_frames=0
swarm_warmup_frames_glob=...

Если локальная история покрывает только закрытые UTC-дни, а tenant стартует внутри текущего 288-блока, нужен intraday backfill от начала текущей session до момента запуска. Без этого границы уже правильные, но память Decima все еще недогрета.

Диагностический sweep по offset остается исследовательским инструментом:

python3 store/market/tools/whaler_session_offset_sweep.py \
  --start 2026-01-01 \
  --end 2026-06-15 \
  --out-dir store/market/runs/v42_memory288_offset_sweep_all_history \
  --offsets 0,48,96,144,192,240 \
  --anchor local \
  --entry-exposure 0.5 \
  --carry-open-position

Отчет пишет:

summary.offset_sweep.json
summary.offset_sweep.tsv
summary.offset_sweep.html

Важные поля:

  • return_pct и max_dd_pct показывают финансовую устойчивость режима;
  • exact_jaccard_vs_base сравнивает точное совпадение s300-сигналов с baseline offset;
  • near_jaccard_vs_base считает совпадение с небольшим допуском по s300-фреймам;
  • низкий Jaccard при положительном PnL означает, что edge может быть устойчивым, но конкретные входы сильно зависят от фазы памяти.

Режим utc-day=288 является текущей базой только для BTCUSDT / crypto 24/7. Для других рынков он должен проверяться как гипотеза market session profile, а не переноситься автоматически:

--swarm-session-reset utc-day
--swarm-session-frames 288

Он привязывает Decima к внешней UTC-границе. На BTCUSDT это оказалось воспроизводимой рабочей сеткой. На рынке с биржевым открытием/закрытием такой якорь может быть неверным, потому что ночь, аукционы, premarket и тонкая ликвидность имеют другую гидравлику.

Чистый utc-month тоже проверен отдельно. Он хорошо прошел июнь, но на all-history дал слабый результат. Ранний результат utc-month-frame-count=288 был выше, но оказался артефактом расчета session bucket через фактический frame_seconds=299 на последнем укороченном bucket дня. После фикса bucket считается по номинальному s300=300 секунд, и utc-month-frame-count=288 совпадает с utc-day.

Level-v3 invariant

Для истории level-v3 держит инвариант:

86400 frames per day

Это было специально исправлено, потому что без этого MTF и walk-forward могут "терять" пустые секунды и давать другой слух.

Для исторического walk-forward текущий канонический вход:

store/market/runs/SYMBOL-YYYY-MM-DD/frames.level-v3.jsonl

Для v42 replay это означает, что WF строит MTF заново из raw level-v3, а не полагается на сохраненные sidecar-файлы. Канонический торговый вход Decima получается детерминированной командой:

frames.level-v3.jsonl
-> frames.mtf.s300.jsonl
-> frames.mtf.s1800.jsonl

Параметры rebuild зафиксированы:

level_v3_frame_anchor=utc-day-first-trade
mtf_bucket_anchor=utc-day-row
mtf_bootstrap_buckets=16
mtf_lane_session_reset=utc-day

Изменение любого из этих параметров создаёт другую VSB, даже если исходные aggTrades и число кадров совпадают. Поэтому отчёты разных anchor/bootstrap не склеиваются в один benchmark.

Перед сравнением результатов история должна пройти инвариант:

level-v3=86400
s300=288
s1800=48

Сохраненные frames.mtf.s*.jsonl можно использовать как cache или для диагностики, но не как источник публичной текущей витрины, если они были собраны старой версией MTF-нормализации. Для проверяемости текущий baseline считается через --no-use-existing-mtf.

Старый frames.jsonl в дневных исторических run-директориях относится к раннему raw-поколению pipeline и остается только для совместимости старых research-инструментов. Новые replay/WF-команды не должны брать его по умолчанию.

В live run-директориях имя frames.jsonl пока сохраняется как рабочий секундный поток live runner:

store/market/runs/live/s300_stage_SYMBOL_YYYYMMDD_HHMMSS/frames.jsonl

Это не тот же случай, что дневная историческая база. Live frames.jsonl является входом для MTF watcher внутри текущего процесса, а исторический replay должен идти через frames.level-v3.jsonl.

Проверка ETHUSDT после исправления:

150 дней
каждый день: 86400 frames.level-v3.jsonl

Ведение истории

Историю надо вести постоянно, потому что она стала частью live-эксплуатации, а не только материалом для research.

Ежедневное обновление закрытых дней:

cd store/market
./tools/whaler_update_history.py --symbol BTCUSDT

По умолчанию инструмент берет последний закрытый UTC-день, докачивает недостающие Binance daily aggTrades, строит level-v3, затем пересобирает MTF:

frames.mtf.s300.jsonl
frames.mtf.s1800.jsonl
tape.mtf.s300.raw8.vsb
tape.mtf.s1800.raw8.vsb
mtf.s300.html
mtf.s1800.html

Для полного дня он валидирует:

level-v3 = 86400
s300     = 288
s1800    = 48

Если проверка не проходит, такой день нельзя считать надежным для replay и phase warmup.

Phase warmup из истории

Проблема холодного старта: директору нужен phase-lookback=224 на s1800. Без истории это примерно:

224 * 30 минут = 112 часов = 4.7 суток

Это слишком долго для VPS-перезапусков. Поэтому live-директор может брать исторические s1800-кадры как фазовый warmup:

store/market/tools/whaler_s300_stage_live.py \
  --replace \
  --symbol BTCUSDT \
  --swarm-session-reset utc-day \
  --swarm-session-frames 288 \
  --swarm-session-anchor utc \
  --long-trail-activate-bps 75 \
  --long-trail-giveback-bps 50 \
  --trail-reentry-block same-side-reclaim \
  --trail-reclaim-cooldown-frames 12 \
  --trail-reclaim-buffer-bps 0 \
  --trail-reclaim-max-entries 1 \
  --trail-reclaim-failure-bps 150 \
  --short-trail-activate-bps 300 \
  --short-trail-giveback-bps 350 \
  --phase-warmup-frames-glob 'store/market/runs/BTCUSDT-2026-*/frames.mtf.s1800.jsonl'

Важно: warmup не делает историю частью текущего PnL.

Он используется только для расчета HTF-фазы:

phase context = historical closed s1800 + live closed s1800
trade frames  = live closed s300 only
signals       = live Decima s300 only
curve/equity  = live decisions only

В director.json/html это видно отдельными счетчиками:

phase_live_frames
phase_warmup_frames
phase_total_frames

Нормальная картина сразу после перезапуска:

phase_live_frames   = 0
phase_warmup_frames > 224
signals             = 0
trades              = 0

Это значит, что фазовый контекст уже есть, но текущий live пока не закрыл s300-bucket и Decima пока не выдала новый live-сигнал.

Пример ETHUSDT transfer

Команда:

python3 store/market/tools/whaler_live_replay_walkforward.py \
  --frames-glob 'store/market/runs/ETHUSDT-2026-*/frames.level-v3.jsonl' \
  --start 2026-01-01 \
  --end 2026-05-30 \
  --out-dir store/market/runs/v42_transfer_ETHUSDT_levelv3_fixed_2026-01-01_2026-05-30

Результат:

raw_frames=12960000
s300 closed_frames=43200
s1800 closed_frames=7200
short signals=138
trades=68
pnl=+13.6846
return=+13.68%
win=64.7%
dd=19.22%
equity=113.6846
position=flat

Помесячно:

2026-01: trades=13 pnl=+10.0041 win=76.9%
2026-02: trades=15 pnl=-6.2165  win=46.7%
2026-03: trades=15 pnl=+2.6741  win=60.0%
2026-04: trades=16 pnl=+5.3470  win=68.8%
2026-05: trades=9  pnl=+1.8759  win=77.8%

Вывод: ETH transfer не идеален, но положительный. Это полезный стресс-тест переносимости.

Что сравнивать live vs replay

Минимальный parity checklist:

одинаковая .d8p
одинаковый labels TSV
одинаковый MTF scale
одинаковые closed bucket rules
одинаковая bootstrap policy
одинаковая session reset policy
одинаковый phase lookback
одинаковая phase warmup policy
одинаковая комиссия
одинаковый same-side режим
одинаковые position manager exits
одинаковый catalog-фильтр, если он включен
одинаковое carry/close at EOF

Сравниваемые выходы:

s300 frame count
s1800 frame count
signals count
pattern ids
director decisions
position transitions
position manager close reasons
realized pnl
mark pnl
drawdown

Почему live может временно молчать

v42 может долго не давать торговых действий:

  • s300 прогревается;
  • s1800 прогревается дольше, если phase warmup не подключен;
  • phase может быть neutral;
  • боковик не проходит фильтры;
  • Decima может услышать сигнал, но директор решит skip;
  • позиция может быть уже открыта, а same-side не дает причины закрыть.

Это нормальное поведение. Молчание в боковике - часть стратегии.

Что видно в HTML-мониторе

director.html в v42 используется как рабочий live-монитор директора. Это не финальный отчет по стратегии, а экран состояния текущего run.

Сейчас в монитор выводятся ключевые торговые и фазовые счетчики:

signals
trades
phase_live_frames
phase_warmup_frames
final_equity
final_mark_equity
return_pct
mark_return_pct
win_rate
profit_factor
max_drawdown_pct
ws_state
inventory_state
price/equity chart
decision markers
JSON/TSV/HTML paths

Для VPS-мониторинга поверх этого полезно держать отдельный слой health status:

run id
symbol
env
start time
uptime
last update time
websocket messages
trades received
frames
reconnects
s300 frames
s1800 frames
position
equity
realized pnl
mark pnl
last decision
last reason

director.html закрывает мониторинг торговой логики. Отдельный VPS health status закрывает мониторинг процесса, данных и инфраструктуры.

Когда live "ок"

Live считается технически здоровым, если:

  • WebSocket не завис;
  • ws_messages растет;
  • trades растут;
  • frames растут;
  • reconnects не растут бесконтрольно;
  • s300 и s1800 файлы обновляются;
  • director.json/html обновляются;
  • inventory не расходится с директором;
  • нет неожиданных exceptions;
  • свободное место на диске не заканчивается.

Когда live не "ок"

Нужно вмешиваться, если:

  • WebSocket сообщений нет;
  • trades не растут при живом рынке;
  • frames не растут;
  • s300/s1800 html не обновляется;
  • director не пишет новые отчеты;
  • reconnects растут часто;
  • position в inventory отличается от биржи;
  • диск почти заполнен;
  • director.json битый или пустой;
  • live использует не ту .d8p или labels.

Правило для будущих экспериментов

Для любого нового v43+ эксперимента фиксируем короткий паспорт:

чем он отличается от v42?
какой слой изменен?
сохраняется ли live/replay parity?
какой baseline replay?
какой transfer по ETHUSDT?
какой риск хуже, чем у v42?

Без этого v42 остается базой.