Как у меня НЕ получился торговый бот на Polymarket. Часть 2
Риск-слой, LLM-как-трейдер с инверсной калибровкой и survivorship-free replay к реальной резолюции: почему у favorite-longshot bias нет торгового края, а методология дороже стратегии.В первой части — рынок предсказаний, архитектура, недостижимые пороги, +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 сигмы), и (б) экономически мёртв.
Где честно слабо
Ограничения, к которым я пришёл:
outcomePricesпринимаются как истина без аудита UMA-историй. UMA-резолв исторически имел спорные кейсы (определения «happens», временны́е границы, ручные оверрайды). В моей видимой выборке 0% неоднозначных резолвов — но это потому, чтоclosed=trueпо построению отдаёт только формально резолвнутые, а диспьюты я не аудировал.- Selection bias вселенной. Воид, отменённые и зависшие-нерезолвнутые лонгшоты в
closed=trueне попадают вовсе, и их долю отсюда не видно, ведь лонгшоты снимаются чаще среднего — значит часть вселенной я просто не вижу. Направление биаса, скорее, консервативно к моему выводу: если воиднутые лонгшоты — это в основном несостоявшиеся события (были бы NO), их исключение завышает измеренный q и делает мой анти-FLB-результат ещё пессимистичнее для стратегии, но без аудита пропущенного это догадка. - Earliest-in-band — оптимистичная цена входа. Я вхожу в момент первого касания полосы, но в реальной торговле сигнал был бы позже (увидеть → подтвердить → исполнить), а на illiquid-рынках разница в днях — это другая цена и другой q. Sensitivity по delayed-entry я не гонял, приведённый gross edge — это ВЕРХНЯЯ оценка, лучший случай для стратегии и даже верхняя оценка ≤ 0 после спреда.
- Спред в хвосте занижен референсом. Брейкивен 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. Складываем:
параметрика ─┐
LLM-трейдер ─┼─▶ edge нет
FLB-харвест ─┘
- Не было модели. Формула edge была тавтологией (конвикшен LLM ×0.5), не сравнённой с реальностью.
- «Прибыль» была артефактом измерения. Календарный распад почти решённых рынков по марку маскировался под альфу; survivorship по двум победителям достраивал иллюзию.
- LLM не спасает. Сильная модель не создаёт edge автоматически — v5 дала сигнал инверсной калибровки (на крошечной выборке).
- 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 — это не вайбкодинг ради хайпа, а инструмент. Вся аналитика, замеры, разбор кода и формулировки — мои, но писались и проверялись в диалоге.