ArbScanner: калькулятор, фильтры и торговый движок

Если ты когда-нибудь смотрел на таблицу арбитражных возможностей и думал «окей, вилка найдена, но что конкретно мне делать?» — ты понимаешь, с чего всё началось. ArbScanner умел находить расхождения между Polymarket, Kalshi и PredictIt, но дальше начиналась ручная работа: считай в голове, куда ставить, сколько, на какой исход, какой профит в худшем сценарии. Плюс в таблице спокойно висели вилки с экспирацией в 2027 году — то есть деньги придётся морозить на год+, и никакого способа это отфильтровать не было.
Мы это починили. А заодно заложили фундамент для автоматической торговли.
Что было не так с калькулятором
Старый detail-panel.tsx был честным, но бесполезным. Он показывал примерно вот что:
Stake on Polymarket: $52.10
Stake on Kalshi: $47.90
Total: $100
Payout: $104.30
Profit: $4.30
На первый взгляд — вся информация есть. На практике — ты не знаешь:
- На какую сторону ставить (Yes или No)?
- Сколько контрактов купить по какой цене?
- Что произойдёт, если выиграет исход A? А если B?
- Какой реальный ROI с учётом комиссий обеих платформ?
- Есть ли ссылка на конкретный маркет, чтобы не искать вручную?
При работе с реальными деньгами эти вопросы критичны. Ошибиться со стороной ставки — значит удвоить риск вместо хеджа.
Новый калькулятор: пошаговая инструкция
Переделали панель с нуля. Теперь она строится вокруг двух карточек-шагов и таблицы сценариев.
Шаг 1 и Шаг 2 — по одному на каждую платформу:
// Структура шага в новом detail-panel.tsx
interface StepCardProps {
step: number;
platform: string;
side: 'YES' | 'NO'; // явно указываем сторону
stake: number; // сумма ставки в долларах
contractPrice: number; // цена одного контракта
contractCount: number; // stake / contractPrice
marketUrl: string; // прямая ссылка на маркет
fee: number; // комиссия платформы
}Пользователь видит: «Шаг 1: зайди на Polymarket, купи YES, потрать $52.10 = 100 контрактов по $0.52, ссылка вот». Никакой двусмысленности.
Таблица сценариев — главное нововведение:
| Сценарий | Выплата | Комиссия | Нетто-профит | |---|---|---|---| | YES выигрывает | $100.00 | $3.20 | +$4.70 | | NO выигрывает | $100.00 | $2.80 | +$4.30 |
Внизу — гарантированный профит (worst case из двух сценариев) и ROI. Теперь человек видит, что в любом из исходов он в плюсе — это и есть суть арбитража.
Дополнительно добавили блок рисков прямо в панель: дата экспирации, уровень confidence матча, предупреждение если маркеты могут резолвиться по разным правилам.
Проблема matching/rules risk
Вот что важно понимать про арбитраж между предикшн-маркетами. Кажется, что если Polymarket говорит «No» стоит $0.48, а PredictIt за того же кандидата даёт $0.52 — это чистая вилка. Но это не всегда так.
Формулировки рынков могут отличаться. Один маркет может быть про «официального номинанта», другой — про «победителя праймериз». Дедлайны могут не совпадать. Один маркет может уже войти в странное состояние ликвидности или settlement delay.
Поэтому в калькулятор добавили match_type с тремя градациями:
- exact (✓) — одинаковые условия резолюции, одинаковые дедлайны
- probable (≈) — очень похожие, но есть нюансы
- speculative (?) — формально похоже, но проверяй вручную
Это не устраняет риск полностью, но делает его видимым.
Фильтрация по дате экспирации
Вторая крупная задача — прокинуть end_date из маркетов в интерфейс.
Данные об экспирации были в ответах API всех платформ, но до фронтенда не доходили. Тип Opportunity в types.ts не содержал end_date. Исправили в два слоя.
Бэкенд — в scan route добавили маппинг:
// scan/route.ts — до фикса
const opportunity: Opportunity = {
id: `${polyMarket.id}_${kalshiMarket.id}`,
spread: calculateSpread(polyPrice, kalshiPrice),
// end_date отсутствовал
};
// После
const opportunity: Opportunity = {
id: `${polyMarket.id}_${kalshiMarket.id}`,
spread: calculateSpread(polyPrice, kalshiPrice),
end_date: polyMarket.end_date ?? kalshiMarket.end_date ?? null,
days_to_expiry: getDaysToExpiry(polyMarket.end_date),
};Отдельно добавили хелпер для инференса даты из текста вопроса — Polymarket иногда не возвращает end_date явно, но в названии маркета написано «...by November 2026»:
// Инferring end_date из текста вопроса
function inferEndDateFromQuestion(question: string): string | null {
const yearMatch = question.match(/\b(202[4-9]|203\d)\b/);
if (yearMatch) {
return `${yearMatch[1]}-12-31`; // консервативная оценка
}
return null;
}Фронтенд — новая колонка «Expires» в таблице с умным форматированием:
3d— если до экспирации меньше неделиMar 15— если в текущем годуJan 5 '27— если в следующем году и дальше
Фильтр «Max Expiry» в сайдбаре с вариантами: 1 неделя / 1 месяц / 3 месяца / 6 месяцев / 1 год / без ограничений. По умолчанию — 3 месяца, чтобы вилки на 2027 год не захламляли экран.
Также добавили сортировку по дате экспирации — иногда удобнее начать с тех возможностей, которые закрываются скоро.
APY вместо голого процента
Профит 2% звучит по-разному в зависимости от того, на сколько заморожены деньги. Две недели — это 52% годовых. Год — это просто 2%. Добавили колонку APY:
function calculateAPY(netPct: number, daysToExpiry: number): number {
if (daysToExpiry <= 0) return 0;
return (netPct / daysToExpiry) * 365;
}Теперь таблица sortable по APY, и это меняет картину. Вилка на 3 дня с профитом 1.5% выглядит привлекательнее вилки на 180 дней с профитом 4% — когда видишь, что первая даёт 180% годовых, а вторая — 8%.
Фильтрация устаревших маркетов
Отдельно почистили мусор. Маркеты, у которых end_date < now, фильтруются на уровне venue — ещё до попадания в список opportunities. Это касается всех четырёх источников данных. Expired маркеты больше не показываются вообще.
Добавили статусные бейджи:
- STALE — мало ликвидности, цены могут не исполниться
- SOON — менее 24 часов до закрытия, нужно действовать быстро
И колонку Max $ — максимальный размер ставки исходя из доступной ликвидности в ордербуке. Это предотвращает ситуацию, когда калькулятор показывает «поставь $500», а в стакане есть только $80.
Торговый движок: от сканера к боту
После того как сканер стал действительно информативным, встал следующий вопрос: а почему нажимать кнопки вручную? Посмотрели на кейсы из исследований — боты на Polymarket зарабатывают реальные деньги. Бот на базе Claude заработал $14K за 48 часов. Аноним с whale tracker вытащил $75K за день на политическом маркете.
Решили строить execution layer. Но сразу с прицелом на бюджеты $10K+, а не на игрушечные $100. Это меняет архитектурные требования принципиально:
- При мелких суммах можно держать логику в Next.js API routes
- При $10K+ latency стоит реальных денег: незаполненная нога на $5K — это потеря
- Нужны persistent WebSocket соединения к CLOB, а не REST polling
- Нужен kill switch, который останавливает торговлю мгновенно, а не через минуту
- Partial fill на одной ноге при больших суммах — штатная ситуация, которую нужно обрабатывать
Выбрали Approach B — отдельный Trading Engine на Node.js, который общается с UI через SSE (Server-Sent Events). Next.js остаётся фронтендом и контрольной панелью, Python-сервис — мозгом торговли.
Текущий статус — реализован paper trading модуль: симуляция стратегий на виртуальном балансе $1000. Все стратегии можно включать/выключать из UI, настраивать sizing и минимальную ликвидность. История сделок с P&L, цветовая кодировка результатов. Все данные хранятся в Supabase — переживают редеплои.
Git-история показывает, насколько активно шла работа:
- Исправлен
condition_idмаппинг для резолюции сделок - Добавлена дедупликация opportunities
- Починен race condition в daily spend limit
- CLOB bid price вместо синтетической mid-price
- PredictIt $850 liquidity cap в калькуляторе
Инфраструктура деплоя
Деплой через Dokploy на arbscanner.pashavin.ru. Небольшое приключение случилось с вебхуком GitHub → Dokploy — он возвращал 401, потому что refresh token устарел. Пришлось тригернуть деплой вручную через API:
curl -X POST https://dokploy.pashavin.ru/api/application.deploy \
-H "Authorization: Bearer [СКРЫТО]" \
-H "Content-Type: application/json" \
-d '{"applicationId": "arbscanner"}'После этого статус вернул done, https://arbscanner.pashavin.ru/ отвечает 200, API возвращает end_date в opportunities. Вебхук нужно починить отдельно — обновить токен.
Технологии и ссылки
В проекте используются:
- Polymarket CLOB API — для получения цен и исполнения ордеров
- Kalshi Trading API — второй основной источник маркетов
- Next.js App Router — фронтенд и API routes
- Supabase — хранение позиций и истории сделок
- Dokploy — деплой и оркестрация
Итог и что дальше
За несколько итераций сканер превратился из «вот таблица цифр» в инструмент, которым реально можно пользоваться. Калькулятор теперь отвечает на конкретный вопрос: что именно делать прямо сейчас, шаг за шагом. Фильтр по дате убирает шум — маркеты с экспирацией через год не засоряют экран. APY колонка меняет приоритизацию: быстрые маленькие вилки часто лучше долгих больших.
Но самое важное — это архитектурное решение строить execution layer с расчётом на масштаб. Много проектов такого рода начинают с «сделаем просто» и потом переписывают всё, когда деньги становятся серьёзными. Мы сразу заложили отдельный trading engine, Supabase для персистентности, kill switch как первоклассную фичу.
Paper trading сейчас работает в продакшне и собирает данные. Следующий шаг — live trading с маленьким реальным бюджетом, верификация что execution работает как ожидается, потом масштабирование. Whale tracking и AI-estimates из исследовательского плана остаются в очереди — они дают дополнительный сигнал, но execution без них уже работает.
Главный урок: арбитраж на предикшн-маркетах выглядит просто на графиках, но дьявол в деталях — matching risk, liquidity depth, settlement timing. Инструмент должен делать эти детали видимыми, а не прятать их за красивым числом "профит 3.2%".

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.