Как у меня НЕ получился торговый бот на Polymarket. Часть 2

Николай Носков 12 мин

В первой части — рынок предсказаний, архитектура, недостижимые пороги, +9% за два дня и два корня всех проблем: измерение по марку и формула edge, которая оказалась тавтологией. Здесь — три попытки это починить и честный замер, который закрыл вопрос.

6. v4: строим риск-слой и получаем неопровержимую улику

Решил добавить структурный риск-слой, не трогая пороги (коммит 720e642). Три гварда в paper_trading:

daily_loss_limit_abs           = 5.0   # дневной ранний стоп
max_trades_per_market_per_day  = 10    # потолок перезаходов в один рынок
max_same_direction_open        = 10    # анти-корреляционный гвард

v4 не создаёт edge — она снимает маску концентрации, чтобы стала видна истинная ожидаемость движка. Замер v4 (n=288 за ~5 дней), и доказательства её отсутствия:

(1) Та же ставка — перевёрнутый знак. Тот самый иранский рынок:

US x Iran peace by May 31:
  v3:  +$10.55,  winrate 94.6%
  v4:  −$2.80,   winrate 16.7%

Тот же рынок и тот же движок, а знак перевернулся за дни.

В v3 он ловил удачу распада, в v4 потолок перезаходов это убрал, и «навык» испарился. Был бы это edge — он бы не зависел от того, разрешил ли я заходить 371 раз или 10: edge от количества заходов не зависит, а сбор премии распада — да.

(2) Бимодальный winrate. Рынки кучкуются на 62–100% ИЛИ 0–17%. Ни одного в зоне «навыка» 45–58%. Это лотерея направления дрейфа, а не предсказание.

(3) Профит снова концентрирован: 2 рынка (Spurs +$3.64, Iran-2027 +$1.56) = +$5.2, весь остальной стакан = −$2.32.

Неожиданный прикол — иллюстрация закона Литтла: потолок перезаходов срезал rate входов ~в 9 раз, и среднее число открытых позиций упало с 16 до 3. «16 открытых» в v3 были не диверсификацией.

Вывод: параметрическая глава закрыта. Никакая настройка порогов, направления или размера не создаёт edge — проблема выше по течению, в фейковой формуле.

7. v5: «Отдадим всё LLM» и нарвёмся на инверсную калибровку

Логичный шаг: убрать фейковую формулу совсем. v5 (коммит ea1847a) — параметрический торговый код выходит на пенсию (signal_engine и paper_trading — inert no-op), а ВСЕ решения (открыть/закрыть/держать/изменить, сторона, размер, риск) принимает LLM-трейдер. Ноль кодовых риск-лимитов, единственный инвариант — не заполнять сделку по невалидной цене, это защита от мусорного PnL. Каждое решение логируется в новую таблицу llm_decisions.

Первый же поведенческий сигнал (n=5) был ну прям дежавю: LLM сам, за часы, переоткрыл ловушку распада. Все 4 позиции NO на низковероятных рынках (p0 = 0.295, 0.155, 0.152, 0.008). Позиция по 0.008 — это максимум +$0.08 против −$9.92 риска! Справедливости ради, одно его решение было корректным hold с формулировкой «цена уже у пола, edge нет». То есть распознать ловушку он МОЖЕТ, просто непоследовательно. Ну прям как я!

Замер v5 показал инверсную калибровку: в зоне core (p0 0.15–0.85) рынок шёл против решений модели ~70% времени. В полосе максимальной уверенности [0.70, 0.85] — 1 из 10 благоприятных на +1ч и 0 из 9 на +4ч. То есть модель ошибалась сильнее всего там, где была увереннее всего.

Честная оговорка, без которой раздел разваливается. Я только что (раздел 4) объявил: мерить надо ИСХОД, а не марк или дрейф на +Nч, — и тут же строю вердикт v5 ровно на горизонтах +1ч/+4ч. То есть на марках, тот же самый корневой баг измерения.

Поэтому вот что:

  • n=18 направленных решений — это не выборка для калибровочного утверждения. На 18 точках любой паттерн чувствителен к 1-2 рынкам. «p ≈ 0.2%» формально считается, но статистически это сигнал, а не доказательство: для устойчивого вывода нужен n ≥ 100.
  • Разбивку по рынкам я привести не могу — серверную БД v5 я стёр до того, как осознал важность пересчёта к исходам. Тут так: если те 18 решений были по 3-5 рынкам, я мерил удачу на этих рынках, а не калибровку модели. Это ограничение, и я его не закрываю :(
  • Поэтому снижаю риторику: v5 дала сигнал инверсной калибровки на марк-горизонтах, согласующийся с последующим честным замером, однако сам по себе он недостаточен. Чистый вердикт дал только replay к резолюции в v6, и именно осознание слабости v5-замера заставило меня построить v6 правильно.

Заголовочная equity v5 ($85.66 → $108.33 за неделю) — шум марка на n=18.

8. v6: последняя честная гипотеза и как её правильно убить

Я смешивал два утверждения:

  • (A) «модель умеет прогнозировать» — опровергнуто;
  • (B) «входы fade-the-longshot, к которым вёл кламп, имеют реализованный edge» — а вот это к резолюции ни разу чисто не мерилось.

FLB — реальная вещь из литературы! Может, иранские +$10.55 были настоящим харвестом FLB, просто я случайно его собирал? Два победителя — это survivorship, не доказательство, это надо проверить чисто!

Математика всё решает, ибо царица наук

Продать YES-контракт по цене p = взять сторону NO. Выплата: +p, если резолв NO (вероятность 1−q), и −(1−p), если YES (вероятность q), где q — эмпирическая частота срабатывания лонгшота. Ожидание на контракт:

EV = (1−q)·p + q·(−(1−p)) = p − q

+EV тогда и только тогда, когда p > q. То есть лонгшоты систематически ПЕРЕоценены. Чистый тайм-распад — мартингейл, в ноль на справедливой книге. Edge ограничен размером мисприсинга и ОБЯЗАН побить спред! Ну обязан же…

«Починим правилами, чтобы увернуться от хвоста» — самоподрыв: отфильтровать «новостные» лонгшоты = протащить обратно claim-A («модель умеет прогнозировать новости»), уже мёртвый.

Survivorship-free replay

Офлайн-харнес app/research/flb_replay.py (коммит 04a706a) не ходит в БД и ничего не пишет — только читает публичный API. Реальная воронка (числа из реального прогона):

2100  закрытых рынков, вытянутых из Gamma (closed=true, по убыванию объёма)
1202  бинарных Yes/No
1202  резолвнулись чисто в ~0/1   (ambiguous: 0, malformed: 0)
1176  с YES-токеном, датой резолва и объёмом ≥ $10k
 400  топ по объёму — прогон сэмплил их (max_markets)
  48  имели тик цены в полосе [0.05, 0.15] с runway (7, 90]d  → сработали

Для каждого сработавшего: вход — первая точка в полосе (ex-ante, исход не подсматривается), скоринг — к РЕАЛЬНОЙ резолюции из outcomePrices. Чистые функции покрыты юнит-тестами (tests/unit/research/test_flb_replay.py): парсинг исходов, ex-ante вход без look-ahead, тождество mean_pnl == p̄ − q при нулевом спреде.

Базовый якорь: по всем 1202 бинарным рынкам безусловный YES-rate = 318/1202 = 26.5%. Триггер «лонгшот» опускает q до 10–21% — то есть он действительно отбирает низковероятные рынки, как задумано. Ну это я хотя бы не наврал!

Три прогона — и приговор

прогон вселенная / вход n p̄ (цена) q (выбило YES) gross edge @ спред 2c
исходный ликвид, earliest-in-band 48 0.104 0.208 −0.105 −0.11
stable ликвид, сидит в полосе ≥14д 52 0.081 0.096 −0.015 −0.025
tail ликвид, vol $2k–50k 58 0.111 0.103 +0.008 −0.002

Что тут вообще:

  • Ликвид — анти-FLB. Лонгшоты по ~10% выбивали YES в 20.8%. То есть они НЕДОоценены, а не переоценены. Тут фейд теряет −0.105 на контракт, а ликвид — единственное место, где можно реально разместить капитал.
  • Фильтр стабильности работает как сигнал, но приземляется на справедливую цену. Требование «цена сидела в полосе ≥14 дней» (это стилман моего же v6-правила про календарный распад без новостей) срезало q с 0.208 до 0.096 — вычистило climbers и news-волатильность. Фильтр молодчик, короче! Но q (9.6%) ≈ p̄ (8.1%): премии для «страховщика» под спокойными лонгшотами нет, цена справедливая.
  • Остаток FLB живёт в illiquid-хвосте (p̄ 11.1% > q 10.3%) — ровно как предсказывает теория (шарпам невыгодно чинить мелкие рынки). Но он (а) статистически ноль (при n=58 σ(q) ≈ 0.039, edge 0.0078 ≈ 1/5 сигмы), и (б) экономически мёртв.

Где честно слабо

Ограничения, к которым я пришёл:

  1. outcomePrices принимаются как истина без аудита UMA-историй. UMA-резолв исторически имел спорные кейсы (определения «happens», временны́е границы, ручные оверрайды). В моей видимой выборке 0% неоднозначных резолвов — но это потому, что closed=true по построению отдаёт только формально резолвнутые, а диспьюты я не аудировал.
  2. Selection bias вселенной. Воид, отменённые и зависшие-нерезолвнутые лонгшоты в closed=true не попадают вовсе, и их долю отсюда не видно, ведь лонгшоты снимаются чаще среднего — значит часть вселенной я просто не вижу. Направление биаса, скорее, консервативно к моему выводу: если воиднутые лонгшоты — это в основном несостоявшиеся события (были бы NO), их исключение завышает измеренный q и делает мой анти-FLB-результат ещё пессимистичнее для стратегии, но без аудита пропущенного это догадка.
  3. Earliest-in-band — оптимистичная цена входа. Я вхожу в момент первого касания полосы, но в реальной торговле сигнал был бы позже (увидеть → подтвердить → исполнить), а на illiquid-рынках разница в днях — это другая цена и другой q. Sensitivity по delayed-entry я не гонял, приведённый gross edge — это ВЕРХНЯЯ оценка, лучший случай для стратегии и даже верхняя оценка ≤ 0 после спреда.
  4. Спред в хвосте занижен референсом. Брейкивен illiquid-прогона = 2 × gross = 2 × 0.0078 ≈ 0.0156 (1.6c). В модели платишь полспреда на входе и держишь до резолюции, без спреда на выходе, а на лонгшотах с объёмом $2–10k реальный bid-ask чаще 8–20c плюс слиппедж от собственного ордера. Вот вам точная формулировка: edge мёртв при любом реалистичном спреде — точка безубыточности 1.6c, а такого спреда на этих рынках не бывает.

v6 не построен. Закрыт ДО единой строчки торгового кода.

9. Технические засады в API Polymarket

Несколько вещей, на которых я потерял время:

  • Gamma кодирует списочные поля как JSON-строки. outcomes, outcomePrices, clobTokenIds приходят строками вида '["Yes", "No"]'. Нужен ручной json.loads. Прям как в нулевых.
  • Gamma отдаёт 422 на большом offset. Это конец пагинации, не ошибка! Ловите и останавливайтесь.
  • CLOB /prices-history режет окно ~14 днями независимо от fidelity. interval=max/1d/1w для тестовых токенов возвращал пусто. Лечится чанкингом по 13 дней со склейкой и дедупом по timestamp. Голый except: return это глушил и давал «0 сработало».
  • Offset-пагинация упирается в ~2100 строк на направление. Mid-tail (vol $2k–50k) сидит МЕЖДУ «верхними 2100» и «нижними 2100» и недостижим через offset. Достаётся серверным фильтром volume_num_min / volume_num_max.

10. Почему это не работает: синтез

Три независимые главы — параметрика (v1–v4), LLM-как-трейдер (v5), FLB-харвест (v6) — дали no/negative edge. Складываем:

  1. Не было модели. Формула edge была тавтологией (конвикшен LLM ×0.5), не сравнённой с реальностью.
  2. «Прибыль» была артефактом измерения. Календарный распад почти решённых рынков по марку маскировался под альфу; survivorship по двум победителям достраивал иллюзию.
  3. LLM не спасает. Сильная модель не создаёт edge автоматически — v5 дала сигнал инверсной калибровки (на крошечной выборке).
  4. FLB неторгуем. Там, где ликвидно — его нет или он обратный. Где он шепчет (хвост) — он ниже стоимости транзакции.

И сразу — чего я НЕ доказал. «Рынок эффективен» — утверждение сильнее моих данных. Я показал ровно следующее: в той части рынка, где можно разместить капитал, методы, которые я пробовал (predictive-бот на новостях + ex-ante FLB-харвест), edge не дают, но это не исключает других ниш, которые мой опыт просто не покрывает:

  • resolution arbitrage — LLM парсит точные условия UMA-резолва и ловит расхождение буквы условия с тем, как рынок его понимает;
  • cross-market coherence — арбитраж логически связанных рынков (если A⇒B, цены должны быть согласованы, и если нет — есть бесплатные деньги);
  • market making на широких спредах в хвосте.

Их держите отдельно от моего вывода — я их не трогал.

11. Что я вынес: методология дороже стратегии

Стратегии у меня нет, а вот метод — есть. И он, по-моему, ценнее самой стратегии:

  • Никогда не верь заголовочному PnL. Первое — разбивка по концентрации (по рынкам и по полосам цены входа). Один-два рынка, делающие весь результат, — красный флаг survivorship. Если бы я в первый день после +9% не пошёл в разбивку, я бы реально публиковал статью о «успехе».
  • Измеряй ИСХОД, а не отметку. Единственная честная метрика — survivorship-free replay к реальной резолюции, так как марки и +Nч-дрейф врут систематически в одну сторону.
  • Перед настройкой порогов измерь распределение того, что они ловят. Если бы я это сделал в v1, не было бы v2.
  • Различай «баг измерения» и «отсутствие edge». Я честно смешал их в v3/v4 (и в v5) и потерял время. Чистый тест каждой гипотезы по отдельности экономит недели.
  • Negative result — это результат. Четыре гипотезы закрыты, и могу написать об этом статью.

Контрфактическое

Если бы я остановился на втором дне, опубликовал «+9% за два дня», вечером того же дня прочитал статью про favorite-longshot bias и решил: «Ого, я случайно делаю именно это! Бесплатные деньги!» — я поверил бы в свой edge и, блин, вполне возможно, занёс бы на этот рынок реальные деньги, а через две недели потерял бы их на флипе иранского рынка после первой реальной новости.

Защищает одна привычка: разбей PnL и измерь к исходу, прежде чем поверить себе.

Эта статья — лог того, как эта привычка четыре раза спасала меня от собственного оптимизма.

12. Масштаб и репозиторий

Фейковый капитал — стартовые $100, ноль реализованного edge на выходе.

Расход на LLM-токены — $20, из них $13 — Claude Sonnet 4.6 по $2.40/Mtok (трейдер-LLM). Остальное — ChatGPT 5.1 для оценки релевантности новостей.

Это 20% от paper-капитала, ушедшие на инференс ради количественного нуля. Если читать как unit economics реального проекта — даже идеальная стратегия с 20% APR на $100 капитала просто окупила бы токены и не принесла бы ничего сверху.

Одностишная мораль для подобных проектов: слил $20 на LLM, чтобы количественно показать ноль на бумажных $100.

Открыты: код, журнал стратегий с замерами по каждой версии (docs/STRATEGY_JOURNAL.md), полный отчёт по FLB pre-mortem (docs/research/flb_pre_mortem.md) и воспроизводимый харнес. Цифры из раздела 8 воспроизводятся командой:

python -m app.research.flb_replay     # нужен только Python + интернет, ни БД, ни Docker

Навигация по эволюции:

3ed8777  начальный конвейер (v1/v2)
4c2d98d  v3: пороги по измеренной амплитуде
720e642  v4: структурный риск-слой
ea1847a  v5: pivot на LLM-as-trader
c9722f3  v5: скорер (калибровка + edge)
04a706a  v6: survivorship-free FLB replay

Проект разрабатывался в плотной паре с Claude Code — это не вайбкодинг ради хайпа, а инструмент. Вся аналитика, замеры, разбор кода и формулировки — мои, но писались и проверялись в диалоге.