Skip to content

Разбор торговой сессии harvester-engine 12–16 августа 2026 ​

Границы доказательности. Офлайн-часть — по неизменяемой копии data/harvester-engine-session-20260816T144518Z/ (происхождение и контрольные суммы — в harvester-engine-session-backup-2026-08-16.md). Живая часть — прогоны 16.08 против боевого инстанса, где все 42 позиции сессии на месте. Каждое утверждение ниже несёт источник: подкоманду forensics.sh, имя таблицы или файл кода. Где источника нет, стоит «не измерено».

Что этот документ НЕ делает. Не назначает пороги и не предлагает правок кода. Пункты, у которых уже есть заведённая заявка, сведены в §9 ссылкой, а не переписаны заново.


1. Окно торговли — 17 часов из четырёх суток ​

СобытиеМомент (UTC)Источник
Первое событие движка12.08 15:52:18events.ts_ms min
Первая позиция открыта12.08 15:59:44positions.opened_at min
Пауза (§8)13.08 04:09:34 → 08:31:16events
Последняя позиция открыта13.08 08:58:33positions.opened_at max
Остановка входов оператором13.08 09:02:26entry_halt_state, reason=operator_drain
Последний выход15.08 ~14:00positions.closed_at, balance_hourly

operator_drain — ручная остановка: это DRAIN_DEFAULT_REASON (crates/pm-engine/src/sse/local_api.rs:47); у автоматического рубильника своя причина circuit_breaker (crates/pm-engine/src/engine/circuit_breaker.rs:73).

Внутри окна — 18 опубликованных версий конфигурации (24–41 из 21 существующей, config.db/strategy_config_versions) и 20 стартов движка (bootstrap_audit): 2–3 позиции на параметрическую эпоху. Версия вступает в силу только при рестарте, и это видно замером: версия 24 (buy_order_ttl_seconds=60) опубликована 15:51:09, старт 15:51:18; версия 25 (180) — 15:54:22, старт 15:54:33; все 118 отказов по сроку GTD лежат строго внутри 15:52–15:54.

Следствие для всей сессии: ни один параметр по этим данным настраивать нельзя — любое сравнение подмножеств будет сравнением эпох. Исключения названы там, где они есть: механизм входа (§6) не менялся ни в одной версии, порог стопа (§5) держался на 0.40 почти всё окно входов.


2. Деньги — два независимых источника сходятся ​

ПоказательЗначениеИсточник
Позиций закрыто42 (38 CLOSED + 4 REDEEMED)positions
Оборот (Σ entry_cost)$190.43positions.entry_cost_micro_usd
Реализованный результат+$1.6193positions.realized_pnl_micro_usd
В долях оборота+0.85 %производное
Изменение баланса кошелька+$1.6260balance_hourly

Сверка баланса независима от учётной книги движка: balance_hourly пишется он-чейн чтением ERC-20 balanceOf pUSD по адресу подписанта через Polygon RPC (crates/pm-engine/src/observability/balance.rs). На обоих концах сравнения книга плоская (1000.167147 стояло с 12.08 02:00 до 15:00; 1001.793159 стоит с 15.08 14:00). Расхождение $0.0067 — округление и газ на redeem.

Причина выходаnСумма PnLСредняя цена входаСредняя цена выхода
exit_target23+$46.700.6940.990
stop_loss_trade_confirmed15−$37.620.6670.300
resolution4−$7.470.7032 по 1.0, 2 по 0

Пиково задействовано ≈ $107 из $1000 (минимум баланса 892.62 в 13.08 08:00) — загрузка ≈11 %.


3. EV-вердикт эталонной модели: положительный, но внутри одной ошибки ​

forensics.sh config-report --engine с живым Gamma; r и s_eff выведены из РЕАЛЬНЫХ стоп-филлов, население очищено (0 пустых condition_id, 0 фантомов; в EV идут 37 позиций с fill-checked входом из 42):

n = 37     w = 0.5946     p_e = 0.6854
r = 0.8000 (12 из 15 проигравших закрыты стопом)
s_eff = 0.3101 (средняя цена ФИЛЛА стопа)     exit_price = 0.99

break_even_winrate w* = 0.5895
edge_margin            = +0.0051
expected_pnl_per_share = +$0.0038
positive_ev            = true

Достаточность: 35 рынков разрешены окончательно (closed=true), 1 предварительный, стопов 15 при пороге инструмента 10.

Как это читать. Запас над стеной безубыточности — 0.5 процентного пункта доли выигрышей при стандартной ошибке w ≈0.081 на n=37. Знак положительный, но лежит внутри одной ошибки: это «модель не запрещает», а не «доходность доказана». Ожидаемые +$0.0038 на долю при среднем размере ≈7 долей дают ≈+$0.027 на позицию, тогда как один эпизод whipsaw (§5) стоил $2–3.5. Масштаб заработка на два порядка меньше масштаба ошибок исполнения — это согласуется с уже установленным: кромка харвестера есть надбавка рынка, и решают охват и чистота исполнения, а не предсказание.


4. Охват: 1.7 корзины на событие ​

рынков, дошедших до пути входа                209  на 106 событиях  (decision_liquidity_profiles)
рынков, дошедших до ворот отбора              206  на 105 событиях  (selection_signals)
   выпущено сигналов                          118
   заветировано ranking-воротами               85   (41 % дошедших)
   дубль сигнала / пропуск по бюджету           3

попыток выставить заявку                     2304  на 126 рынках
позиций реально открыто                        42  на  25 событиях
   событий с 1 корзиной 15 | с 2 — 4 | с 3 — 5 | с 4 — 1

Распределение рынков на событие в decision_liquidity_profiles (1 рынок — 24 события, 2 — 62, 3 — 19, 4 — 1) практически совпадает с selection_signals: до пути входа доходит порядка двух рынков на событие, и сужение произошло ВЫШЕ обеих таблиц.

Какие именно верхние ворота срезали корзины — по этой сессии установить нельзя. Счётчики воронки живут в кольце на 300 тиков; прогон funnel-window 16.08 показывает ring_capacity_each=300 и окно в 300 секунд, то есть данные сессии вытеснены, а текущее состояние — 100 % отсева на стадии drain. Кандидаты по конфигурации (не по замеру): entry_price_band_lower_milli=450 вместе с max_entry_price_milli 650…850 — цена NO вне коридора 0.45–0.75 отсекается с обеих сторон; min_probability_milli=250; окно входа finish_type=GstLocalMidnight.

Это ограничение — предмет уже заведённой работы, см. §9.


5. Стоп: треть срабатываний продала победителя ​

15 стопов забрали $37.62 при $46.70 заработка целевыми выходами. Все 15 реально налились (exit_filled_rate = 1.0000), s_eff = 0.3101 при пороге 0.40 практически на всём окне входов (stop_target=400 в версиях 23–37; 300 с 08:46, 350 с 09:01 — уже после остановки входов). Выборка почти одноэпоховая, n=15 проходит порог инструмента.

Блок stop-quality config-report (пере-разрешение через Gamma):

ПоказательЗначение
whipsaw_rate — стопов, где удерживаемая сторона ВЫИГРАЛА0.3333 (5 из 15)
real_stop_save_rate0.6667
$_booked_on_winners — фактически записанный убыток на победителях−$12.4476
$_saved_on_losers — ВЕРХНЯЯ ОЦЕНКА спасённого−$25.1683

Складывать эти два числа нельзя, и инструмент это подписывает: $12.45 — реально записанные деньги, $25.17 — верхняя оценка, построенная на досиживании до разрешения, чего харвестер не делает (он выходит на ≈0.99). Корректную оценку даёт stop-counterfactual — цена ровно через час после решения против цены решения; результат см. §7.

Показательный случай — позиция 216 (Гонконг, 13.08):

куплено          7.00 долей по 0.71                 = $4.970
стоп продал      5.55 долей по 0.36                 = $1.998
остаток          1.45 доли погашен по 1.00          = $1.450
реализовано                                          −$1.586
если бы держали  7.00 × 1.00                        = $7.000  →  +$2.030
цена решения стопа                                             ≈ $3.5

Недопроданный остаток. У 3 из 4 позиций, доехавших до разрешения, стоп продал только часть, а хвост оказался ниже min_size_shares (5 долей) и стал непродаваемым:

ПозицияПродано стопомОсталосьЧем кончилось
216 (Гонконг)5.551.45погашен по 1.0 — остаток спас часть убытка
217 (Торонто)3.653.35погашен по 0 — $2.25 из $3.60 убытка это остаток
237 (Варшава)4.341.66погашен по 0

dust-exposure --min-size-shares 5000000: 1 dust-позиция из 42, доля незащищённого нотионала по времени 0.0074. Системного масштаба нет, но на отдельной позиции хвост решает больше половины результата, и направление даёт в обе стороны — это неуправляемый остаток, а не односторонний убыток.


6. Вход: пассивная заявка по лучшему биду собирает провалы ​

Механизм не менялся ни в одной из 21 версии конфигурации — единственный вывод, полностью свободный от проблемы эпох:

entry_price_base         = BestBid
entry_price_offset_milli = 0
post_only                = true
ПоказательЗначение
BUY CANCELLED (истекли/сняты)2135, средняя жизнь 94.8 с, с филлом — 5
BUY FILLED37
BUY REJECTED127
Позиций с налитым входом42

37 против 42 — не расхождение: у 5 позиций вход налился частично на заявке, ушедшей затем в CANCELLED (позиции 202, 208, 211, 218, 226).

forensics.sh entry-dip --engine, покрытие 100 % (42 из 42 входов, ни одного класса gap/trunc/no-hist/ERR):

Порог просадки в окне [филл … филл+900 с]Провалов из 42Доля
≥1 цент2559.5 %
≥3 цента (главный замер)1638.1 %
≥5 центов1331.0 %
≥10 центов819.0 %

Медиана просадки — 150 pip. Из 16 провалившихся входов 8 закрыты стопом. Вторичный замер (цена к горизонту 900 с): 14 из 42 (33.3 %) остались ниже цены входа, медиана 50 pip.

Инструмент предупреждает: для отстоявшей LIMIT-заявки, исполнившейся ниже середины, просадка ЗАНИЖЕНА — то есть 38.1 % это пол, а не потолок. Прямая иллюстрация: позиция 200 куплена по 0.58 и остановлена стопом через 2.4 секунды; кадры stop_dip_contexts показывают в этот момент книгу 0.30/0.44.

Когорты liquidity-profile (у «вошли-провалились» денег на лучшем аске $42.9 против $9.34 у «вошли-выжили») указывают туда же, но инструмент печатает «ВЫБОРКА МАЛА, ВЫВОД НЕ ОБОСНОВАН» (18 и 24 позиции при пороге 30). Основанием для порогов не служит.


7. Контрфакт стопа: за час после выхода стоп стоил вшестеро больше, чем спас ​

forensics.sh stop-counterfactual --engine --cid <cid> по каждому из 15 остановленных рынков (прогон по всему инстансу не укладывается в срок: он опрашивает историю цен на каждый кадр, включая досессионные). Все 15 позиций дали ровно ОДИН эпизод класса fire — тот, что привёл к выходу, — поэтому двойного счёта эпизодов одной позиции нет.

Величина Δ$вых = (цена через час − ФАКТИЧЕСКАЯ цена выхода) × размер. Положительная означает, что выход обошёлся в эти деньги: цена вернулась выше той, по которой продали.

эпизодовсумма Δ$вых
выход обошёлся в деньги9+$14.30
стоп спас6−$2.06
итог за час15+$12.23

Два эпизода дают половину всей цены: 0x4601d2a3b6 — выход по 0.3091, через час 0.9995 (+$5.52), и 0xf2a72f9baa — выход по 0.2993, через час 0.9985 (+$3.50). В обоих случаях рынок в течение часа ушёл к почти достоверному исходу В НАШУ пользу, а позиция была продана внизу.

Это закрывает разрыв, оставленный §5. Там были две несопоставимые величины: реально записанный ущерб на победителях −$12.45 (Gamma) и ВЕРХНЯЯ оценка спасённого −$25.17, завышенная по построению, потому что считает досиживание до разрешения. Контрфакт даёт честную встречную величину на том же горизонте: спасено $2.06, а не $25.17. Два независимых замера с разными горизонтами (разрешение рынка и один час) сходятся по знаку и близки по порядку.

Оговорки, без которых цифру читать нельзя:

  • Горизонт — статический час, а не разрешение рынка. Это осознанное свойство замера: харвестер выходит на ≈0.99 и не досиживает, поэтому ride-to-resolution завышал бы убытки.
  • Группировка по сессии — моя, а не инструмента. Он отказывается выносить вердикт группе меньше 10 эпизодов, а прогон по одному рынку даёт 1–7. Здесь 15 эпизодов, то есть порог достаточности выполнен, но собраны они из 15 отдельных прогонов.
  • Знак неустойчив на мелких эпизодах: ряд цен отдаёт середину книги, и там, где |Δ$| меньше полуспреда на размер позиции, «откатился выше» и «ушёл ниже» неразличимы. Четыре из шести «спасённых» эпизодов имеют |Δ$вых| меньше $0.10 и по величине не считаются — только по счёту.
  • Вывод НЕ «убрать стоп». Измерено, что действующий порог на часовом горизонте отдаёт больше, чем удерживает. Куда двигать порог, эта величина не говорит: для этого нужно распределение просадки относительно порога, а не частота срабатывания (см. §11 п. 2).

8. Пауза 13.08 04:09:34 → 08:31:16 ​

Установлено по снимку:

  1. Закрытия 223 (03:08:07, −$2.17) → 225 (03:33:31, −$2.29) → 212 (04:09:34, −$2.32) — три убыточных стопа подряд при consecutive_stop_outs=3; серия считается выборкой ORDER BY p.closed_at DESC, p.id DESC (crates/pm-engine/src/adapters/storage/sqlite.rs:3242).
  2. В ту же секунду 04:09:34, сразу после PositionClosed позиции 212, разом сняты 6 стоявших BUY со сроками 04:10:27…04:12:26 — за 53–172 с ДО истечения. Ранние массовые снятия за всю сессию встречаются ровно трижды: 12.08 16:01:40–41 (за 18 с до старта движка — Reset), 13.08 04:09:34, 13.08 09:02:27–28 (известная ручная остановка).
  3. После 04:09:34 нет ни одного SignalEmitted/OrderPlaced до 08:31:16, при этом рестарта в интервале нет (bootstrap_audit пуст между 03:53:15 и 08:34:49) — значит, не Reset.

Подпись «снять все стоявшие BUY, не перезапускаясь, и перестать входить» принадлежит enter_persistent_drain. Единственный автоматический путь с таким триггером — рубильник по серии стоп-аутов; альтернатива требует, чтобы оператор нажал drain в ту же секунду, в которую закрылся третий стоп. Остаточная неопределённость — только ярлык, и снимает её строка журнала: docker logs pm-engine-harvester-engine | grep "CIRCUIT BREAKER TRIPPED".

Второй порог рубильника (rolling-24h −$6) не сработал ПРАВИЛЬНО: чистый реализованный за окно был −$2.23.


9. Наблюдения, у которых УЖЕ есть заявка ​

Эти пункты сессия подтверждает боевыми числами, но работу по ним заводить не нужно — она заведена. Числа приведены как подтверждающий замер.

Наблюдение сессииЗаявкаГотовность
118 BUY отклонены invalid expiration value…, все в 15:52–15:54 на эпохе buy_order_ttl_seconds=60gtd-expiration-in-future — там установлено больше: биржа отвергает срок GTD короче 3 минут, 60 уходило в провод как 120, отсюда 118 подряд и десять минут без торговли; чинится порогом в pm-sdk плюс полом конфигурации 180 с21/23
reject_reason_code у всех 193 отказов = legacy, unclassifiedorder-reject-taxonomy-observability45/60
Двойной учёт стоимости филла: позиция 218, entry_cost 4 754 050 против orders.filled_value 5 434 050 (PM-ENGINE-39)fill-accounting-idempotency1/30
Отмена заявок при drain записывается как reject_reason='expired', неотличимо от истечения срокаaccounting-drain-shutdown-discrimination31/34
Воронка живёт 300 тиков, разобрать прошедшую сессию нечем (§4)entry-funnel-domain-rollup (панель и подсев истории из памяти) + analytics-telemetry-to-clickhouse (лосслесс-таблица admission_funnel_counters поминутно и selection_verdicts без схлопывания по UNIQUE(condition_id))72/75 и 15/80
Этап наблюдения перед входом на бою не работал (entry_observation_* в схеме есть, строк 0)entry-observation-stage17/26

Замер, разрешающий открытый спор в analytics-telemetry-to-clickhouse (tasks.md §1, последний пункт): docs/target/storage.md утверждает, что слой decision_liquidity_profiles в боевом инстансе не включён и «строк там нет ни одной». В снимке этой сессии их 5091. Документ расходится с боевым замером; спор разрешается в пользу замера.


10. Наблюдения БЕЗ заявки ​

  1. Задержка защитного выхода. 66 SELL отклонены «not enough balance»: 54 с balance: 0 (расчёт долей после филла ещё не прошёл) и 12, где размер заявки больше осевшего баланса — продаём 7.00 при осевших 6.99, 8.00 при осевших 5.00. Средняя задержка до первой не отклонённой целевой SELL — 129 с, максимум 4915 с. Два разных корня: нет ожидания расчёта, и размер выхода берётся из НАМЕРЕНИЯ, а не из фактических filled_shares.
  2. Расхождение накопителя выхода с филлом. Позиция 216: накопитель записал 1 934 060 µUSD за филл, для которого ордер и Fill-событие дают 1 998 000 (5.55 доли по 0.36). $0.064 на одном филле, знак противоположен PM-ENGINE-39. Кандидат в ту же семью — сверять отчётом, не автопроводкой.
  3. Расхождение документа с кодом.docs/operations/alerts-and-monitoring.md перечисляет «воронку входа pm_engine_entry_*» среди экспортируемых семейств. Воронка Prometheus-метрик не имеет вовсе: в crates/pm-engine/src/engine/admission_funnel.rs ни одного вызова counter!/gauge!/histogram!, она живёт только как AdmissionFunnelCounters → SSE-кадр EntryFunnelTick → кольцо на 300 тиков. Живой /metrics 16.08: 34 имени метрик при 130 описанных в observability/metrics.rs (отсутствуют те, что не сработали в текущем процессе).

11. Что делать дальше — порядок и его причина ​

Порядок не косметический: расширять охват ДО починки механизма входа значит умножить число пассивных заявок, которые сносят при движении против нас — 38.1 % входов проваливаются уже сейчас.

0. Протокол замера — до любой правки параметров. Одна эпоха = одна версия конфигурации; во время прогона конфигурация не правится; минимальный объём эпохи объявляется заранее (пороги инструментов — 30 позиций на когорту, 10 стопов); рестарты считаются и попадают в отчёт. Структурная поддержка этого — таблица epochs в analytics-telemetry-to-clickhouse.

1. Механизм входа. Три несовместимых по цене варианта, выбор за оператором: положительный entry_price_offset_milli либо пересечение спреда по подтверждённому сигналу; включение этапа наблюдения (entry-observation-stage); перевод детектора токсичного потока из наблюдения в действие — сейчас toxic_flow.log_only = true при active_cancel_enabled = true.

2. Стоп. Контрфакт снят (§7): на часовом горизонте действующий порог отдал +$12.23 и удержал −$2.06, то есть стоил вшестеро больше, чем спас. Это основание порог ПЕРЕСМОТРЕТЬ, но НЕ основание назначить новый: частота срабатывания не говорит, куда двигать границу. Нужен ещё один замер — распределение величины просадки относительно порога (не частота, а форма). До него любое новое число будет подгонкой под 15 наблюдений.

Независимо от порога и от замера — доводить продажу до конца, не оставляя хвост ниже min_size_shares (§5): на позиции 217 непроданный остаток дал $2.25 из $3.60 убытка.

3. Охват. Сначала наблюдаемость (заявки из §9), потом правка max_entry_price_milli и selection.min_probability_rank.

4. Загрузка капитала. $107 из $1000. Модель даёт «+EV», но с запасом внутри одной стандартной ошибки, а масса усиливает знак EV в обе стороны. Наращивать только после §1 и §2, на данных одной зафиксированной эпохи.


Как перепроверить ​

bash
# целостность набора
cd docs/analysis/data/harvester-engine-session-20260816T144518Z
sha256sum -c SHA256SUMS
sqlite3 'file:engine.db?mode=ro&immutable=1' 'PRAGMA integrity_check;'

# офлайн-часть
FORENSICS_TESTDATA=docs/analysis/data/harvester-engine-session-20260816T144518Z/engine.db \
  scripts/analysis/forensics.sh <positions|stops|liquidity-profile|exit-protection|reject-reasons|rank-window|accounting-preconditions> --engine
# dust-exposure дополнительно требует --min-size-shares 5000000

# живая часть (Gamma / CLOB prices-history, ходит на VPS сама)
scripts/analysis/forensics.sh <config-report|entry-dip|stop-counterfactual> --engine

Сверка денег: positions.realized_pnl_micro_usd против balance_hourly на границах 12.08 14:00 и 15.08 14:00 UTC.