ArbScanner: калькулятор ставок и фильтры по дате

Когда смотришь на арбитражный сканер и видишь строчку «Stake on Polymarket: $52.10» — это абсолютно бесполезная информация. Куда ставить? На какой исход? Что будет, если выиграет другая сторона? Сколько это в годовых, если позиция висит до 2027 года? Именно с этих вопросов начался большой рефакторинг калькулятора и системы фильтрации в проекте arbscanner.
В этой статье расскажу, что конкретно было сделано: как из сухой таблички получился нормальный инструмент с пошаговыми инструкциями, таблицей сценариев и умной фильтрацией по дате истечения.
Проблема: калькулятор без смысла
Исходный detail-panel.tsx показывал примерно следующее:
Stake on Polymarket: $52.10
Stake on Kalshi: $47.90
Total: $100.00
Payout: $103.20
Fee: $1.50
Profit: $1.70
На первый взгляд — вся информация есть. Но на практике это бесполезно, потому что:
- Непонятно, на какой исход ставить (Yes или No)
- Непонятно, что произойдёт, если выиграет каждая из сторон
- Нет ссылок на конкретные маркеты
- Нет понимания, когда истекает позиция
- Нет метрики годовой доходности (APY), а это критично для долгосрочных ставок
Вторая проблема была ещё острее. В таблице с арбитражными возможностями спокойно висели ставки с датой истечения в 2027 году. Пользователь видит «профит 1.8%» и думает — отлично. Но не понимает, что капитал будет заморожен на полтора года, а вилка скорее всего перестанет работать за это время по десятку причин: изменится ликвидность, поменяются правила резолюции, один из маркетов уйдёт в паузу.
Про это — отдельный риск. Самая большая угроза в арбитраже на предикшн-маркетах — не движение цены, а matching risk: два рынка могут выглядеть как зеркало, но на деле иметь разные формулировки, разные дедлайны резолюции и разные условия. Никакой калькулятор не защитит от этого автоматически, но хотя бы показывать пользователю всю информацию — обязанность инструмента.
Решение: пошаговый калькулятор со сценариями
Первым делом — полная переделка detail-panel.tsx. Концепция простая: показывать не абстрактные числа, а инструкцию к действию.
Два шага вместо списка
Калькулятор теперь состоит из двух карточек-шагов:
// Шаг 1
<StepCard>
<Platform>Polymarket</Platform>
<Action>Buy NO</Action>
<Amount>$52.10</Amount>
<Detail>Цена контракта: $0.521 × ~100 шт.</Detail>
<Link href={marketUrl}>Открыть маркет →</Link>
</StepCard>
// Шаг 2
<StepCard>
<Platform>Kalshi</Platform>
<Action>Buy YES</Action>
<Amount>$47.90</Amount>
<Detail>Цена контракта: $0.479 × ~100 шт.</Detail>
<Link href={marketUrl}>Открыть маркет →</Link>
</StepCard>Это уже отвечает на вопрос «куда и что». Но самое важное — таблица сценариев.
Таблица сценариев: что будет, если...
Каждая арбитражная позиция имеет два возможных исхода. Мы показываем оба:
┌──────────────────┬────────────┬──────────┬────────────┐
│ Сценарий │ Выплата │ Комиссия │ Нетто │
├──────────────────┼────────────┼──────────┼────────────┤
│ YES выигрывает │ $100.00 │ -$1.50 │ +$1.50 │
│ NO выигрывает │ $103.20 │ -$1.80 │ +$1.40 │
└──────────────────┴────────────┴──────────┴────────────┘
Гарантированный профит (worst case): +$1.40 / ROI: 1.4%
Теперь пользователь видит: независимо от исхода событий, он зарабатывает от $1.40 до $1.50. Это и есть суть арбитража — безрисковый профит. Именно слово «безрисковый» в скобках, потому что matching risk никуда не делся.
Под таблицей — блок с экспирацией, confidence score и предупреждением о рисках.
Фильтр по дате истечения
Вторая большая задача — добавить end_date в весь pipeline от бэкенда до UI.
Бэкенд: прокидываем end_date
Поле end_date уже было в сырых данных с маркетов, но не попадало в тип Opportunity. Исправляем:
// types.ts
export interface Opportunity {
id: string
profit_pct: number
// ... другие поля
end_date: string | null // ISO 8601
market_status: 'active' | 'stale' | 'closing_soon'
apy: number | null
match_type: 'exact' | 'probable' | 'speculative'
max_executable: number | null
capital_lock_days: number | null
}В scan/route.ts — прокидываем end_date из NormalizedMarket в оба метода (pred↔sb и cross-pred):
const opportunity: Opportunity = {
// ...
end_date: marketA.end_date ?? marketB.end_date ?? null,
apy: computeAPY(profitPct, marketA.end_date),
market_status: getMarketStatus(marketA, marketB),
match_type: classifyMatchType(marketA, marketB),
}
function computeAPY(profitPct: number, endDate: string | null): number | null {
if (!endDate) return null
const daysToResolution = Math.max(
1,
(new Date(endDate).getTime() - Date.now()) / 86400000
)
return (profitPct / daysToResolution) * 365
}АPY — это ключевая метрика. Профит 1.4% за 3 дня — это 170% годовых. Профит 1.4% за 500 дней — это 1% годовых. Очевидно, что первое интереснее.
Фильтрация expired маркетов
Параллельно обнаружили баг: в результатах показывались маркеты с уже прошедшей датой окончания. Добавили фильтр на уровне каждого venue-адаптера:
// В каждом venue модуле
function filterExpired(markets: NormalizedMarket[]): NormalizedMarket[] {
const now = new Date()
return markets.filter(m => {
if (!m.end_date) return true // без даты — оставляем
return new Date(m.end_date) > now
})
}UI: колонка Expires и фильтр в сайдбаре
В таблице появилась колонка «Expires» с умным форматированием:
function formatExpiry(endDate: string): string {
const diff = new Date(endDate).getTime() - Date.now()
const days = Math.floor(diff / 86400000)
if (days < 0) return 'Expired'
if (days === 0) return 'Today'
if (days <= 7) return `${days}d`
if (days <= 365) return format(new Date(endDate), 'MMM d')
return format(new Date(endDate), "MMM d ''yy")
}Результат: «3d», «Mar 15», «Jan 5 '27» — сразу понятно, насколько долгосрочная позиция.
В сайдбар добавили фильтр Max Expiry:
Max Expiry: [1 неделя ▼]
• 1 неделя
• 1 месяц
• 3 месяца
• 6 месяцев
• 1 год
• Без лимита
По умолчанию — 3 месяца. Это разумный дефолт: вилка с горизонтом больше 3 месяцев несёт слишком много неопределённости.
Status badges и Match type
Вместе с датами добавили несколько важных визуальных индикаторов.
Market status badges:
STALE— мало ликвидности, bid-ask spread слишком широкийSOON— маркет закрывается меньше чем через 24 часа
Match type badges вместо числового confidence:
✓ exact— формулировки рынков идентичны≈ probable— высокая вероятность совпадения условий? speculative— рынки могут резолвиться по-разному
Это прямо адресует главный риск арбитража на предикшн-маркетах. Видишь ? speculative — читай описания рынков вручную перед входом.
Max Executable и Capital Lock
Две дополнительные метрики, которые сильно влияют на реальную ценность арбитражной возможности:
Max Executable — максимальная сумма, которую можно поставить до исчезновения арба. Считается по глубине ордербука. Смысл: арб на $200 при максимальном исполнении $15 — это не очень интересно.
Capital Lock — сколько дней капитал будет заморожен до резолюции. Вместе с APY это даёт полную картину эффективности использования капитала.
interface Opportunity {
max_executable: number | null // в долларах
capital_lock_days: number | null // количество дней
}В UI эти поля показываются в карточке детали и в тултипе при наведении на строку таблицы.
Деплой и инфраструктура
После реализации — стандартный цикл: npm run build, все 83 теста зелёные, коммит, деплой через Dokploy.
Кстати, обнаружили проблему: GitHub webhook к Dokploy возвращал 401 — refreshToken устарел. Деплой пришлось тригернуть вручную через API:
curl -X POST https://dokploy.pashavin.ru/api/application.deploy \
-H "Authorization: Bearer [СКРЫТО]" \
-H "Content-Type: application/json" \
-d '{"applicationId": "[APP_ID]"}'Статус вернул done, https://arbscanner.pashavin.ru/ — 200 OK. Webhook нужно починить отдельно, обновив токен в настройках.
Git история: путь проекта
За последние недели проект прошёл большой путь. Если смотреть по коммитам:
- Сначала появился базовый сканер с таблицей
- Добавлен калькулятор (тот самый, который мы переделали)
- Появилась история сделок с P&L и цветовой кодировкой
- Добавлен real bid price из CLOB ордербука вместо фейкового mid price
- Отдельные конфиги стратегий для Real и Paper режимов
- Paper trading модуль с виртуальным балансом $1000
- Миграция всех данных в Supabase для выживания между деплоями
- И наконец — переделка калькулятора и система фильтрации по датам
Проект растёт в сторону полноценного торгового инструмента с автоматическим исполнением сделок.
Технологии
Проект построен на стандартном стеке:
- Next.js — фронтенд и API routes в одном приложении
- React — компоненты UI, в том числе detail-panel и таблица
- TypeScript — строгая типизация, что особенно важно при работе с финансовыми данными
- Supabase — хранение позиций и истории торгов, переживает рестарты контейнера
- Dokploy — деплой через Docker на VPS
Выводы
Главный урок этой итерации: информация без контекста — это не информация. «Stake on Polymarket: $52.10» технически верно, но бесполезно. «Купи NO на Polymarket за $52.10, открой этот маркет, если NO выиграет — получишь $103.20 минус комиссию $1.80, нетто +$1.40» — это уже инструкция к действию. Разница огромная, а кода нужно написать не так много.
Второй урок: дата истечения — это не просто поле для отображения. Это ключевой фактор для оценки возможности. Арбитраж с горизонтом год+ — это совсем другой инструмент, чем арбитраж на неделю. APY позволяет сравнивать их по единой метрике и принимать осознанные решения. Без этого числа пользователь просто не может адекватно оценить привлекательность возможности.
Третий урок: matching risk нельзя решить алгоритмически, но можно хотя бы честно показать. Бейджи exact / probable / speculative не защищают от ошибки, но дают пользователю сигнал: вот здесь нужно думать самому. Хороший инструмент не притворяется, что решил проблему, которую не решил.
Четвёртый урок: фильтрация expired маркетов должна быть на уровне бэкенда, а не только на фронте. Показывать просроченные возможности — это не просто UX-баг, это активно вводящая в заблуждение информация. Такие вещи нужно чистить у источника, в каждом venue-адаптере отдельно, потому что каждый источник данных имеет свою специфику форматов дат и edge cases.
Дальше проект движется в сторону автоматического исполнения сделок через Polymarket CLOB API и Kalshi Trading API. Это уже другой уровень сложности — там появляются вопросы идемпотентности, kill switch, управления позициями при частичном исполнении. Но фундамент — понятный калькулятор и честная фильтрация данных — теперь на месте.

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