Тема
Разбор торговой сессии 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:18 | events.ts_ms min |
| Первая позиция открыта | 12.08 15:59:44 | positions.opened_at min |
| Пауза (§8) | 13.08 04:09:34 → 08:31:16 | events |
| Последняя позиция открыта | 13.08 08:58:33 | positions.opened_at max |
| Остановка входов оператором | 13.08 09:02:26 | entry_halt_state, reason=operator_drain |
| Последний выход | 15.08 ~14:00 | positions.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.43 | positions.entry_cost_micro_usd |
| Реализованный результат | +$1.6193 | positions.realized_pnl_micro_usd |
| В долях оборота | +0.85 % | производное |
| Изменение баланса кошелька | +$1.6260 | balance_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_target | 23 | +$46.70 | 0.694 | 0.990 |
stop_loss_trade_confirmed | 15 | −$37.62 | 0.667 | 0.300 |
resolution | 4 | −$7.47 | 0.703 | 2 по 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_rate | 0.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.55 | 1.45 | погашен по 1.0 — остаток спас часть убытка |
| 217 (Торонто) | 3.65 | 3.35 | погашен по 0 — $2.25 из $3.60 убытка это остаток |
| 237 (Варшава) | 4.34 | 1.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 FILLED | 37 |
| BUY REJECTED | 127 |
| Позиций с налитым входом | 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 цент | 25 | 59.5 % |
| ≥3 цента (главный замер) | 16 | 38.1 % |
| ≥5 центов | 13 | 31.0 % |
| ≥10 центов | 8 | 19.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
Установлено по снимку:
- Закрытия 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). - В ту же секунду 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 (известная ручная остановка). - После 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=60 | gtd-expiration-in-future — там установлено больше: биржа отвергает срок GTD короче 3 минут, 60 уходило в провод как 120, отсюда 118 подряд и десять минут без торговли; чинится порогом в pm-sdk плюс полом конфигурации 180 с | 21/23 |
reject_reason_code у всех 193 отказов = legacy, unclassified | order-reject-taxonomy-observability | 45/60 |
Двойной учёт стоимости филла: позиция 218, entry_cost 4 754 050 против orders.filled_value 5 434 050 (PM-ENGINE-39) | fill-accounting-idempotency | 1/30 |
Отмена заявок при drain записывается как reject_reason='expired', неотличимо от истечения срока | accounting-drain-shutdown-discrimination | 31/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-stage | 17/26 |
Замер, разрешающий открытый спор в analytics-telemetry-to-clickhouse (tasks.md §1, последний пункт): docs/target/storage.md утверждает, что слой decision_liquidity_profiles в боевом инстансе не включён и «строк там нет ни одной». В снимке этой сессии их 5091. Документ расходится с боевым замером; спор разрешается в пользу замера.
10. Наблюдения БЕЗ заявки
- Задержка защитного выхода. 66 SELL отклонены «not enough balance»: 54 с
balance: 0(расчёт долей после филла ещё не прошёл) и 12, где размер заявки больше осевшего баланса — продаём 7.00 при осевших 6.99, 8.00 при осевших 5.00. Средняя задержка до первой не отклонённой целевой SELL — 129 с, максимум 4915 с. Два разных корня: нет ожидания расчёта, и размер выхода берётся из НАМЕРЕНИЯ, а не из фактическихfilled_shares. - Расхождение накопителя выхода с филлом. Позиция 216: накопитель записал 1 934 060 µUSD за филл, для которого ордер и Fill-событие дают 1 998 000 (5.55 доли по 0.36). $0.064 на одном филле, знак противоположен PM-ENGINE-39. Кандидат в ту же семью — сверять отчётом, не автопроводкой.
- Расхождение документа с кодом.
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 тиков. Живой/metrics16.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.