Audit · 2026-05-03 · Sunday

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.

6
P0 critical (broker + advisor)
12
P1 should-fix
9
P2 nice-to-have
14
improvement ideas

Методология

Два параллельных 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)

P0 one_per_ticker_per_day gate INACTIVE
core/adaptive_risk_gates.py:93-103, не вызывается в master.py / order_router.py
record_entry() существует, но не вызывается ниоткуда. validate() читает opened_today, который вечно пуст.
Impact: стратегия может re-enter тот же тикер 10× в день без отказа. Daily duplicate-entry protection полностью неработоспособна.
P0 realized_pnl не сбрасывается между торговыми днями
core/portfolio.py:46 (set only at __init__)
Daily loss gate (risk_manager.py:59) и adaptive_gates.check_daily_loss() читают это поле. Если вчера было -$200, сегодняшние -$300 трипают gate только при -$300 cumulative, а не -$500 intraday.
Impact: daily loss limit фактически measures multi-day cumulative, не per-day. Gate срабатывает поздно или не срабатывает на multi-day winning→losing.
P0 Kill-switch выводит side из size > 0
core/kill_switch.py:172
pos.size хранится как abs(), всегда положителен → side жёстко = "LONG" для всех. SHORT-позиции flatten BUY-ордером (правильно), но логика accidentally right для wrong reason.
Impact: если encoding size когда-то изменится на signed — emergency flat будет слать wrong-side ордера, ухудшая шорты.

P1 — Should-fix (7)

P1 MOO/OPG routing fix UNVERIFIED в live режиме
config/broker.yaml:81-86 + core/grpc_broker_bridge.py:41-50
Intent map MOO→(OPG, Dopg) корректный. Apr 26 P0 (598 MOO как session=EXT) исправлен в коде, НО никогда не проверен живьём — dry_run: true + fallback_to_clipboard: false залочены с Apr 29.
Impact: при flip dry_run=false с fallback=false — любой gRPC failure = silent order drop без alert.
P1 fallback_to_clipboard:false silently drops failed orders
master.py:519-524
log.error идёт в файл, но НЕТ Telegram/Slack alert и НЕТ retry. Намеренно с Apr 29, но без оператора-уведомлений.
Impact: при future live mode целые баскеты могут потеряться без алерта.
P1 Нет concurrency lock на portfolio.positions
core/portfolio.py
_reconcile_positions(), _price_loop(), _on_event(), flat_all_via_grpc — все мутируют positions concurrently. positions.pop() + used_bp -= non-atomic.
Impact: race condition → incorrect used_bp при concurrent basket + reconcile.
P1 _ADVISOR_DB_PATH hardcoded на UUID worktree
master.py:396 — bridge-cse_01WWSfU8F2uwewtCu23z9Rm3
При rebase/rename worktree LLM-skip feature тихо перестаёт работать.
P1 adaptive_gates.reset_day() никогда не запускается
core/adaptive_risk_gates.py:105-108
Метод существует — нет scheduler entry, нет вызова. _emergency_flat_fired latch переживает overnight.
Impact: после EOD flat-fire broker не сможет flat утром снова.
P1 gRPC cancel_order ad-hoc channel + silent except
core/grpc_broker_bridge.py:285-310
Создаёт новый insecure channel per cancel call. CancelRequest.ClOrdId в try/except — silent no-op cancel при schema mismatch.
P1 Universe CSV path hardcoded
master.py:112 — "C:/datum-api-examples-main/universe_adv_2m.csv"
Не из конфига; warning при отсутствии, но pm_bars молча идёт с пустым universe.

P2 — Nice-to-have (5)

P2 moo_955 в config но НЕ зарегистрирована в master
broker.yaml:191-225 (enabled: true) vs master.py:197-205 (нет в registry)
Config drift — стратегия даёт ноль events.
P2 Dashboard 0.0.0.0:8301 — GET endpoints без auth
dashboard/server.py:67-74 + 2115
/api/positions, /api/overview, /api/health unauth (только POST требует token). Любой Tailscale peer читает позиции и BP.
P2 /api/daily_loss/check unauth POST
master.py:1580 (нет Depends(require_token))
Внешний peer может POST'ом форсировать daily loss trip → pause всех стратегий.
P2 _audit() O(N²) parquet read+write на каждый order
core/grpc_broker_bridge.py:399-404
read_parquet + concat + to_parquet под lock на каждый order. На 200-order день финальные writes — несколько секунд latency.
P2 Нет fill-rate / session observability
Нет метрик: order fill latency (submit→adoption), fill rate by session (MOO vs DAY vs MOC), slip distribution. Apr 26 проблема (1% fill в 1s, 54% past 30s) была НЕвидима из логов.
Impact: future routing regressions невидимы до manual audit.

📡 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)

P0 Timezone bug в loop_until_close()
scanner.py:351-352
datetime.combine(_now_et().date(), SCAN_START) - _now_et().replace(tzinfo=None) — оба naive, корректно только если PC в ET-local. На UTC-Windows PC дата может промахнуться на 5h, особенно на DST transition.
Impact: scanner может проснуться на 5h раньше или проспать. Памятка memory: пользователь в UA, host PC — Windows 11 (TZ?).
P0 monitor.py + install_schtasks.bat ОТСУТСТВУЮТ
tools/moo1100_advisor/ — README:12-15 их описывает, файлов нет
Schtask MOO1100_LIVE_MONITOR 08:00→11:15 ET (every 30s) ссылается на monitor.py --loop, которого нет. monitor.json в viz/ никогда не пишется → live-position tile в дашборде вечно пуст.
Impact: user уверен что мониторинг работает, на деле — silent gap.
P0 pm_dvol_usd отображается как «K» но stored как raw $
scanner.py:337 — f"pm$={c.get('pm_dvol_usd'):.0f}K" + 151 (raw pm_close × pm_vol)
Stock $2.3M в Telegram показывается как «pm$=2300000K» — labels ~1000× off.
Impact: silent UX corruption; оператор может неправильно интерпретировать ликвидность.

P1 — Should-fix (5)

P1 Нет freshness check на earnings_calendar_forward.parquet
regime_overlay.py:59-67
Если nightly cron упал, файл может быть несколько дней stale, но classify_today() вернёт уверенный tier без флага стейлности.
P1 OVERLAY_JSON path хардкод may01
regime_overlay.py:44 — research_results/moo1100_earnings_regime_may01/...
При новом research run путь не обновится автоматически.
P1 VIZ_MIRROR = C:/Users/wsu/Downloads/...
regime_overlay.py:47
Не портативно, write success не валидируется. Другой user — silent fail.
P1 BOOST_SUGGEST tier — мёртвый код
regime_overlay.py:6, 113, 243
В docstring и tier_emoji есть, но classification logic его никогда не выставляет.
P1 fomc_today хардкод False + macd_hist_pm=0 stub
scanner.py:171, 221-222
Advisor показывает кандидатов на FOMC days которых live-system гейтит. macd=0 systematically под-оценивает gate bonus, advisory grade ниже live.
Impact: систематический drift между advisory и live signals — сложно использовать advisor для post-trade analysis.

P2 — Nice-to-have (4)

P2 Нет signal-count breakdown в JSON
candidates_total + skip_reasons есть, но нет per-category breakdown (gap vs ADV vs pm_dvol), нет regime confidence interval, нет data-lag метрики.
P2 loop_until_close() sleeps плоско 300s после iteration
scanner.py:370
При 45s scan следующий старт T+345s, не T+300s. За 85min — 16 sкан вместо 17.
P2 all_iterations.json теряет regime + market data
scanner.py:297-302
P2 Нет backtest-vs-live drift detection
Нет механизма сравнить advisor-surfaced candidates vs что asym_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 — нет).

interactivebrokers.github.io/tws-api/basic_orders.html

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) ретрай = двойной ордер.

grpc.io/docs/guides/retry/

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).

discuss.python.org · oneuptime.com/grpc-retry-policies

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).

bixtech.ai/observability · grpc.io · prometheus.io/docs

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.

elitetrader.com/et/threads/daily-loss-limit.349643/

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 и сверять.

interactivebrokers.github.io/tws-api/order_management.html

💡 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 строка кода.

Trade-off: none significant. Edge case: при partial fill duplicate не считается (правильно).

2Daily PnL reset через scheduler 04:00 ET

Новое поле portfolio.daily_pnl отдельно от cumulative_pnl. Risk gates читают daily_pnl. Reset в scheduler @04:00 ET (до pre-market).

Trade-off: требуется DB schema migration для positions table; reconciled positions нужно разделить «realized today» vs «realized prior». ~1 день работы + tests.

3Fix pm_dvol_usd Telegram label

Деление на 1000 ИЛИ удалить «K». 1 строка.

Trade-off: нет. Чистая правка UX.

4asyncio.Lock на portfolio mutations

Wrap positions + used_bp changes в single asyncio.Lock в Portfolio. Не держать lock через await points.

Trade-off: <1ms overhead. Риск deadlock если забыть — нужен careful review всех call sites.

5Создать monitor.py ИЛИ удалить schtask

README обещает live-position tile, но schtask пишет в несуществующий файл. Либо реализовать (proxy на /api/positions broker dashboard каждые 30s, mirror в viz/monitor.json), либо удалить упоминание из README + schtask.

Trade-off: создать — ~50 строк. Удалить — теряешь обещанный feature.

🛠️ 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.

Trade-off: требует pinning ClOrdID на client side и broker должен дедуплицировать по нему. Без идемпотентности retry = double order — ОПАСНО для trading. Плюс: token bucket throttle защищает от retry storm бесплатно.

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).

Trade-off: +1 dependency. Без full Grafana stack теряешь PromQL ad-hoc queries, но получаешь /metrics endpoint совместимый со стандартом — позже Grafana прикрутишь без переписывания.

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) чтобы не флудить.

Trade-off: ~30 строк. Может быть шумно при mass-fail (но именно тогда и нужно).

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 над дефолтом.

Trade-off: minor expansion config. Нужен миграционный пасс по 5-7 файлам.

10Freshness block в каждом advisor JSON

Каждый JSON output (regime_today, scan, monitor) имеет data_freshness: { source: mtime, age_hours, stale: bool }. Dashboard рендерит badge «STALE» при age > threshold.

Trade-off: +10ms stat() calls. Огромный gain visibility — увидишь stale parquet моментально.

11Schedule adaptive_gates.reset_day() daily 09:30 ET

Просто scheduler entry в master.py. Плюс reset latch _emergency_flat_fired.

Trade-off: нет. 5 строк.

🏗️ 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).

Trade-off: +1 файл, +1 источник истины. На retail volume — пара МБ/день.

13SQLite WAL audit вместо parquet O(N²)

Append-only SQLite (WAL mode) для audit log. Daily compaction cron в parquet для analytics.

Trade-off: теряешь columnar query convenience внутри дня. Гейн: O(1) append, нет lock contention.

14Bearer token на ВСЕ dashboard endpoints

Не только POST. GET тоже. Уже есть require_token dependency — добавить ко всем чувствительным GET (positions, overview, daily_loss/check).

Trade-off: нужно прокинуть header в браузер (localStorage + fetch wrapper). Но для one-user setup — ~15 строк.

🚫 Опасайся обмана — что НЕ делать

Принцип: Любая «волшебная» библиотека / framework требует валидации на ваших данных. Маркетинг ≠ работа. Backtest ≠ live.

❌ Не заменять 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.

📊 Сводка приоритетов

#ДействиеComponentEffortPriority
1Fix record_entry() в OrderRouterbroker1 lineP0
2Daily PnL field + reset @04:00 ETbroker1 dayP0
3Fix pm_dvol K-labeladvisor1 lineP0
4asyncio.Lock на portfolio mutationsbroker2-3hP0
5Fix kill_switch side detectionbroker1hP0
6Создать monitor.py или удалить schtaskadvisor2h or 5minP0
7Fix TZ bug в loop_until_closeadvisor1hP0
8Schedule reset_day dailybroker30minP1
9gRPC native retry policy + ClOrdID idempotencybroker2 daysP1
10Prometheus-client /metrics endpointbroker1 dayP1
11Telegram alert на gRPC failuresbroker3hP1
12Все paths → broker.yamlboth1 dayP1
13data_freshness block в advisor JSONadvisor3hP1
14Freshness check на earnings calendaradvisor1hP1
15FOMC из static calendar (не stub False)advisor2hP1
163-way reconcile (intent/acked/fill)broker3 daysP2
17SQLite WAL audit replace parquetbroker2 daysP2
18Auth на все dashboard GETbroker1 dayP2
19moo_955 в registry или удалить config blockbroker30minP2
20Backtest-vs-live drift detectoradvisor2 daysP2

❓ Open questions для пользователя

  1. Был ли когда-либо проведён session=OPG verification в TradingApp logs после Apr 28 fixes? Файл 2026-04-28_CAP.log должен существовать. Если verification не сделан — это первый шаг перед любым разговором про flip dry_run.
  2. Нужно ли реально daily reset для realized_pnl или текущее cumulative-since-start поведение намеренное? Меняет когда -$500 gate срабатывает.
  3. moo_955 в config с enabled:true но не в registry — bug или intentional (заменена на moc_moo)?
  4. Существует ли monitor.py в private branch / другом worktree? Или README обещает фичу которой нет?
  5. На какой TZ установлен host PC (Windows 11 в офисе)? Это определяет масштаб TZ-бага в scanner.
  6. Готов ли пользователь к idempotent-retries (требует ClOrdID на стороне broker для дедупликации)? Без этого включать gRPC native retry policy ОПАСНО.
  7. Нужна ли реальная Prometheus или достаточно lightweight stats в SQLite + custom dashboard?