Broker + Advisor — что чинить, что улучшать
Полный аудит trading stack. Поиск дыр в коде (file:line), внешние best-practices с форумов и docs, идеи улучшений с trade-offs. Скептичный фильтр на маркетинговые «решения».
📋 TL;DR
Стек живой и в целом устойчивый: gRPC + reconcile loop + adaptive risk gates + safety latch работают. Но есть 3 P0 в broker (один inactive risk gate, неправильный daily PnL reset, кривая kill-switch side detection) и 3 P0 в advisor (TZ-баг, отсутствует monitor.py, кривой Telegram-формат pm_dvol). Все исправимо одним днём + неделя на observability и idempotency.
Методология
Два параллельных deep-dive агента прошли по broker/ (master.py, core/*, strategies/*, config/broker.yaml) и tools/moo1100_advisor/ (scanner, regime_overlay, strategy_shim) с конкретными file:line. Параллельно — 4 WebSearch: IBKR TWS API MOO/OPG routing, retail kill-switch паттерны (EliteTrader), gRPC resilience patterns (deadlines/retries/breaker, oneuptime + grpc.io 2026), trading observability (Prometheus histograms p95/p99). Каждая идея помечена trade-off; в конце — секция «осторожно от обмана».
🔧 Broker stack — gaps
Architecture: single-process Python asyncio (master.py) → 8+ strategies → RiskManager → OrderRouter → GrpcBroker (или clipboard fallback) → reconcile loop. FastAPI dashboard на 8301. DuckDB state. Named-pipe TRAP reader.
P0 — Critical (3)
one_per_ticker_per_day gate INACTIVErecord_entry() существует, но не вызывается ниоткуда. validate() читает opened_today, который вечно пуст.realized_pnl не сбрасывается между торговыми днямиrisk_manager.py:59) и adaptive_gates.check_daily_loss() читают это поле. Если вчера было -$200, сегодняшние -$300 трипают gate только при -$300 cumulative, а не -$500 intraday.size > 0pos.size хранится как abs(), всегда положителен → side жёстко = "LONG" для всех. SHORT-позиции flatten BUY-ордером (правильно), но логика accidentally right для wrong reason.P1 — Should-fix (7)
dry_run: true + fallback_to_clipboard: false залочены с Apr 29.fallback_to_clipboard:false silently drops failed orderslog.error идёт в файл, но НЕТ Telegram/Slack alert и НЕТ retry. Намеренно с Apr 29, но без оператора-уведомлений.portfolio.positions_reconcile_positions(), _price_loop(), _on_event(), flat_all_via_grpc — все мутируют positions concurrently. positions.pop() + used_bp -= non-atomic.used_bp при concurrent basket + reconcile._ADVISOR_DB_PATH hardcoded на UUID worktreebridge-cse_01WWSfU8F2uwewtCu23z9Rm3adaptive_gates.reset_day() никогда не запускается_emergency_flat_fired latch переживает overnight.cancel_order ad-hoc channel + silent exceptCancelRequest.ClOrdId в try/except — silent no-op cancel при schema mismatch."C:/datum-api-examples-main/universe_adv_2m.csv"P2 — Nice-to-have (5)
moo_955 в config но НЕ зарегистрирована в master/api/daily_loss/check unauth POST_audit() O(N²) parquet read+write на каждый order📡 Advisor stack — gaps
Architecture: чистый observation layer. scanner.py (5-min loop pre-market через moo_1100_asym.score_candidate()), regime_overlay.py (earnings tier classifier с May 1 fresh calendar), strategy_shim.py. Live execution живёт в broker/strategies/asym_pump_module.py. Decoupling — реальный.
P0 — Critical (3)
loop_until_close()datetime.combine(_now_et().date(), SCAN_START) - _now_et().replace(tzinfo=None) — оба naive, корректно только если PC в ET-local. На UTC-Windows PC дата может промахнуться на 5h, особенно на DST transition.monitor.py + install_schtasks.bat ОТСУТСТВУЮТMOO1100_LIVE_MONITOR 08:00→11:15 ET (every 30s) ссылается на monitor.py --loop, которого нет. monitor.json в viz/ никогда не пишется → live-position tile в дашборде вечно пуст.pm_dvol_usd отображается как «K» но stored как raw $f"pm$={c.get('pm_dvol_usd'):.0f}K" + 151 (raw pm_close × pm_vol)P1 — Should-fix (5)
earnings_calendar_forward.parquetclassify_today() вернёт уверенный tier без флага стейлности.OVERLAY_JSON path хардкод may01research_results/moo1100_earnings_regime_may01/...VIZ_MIRROR = C:/Users/wsu/Downloads/...BOOST_SUGGEST tier — мёртвый кодtier_emoji есть, но classification logic его никогда не выставляет.fomc_today хардкод False + macd_hist_pm=0 stubmacd=0 systematically под-оценивает gate bonus, advisory grade ниже live.P2 — Nice-to-have (4)
loop_until_close() sleeps плоско 300s после iterationall_iterations.json теряет regime + market dataasym_pump_module.py реально торговал.🔍 Внешние best-practices — что собрать с форумов и docs
1. IBKR MOO/OPG semantics — детали
MOO = market order с TIF=OPG. Если не filled на opening auction по COP (Calculated Opening Price) → IBKR автоматически re-submits как limit @ COP или best bid/ask. То есть EXT-routing симптомы Apr 26 (54% past 30s) могут быть НЕ вашим багом, а fallback от non-auction venue. Проверяйте: orderRef + permId + orderState.lastFillPrice через execDetails callback. Не все exchanges поддерживают OPG (некоторые OTC — нет).
2. gRPC retry — token bucket throttle (важно!)
gRPC поддерживает throttle limit per-server: client tracks token_count (init = maxTokens). Failed RPC -1, success +tokenRatio. Если token_count < half maxTokens → retries paused. Это родной механизм защиты от retry storm — не нужно строить tenacity+pybreaker. Конфигурируется через service_config.json: methodConfig.retryPolicy.maxAttempts/initialBackoff/maxBackoff/backoffMultiplier/retryableStatusCodes. Critical для trading: без idempotency-key (ClOrdID) ретрай = двойной ордер.
3. Python resilience стек — ловушка
discuss.python.org/t/106597: классическая проблема — tenacity + pybreaker не делятся state. Retries продолжают firing когда circuit breaker уже должен был открыться. Timeouts не учитывают уже потраченные retry attempts. Решение — единая библиотека: aiobreaker или failsafe-py (порт от failsafe-go). Лучше: встроенная gRPC retry policy (см. выше) + один outer breaker на уровне Python (просто counter в master.py).
4. Observability — histogram-based latency
PromQL histogram_quantile(0.95, rate(metric_bucket[5m])) — стандарт для p95/p99 latency. Альтернатива тяжёлому стеку (Prometheus + Grafana + Alertmanager) для single-machine retail: statsd-lite в SQLite + ваш существующий FastAPI dashboard + Telegram alert. Минимально нужные метрики: order_submit_latency_ms (histogram), fills_total{session="OPG|EXT|DAY"} (counter), slip_bps (histogram), reconcile_phantom_drops_total (counter), grpc_breaker_state{state="open|half|closed"} (gauge).
5. EliteTrader — паттерны daily loss / kill switch
Реальные обсуждения retail traders: most common failure mode — daily loss reset не на уровне broker, а на уровне strategy state. Несколько обсуждений жалуются что NinjaTrader/TradeStation reset daily P&L по 6pm ET, а custom limits не учитывают это и трипают на overnight margin moves. Уроки для нашей системы: (a) explicit reset hook ПЕРЕД первым signal дня, (b) явное разделение realized vs marked-to-market, (c) аудит daily loss в логах с timestamp reset.
6. Reconcile correctness — паттерны из HFT
Best practice: не верить broker side как single source of truth. Поддерживай 3 mirror'а: (1) intent log (что хотели послать), (2) acked log (что broker ack'нул), (3) fill log (из execDetails). Reconcile = 3-way diff. Phantom — это (1) есть, (2)/(3) нет. Stuck — (2) есть, (3) нет. Текущий код в нашем broker делает 2-way (TRAP vs internal portfolio); идея для P1 — добавить лог acked_orders.parquet и сверять.
💡 Improvement ideas (приоритезированно)
🔥 P0 — One-day fixes
1Fix record_entry() call site
В OrderRouter.route() после approval вызвать adaptive_gates.record_entry(ticker, side) ПЕРЕД _try_grpc_submit (не после — иначе retry даст duplicates). 1 строка кода.
2Daily PnL reset через scheduler 04:00 ET
Новое поле portfolio.daily_pnl отдельно от cumulative_pnl. Risk gates читают daily_pnl. Reset в scheduler @04:00 ET (до pre-market).
3Fix pm_dvol_usd Telegram label
Деление на 1000 ИЛИ удалить «K». 1 строка.
4asyncio.Lock на portfolio mutations
Wrap positions + used_bp changes в single asyncio.Lock в Portfolio. Не держать lock через await points.
5Создать monitor.py ИЛИ удалить schtask
README обещает live-position tile, но schtask пишет в несуществующий файл. Либо реализовать (proxy на /api/positions broker dashboard каждые 30s, mirror в viz/monitor.json), либо удалить упоминание из README + schtask.
🛠️ P1 — One-week features
6gRPC service config — встроенный retry + throttle
Использовать gRPC native service_config.json с retryPolicy + retryThrottling вместо ручного try/except. Backoff exponential, retryableStatusCodes = [UNAVAILABLE, DEADLINE_EXCEEDED]. ClOrdID как idempotency-key для UNARY_IDEMPOTENT.
7Histogram-based observability — minimal Prometheus
Не Grafana stack. Просто: prometheus-client Python lib + 5 histograms (submit_latency, fill_latency, slip_bps, reconcile_diff, grpc_health) + один scrape endpoint /metrics в существующем dashboard. Telegram alert на простых правилах в master.py (если p95 latency > 500ms за 5min — alert).
8Telegram-alert на gRPC failure (не silent)
При fallback_to_clipboard:false + gRPC fail: вместо log.error — Telegram alert с тикером, side, qty, error code. Backoff: один alert на 60s per (ticker, error_class) чтобы не флудить.
9Все hardcoded paths → broker.yaml
universe_adv_2m.csv, advisor.duckdb worktree path, kill-switch paths, advisor OVERLAY_JSON и VIZ_MIRROR. Env-var override приоритет config над дефолтом.
10Freshness block в каждом advisor JSON
Каждый JSON output (regime_today, scan, monitor) имеет data_freshness: { source: mtime, age_hours, stale: bool }. Dashboard рендерит badge «STALE» при age > threshold.
11Schedule adaptive_gates.reset_day() daily 09:30 ET
Просто scheduler entry в master.py. Плюс reset latch _emergency_flat_fired.
🏗️ P2 — Architecture (one-month)
123-way reconcile (intent / acked / fill)
Добавить acked_orders.parquet append-only лог между intent (что послали) и fill (TRAP). Reconcile = 3-way diff → ясно отличает stuck (acked но не filled) от phantom (filled но не intended).
13SQLite WAL audit вместо parquet O(N²)
Append-only SQLite (WAL mode) для audit log. Daily compaction cron в parquet для analytics.
14Bearer token на ВСЕ dashboard endpoints
Не только POST. GET тоже. Уже есть require_token dependency — добавить ко всем чувствительным GET (positions, overview, daily_loss/check).
🚫 Опасайся обмана — что НЕ делать
❌ Не заменять rule-based regime overlay на ML
«Regime detection» библиотеки (RegimeShift, hmmlearn HMM, change-point detection) часто выглядят отлично in-sample и ломаются OOS. Ваш текущий regime_overlay — explainable правила (HIGH × Megacap BMO etc.) с честным p-value. Не разменивай на black-box. ⚠️ Caveat user'а: 1.99y backtest, N=196-385 buckets — borderline robust сами по себе. Не делай advisory tier live signal до N≥500/bucket.
❌ Не строй tenacity + pybreaker stack
Известная coordination-проблема: state не делится, retries firing когда breaker должен был быть open. Вместо этого — gRPC native retry policy + один outer counter в master.py. Если хочется готовое — failsafe-py или aiobreaker (но всё равно протестируй на симуляции gRPC fail).
❌ Не перетаскивай на FreqTrade / Hummingbot / Lean
Эти фреймворки — generic. Перенос потребует переписать стратегии, потерять tight-coupled reconcile с TradingApp + .NET, и вы теряете все custom risk gates / kill switch паттерны которые вы итеративно собрали (Apr 26-29 fixes). Эта инфра — ваше IP. Не выкидывайте ради синтаксического сахара.
❌ Не запускай Prometheus + Grafana + Alertmanager full stack для one-machine retail
Это overkill: Docker Compose, persistence volumes, Alertmanager routing, dashboards as code. Для одного PC всё это становится новым maintenance burden. Минимум: prometheus-client Python lib + /metrics endpoint + custom rule в master.py отправляющий Telegram. Позже, если данных накопилось — поднимешь Grafana отдельно.
❌ Не верь «AI auto-tuning» советников и hyperparameter optimizers
Optuna / Hyperopt over rolling backtests = 90% overfit на N≤500 buckets. Ваш current approach (3-auditor + permutation tests + WF) уже жёстче многих academic papers. Не променяй на ML auto-tune без strict OOS holdout 30%+.
❌ Не отключай safety latch без OPG verification
User directive Apr 29 «ордерами управлять нельзя» + dry_run=true. Любое предложение «давайте включим автоматическое исполнение, чтобы протестировать MOO routing» — путь к потере денег без оператора. Сначала verify session=OPG в TradingApp logs (минимум 5 days в sandbox/paper) — потом обсуждать flip.
📊 Сводка приоритетов
| # | Действие | Component | Effort | Priority |
|---|---|---|---|---|
| 1 | Fix record_entry() в OrderRouter | broker | 1 line | P0 |
| 2 | Daily PnL field + reset @04:00 ET | broker | 1 day | P0 |
| 3 | Fix pm_dvol K-label | advisor | 1 line | P0 |
| 4 | asyncio.Lock на portfolio mutations | broker | 2-3h | P0 |
| 5 | Fix kill_switch side detection | broker | 1h | P0 |
| 6 | Создать monitor.py или удалить schtask | advisor | 2h or 5min | P0 |
| 7 | Fix TZ bug в loop_until_close | advisor | 1h | P0 |
| 8 | Schedule reset_day daily | broker | 30min | P1 |
| 9 | gRPC native retry policy + ClOrdID idempotency | broker | 2 days | P1 |
| 10 | Prometheus-client /metrics endpoint | broker | 1 day | P1 |
| 11 | Telegram alert на gRPC failures | broker | 3h | P1 |
| 12 | Все paths → broker.yaml | both | 1 day | P1 |
| 13 | data_freshness block в advisor JSON | advisor | 3h | P1 |
| 14 | Freshness check на earnings calendar | advisor | 1h | P1 |
| 15 | FOMC из static calendar (не stub False) | advisor | 2h | P1 |
| 16 | 3-way reconcile (intent/acked/fill) | broker | 3 days | P2 |
| 17 | SQLite WAL audit replace parquet | broker | 2 days | P2 |
| 18 | Auth на все dashboard GET | broker | 1 day | P2 |
| 19 | moo_955 в registry или удалить config block | broker | 30min | P2 |
| 20 | Backtest-vs-live drift detector | advisor | 2 days | P2 |
❓ Open questions для пользователя
- Был ли когда-либо проведён session=OPG verification в TradingApp logs после Apr 28 fixes? Файл
2026-04-28_CAP.logдолжен существовать. Если verification не сделан — это первый шаг перед любым разговором про flip dry_run. - Нужно ли реально daily reset для
realized_pnlили текущее cumulative-since-start поведение намеренное? Меняет когда -$500 gate срабатывает. moo_955в config сenabled:trueно не в registry — bug или intentional (заменена на moc_moo)?- Существует ли
monitor.pyв private branch / другом worktree? Или README обещает фичу которой нет? - На какой TZ установлен host PC (Windows 11 в офисе)? Это определяет масштаб TZ-бага в scanner.
- Готов ли пользователь к idempotent-retries (требует ClOrdID на стороне broker для дедупликации)? Без этого включать gRPC native retry policy ОПАСНО.
- Нужна ли реальная Prometheus или достаточно lightweight stats в SQLite + custom dashboard?