Skip to content

Add FastAPI web version of the crypto arbitrage scanner#3

Open
devin-ai-integration[bot] wants to merge 17 commits into
mainfrom
devin/1778441192-web-version
Open

Add FastAPI web version of the crypto arbitrage scanner#3
devin-ai-integration[bot] wants to merge 17 commits into
mainfrom
devin/1778441192-web-version

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot commented May 10, 2026

Summary

Веб-версия скрипта scanner.py на FastAPI + ванильный JS дашборд с интерактивными улучшениями UI.

Бэкенд

  • app/scanner_core.py — рефакторинг оригинального scanner.py в библиотеку: те же парсеры тикеров, нормализация сетей, проверка депозитов/выводов и анализ стаканов, но без print-ов, с настраиваемым ScannerConfig (прокси, мин. объём, диапазон спреда, ключи API).
  • app/main.py — FastAPI:
    • GET /api/state — последний результат скана + флаг паузы + размер истории.
    • POST /api/scan — ручной запуск с переопределением параметров.
    • POST /api/pause, POST /api/resume — управление фоновым циклом.
    • GET /api/stats?min_spread=&min_profit=&limit=&require_transfer=&pair= — лог арбитражных событий с фильтрами.
    • DELETE /api/stats — очистка истории.
    • GET /healthz.
    • Фоновый цикл SCANNER_REFRESH_INTERVAL (по умолч. 60c). Каждое завершение скана пишет арб-строки в кольцевой буфер (SCANNER_HISTORY_LIMIT, по умолч. 1000 событий).

Фронтенд

  • Тёмный дашборд с двумя вкладками: Арбитраж и Статистика.
  • Кликабельные ссылки на биржи: имена бирж в столбцах «Купить»/«Продать» — это ссылки на спот-страницу пары на бирже (target=_blank). Шаблоны URL для всех 13 поддерживаемых бирж.
  • Пауза/Возобновление: тоггл в топбаре управляет фоновым циклом (ручной скан доступен и в режиме паузы). Отдельная кнопка «↻ Сейчас» для немедленного запуска с текущими параметрами.
  • Сворачиваемые блоки «Параметры сканирования» и «Сводка». Состояние свёрнутости каждого блока запоминается в localStorage (scanner.collapse.*). Активная вкладка также сохраняется (scanner.ui.tab).
  • Вкладка статистики: интерактивные пороги (по умолчанию мин. спред 2%, мин. профит $20), поиск пары, фильтр D+/W+, лимит строк. Фильтрация на сервере, сортировка на клиенте.
  • Все настройки через env: SCANNER_PROXY, SCANNER_MIN_VOLUME, SCANNER_MIN_SPREAD/MAX_SPREAD, SCANNER_HISTORY_LIMIT, MEXC_API_KEY/MEXC_SECRET_KEY, BINANCE_*, BYBIT_* и т.д.

Запуск из PyCharm

  • run.py + .idea/runConfigurations/Web_Scanner.xml — правый клик → ▶.
  • run.py подгружает .env (через python-dotenv) и печатает диагностику ключей: [run.py] env vars: KEY=set(N) / missing.

Дашборд: http://localhost:8000.

Review & Testing Checklist for Human

  • Открыть http://localhost:8000 — таблица заполняется, имена бирж в строках кликабельные и открывают пару на бирже в новой вкладке.
  • Нажать «⏸ Пауза» — статус индикатора становится жёлтым «На паузе», авто-скан останавливается. «↻ Сейчас» при этом всё равно запускает скан вручную.
  • Свернуть оба верхних блока — таблица занимает почти всё окно. Перезагрузить страницу — состояние свёрнутости сохраняется (localStorage).
  • Переключиться на вкладку «Статистика», менять пороги (мин. спред / мин. профит) — таблица фильтруется на лету. Кнопка «Очистить историю» обнуляет буфер.
  • При наличии ключей MEXC_API_KEY/BINANCE_API_KEY/BYBIT_API_KEY проверить, что статусы D/W и список сетей отображаются (а не ?).

Notes

Старая ветка devin/1778304462-web-interface содержала упрощённый прототип (только Binance, 1Hz polling) — она НЕ использовалась, эта PR делает полноценную веб-версию текущего scanner.py (1358 строк). Логика парсинга/расчётов 1:1 повторяет исходный скрипт; добавлены только: вынос print в logging, dataclasses для сериализации, конфиг через env, фоновый цикл, UI и pause/stats-функционал.

Link to Devin session: https://app.devin.ai/sessions/34d6fae82e224fc3b16a8de5edd04b42
Requested by: @evgetos

Refactors the multi-exchange arbitrage logic from the standalone
scanner.py into a reusable async library and ships a FastAPI
application with a dashboard, REST endpoints and a background
scanner loop.

- app/scanner_core.py: ticker, deposit/withdraw and orderbook logic
  from the original script, configurable via dataclass.
- app/main.py: FastAPI app with /api/state, /api/scan, /healthz and
  a static dashboard.
- app/static/: dark-themed dashboard with summary metrics, sortable
  arbitrage table, transfer/D-W filtering and chain matching info.
- Config via env vars (SCANNER_*, MEXC/Binance/Bybit API keys).

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
@devin-ai-integration
Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@devin-ai-integration
Copy link
Copy Markdown
Author

Test results — passed

End-to-end smoke test of the dashboard on localhost:8000 (session).

Test Result
Empty initial state before any scan passed
Manual scan with strict params (min_volume=5M, spread 1–15%) passed
Re-scan with permissive params (500K, 0.3–15%) populates table passed
Sort toggle on Спред column (asc/desc, ▲/▼) passed
Pair-name filter narrows table passed
Transfer-ok checkbox narrows to D+/W+ rows passed
Key evidence — permissive scan + transfer filter

After clicking Запустить сканирование with min_volume=500000, min_spread=0.3, max_spread=15:

Scan complete — 704 USDT pairs, 231 multi-exchange, 13 arb rows, 2 transfer-ok, 1.4s duration

Spreads of all 13 rows are between 0.30% and 10.95% — within the requested 0.3–15% range.

Checking «Только с возможным трансфером (D+/W+)» narrows the table to the 2 rows where both sides show green D+/W+ — NOTUSDT on TON chain and RAINUSDT on ARBITRUM:

Transfer filter — 2 rows with green D+/W+ on both sides

Sort toggle
Click 1 — asc (▲), top spread 0.30% Click 2 — desc (▼), top spread 10.95%
asc desc — same as main screenshot above
Pair filter (typing "BETH")

BETH filter — 1 row (BETHUSDT), badge "1"

Known limitations (not failures)

  • Binance / Bybit returned 0 pairs. Identical to the standalone scanner.py behaviour on the same VM — both endpoints are IP-blocked without a proxy. Resolved by setting SCANNER_PROXY=socks5://….
  • MEXC / Binance / Bybit deposit-withdraw cells show ? because no API keys were available in this env. Other 5 exchanges (Bitget, KuCoin, Huobi, Gateio, XT) returned D/W data via public endpoints and displayed correctly.

devin-ai-integration Bot and others added 16 commits May 10, 2026 19:45
Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
- Pause/resume toggle in topbar controls the background loop (manual scans
  still run). New POST /api/pause and POST /api/resume endpoints; the state
  flag is exposed via GET /api/state.
- Each completed scan appends arb rows into an in-memory ring buffer
  (SCANNER_HISTORY_LIMIT, default 1000). GET /api/stats?min_spread=&
  min_profit=&limit=&require_transfer=&pair= returns filtered events,
  newest first. DELETE /api/stats clears history.
- New Statistics tab in the topbar with interactive threshold inputs
  (defaults: 2% / 20 USD), pair search, transfer filter, sortable table,
  and a Clear-history button.
- Parameters and Summary cards are now collapsible; collapsed state is
  persisted per card in localStorage (scanner.collapse.*). The selected
  tab is also remembered (scanner.ui.tab).
- Buy/Sell exchange cells in both tables are now anchors to that
  exchange's spot trading page for the coin (target=_blank). URL
  templates cover all 13 supported exchanges.
- Small button next to Pause triggers an immediate manual scan with the
  currently entered parameters.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
- New "Auto-refresh" input in the scan-parameters form lets the user
  change the background loop period at runtime (minimum 5 seconds).
  POST /api/scan now accepts an optional refresh_interval field; the
  value is stored on ScannerState and read by the background loop on
  each tick, so the next sleep picks up the change immediately. The
  current interval is reflected in GET /api/state and the topbar label.
- Default values for min_volume / min_spread / max_spread are now 0.
  A max_spread of 0 (or negative) is interpreted as "no upper limit"
  in find_arbitrage, otherwise the form would filter out every row
  by default. SCANNER_MIN_VOLUME/MIN_SPREAD/MAX_SPREAD env defaults
  follow the same convention.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Add POST /api/settings that updates ScannerState overrides (proxy,
min_volume, min_spread, max_spread) and the runtime refresh_interval
without triggering a scan. The frontend now wires every parameter input
to a debounced (600 ms) call to this endpoint, plus an immediate flush
on the 'change' event, so users no longer have to click anything to
make the form take effect — the next background-loop tick or manual
scan picks up the new values automatically.

Renamed the submit button to 'Сканировать сейчас' (since 'apply' is
now implicit) and added a hint that confirms each auto-save with a
timestamp.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Add HistoryStore (app/history_store.py): a tiny append-only JSON Lines
backing store. On every scan completion the newly-detected arbitrage
rows are appended to <repo>/data/scanner_history.jsonl in addition to
the in-memory ring buffer. On startup the tail of the file (up to
SCANNER_HISTORY_LIMIT events) is restored into memory so the Stats tab
is populated immediately after a restart.

New env var SCANNER_HISTORY_FILE controls the path (empty disables
persistence). DELETE /api/stats now also truncates the file. A new
GET /api/history/download endpoint streams the full file to the
browser, surfaced as a '⬇ Скачать историю' button in the Stats tab.
/api/state grew history_file and history_file_size fields used by the
UI footer to show where the data lives and how big it is.

data/ is now gitignored so the runtime artifact never lands in
source control.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
The Stats-tab thresholds (min_spread, min_profit, require_transfer,
pair search) now decide which arbitrage rows actually land in the
in-memory ring buffer AND on disk — rows that don't pass are silently
dropped at record time. Scan-level filters (min_volume / min_spread /
max_spread on ScannerConfig) already filter at scan time, so combined
with the new write gate users only persist events that satisfied BOTH
filter sets, which is what they asked for.

Implementation:
- ScannerState.stats_filter holds the current thresholds, defaulting to
  the same values the UI shows (2% / $20).
- New POST /api/stats/filter applies a filter without triggering a
  scan; the frontend pushes the filter every time the user edits the
  Stats fields (debounced where appropriate) and on initial page load
  so the server picks up any localStorage-restored values.
- _record_history skips events failing event_passes_stats_filter and
  logs a one-line summary of how many rows were kept vs. dropped.
- /api/state echoes the active stats_filter so the UI can show it.

Existing data on disk is left untouched — the filter only governs
future writes. 'Clear history' still wipes both buffer and file when
the user wants a fresh start.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
- TelegramNotifier (app/telegram_notifier.py): async bot client with
  per-scan deduplication keyed on PAIR|BUY_EX->SELL_EX, message
  splitting at 4096 chars, total/per-msg counters, and graceful
  fall-through when not configured.
- After each scan, _record_history() returns the events that passed
  the stats filter; trigger() fires an asyncio task that batches them
  into one Telegram message (split if needed) containing ONLY new
  pairs vs the previous notification.
- New endpoints:
    POST /api/telegram/settings  (toggle / set token / chat_id)
    POST /api/telegram/test      (probe the bot)
    POST /api/telegram/reset_dedup
  Telegram state is echoed in /api/state for the UI.
- 'Telegram' card in the Arbitrage view: enable checkbox, masked
  bot-token input (token never echoed back), chat_id input, test +
  reset-dedup buttons, status badge, last-sent / dedup counters.
- DELETE /api/stats now also resets dedup so old situations can
  re-trigger from scratch.
- Config / env: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_ENABLED

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
…Y fallback)

Telegram is blocked in some regions, which made the new test/notify
endpoints time out after 15 s. The notifier now supports proxies the
same way the scanner already does:

- TelegramNotifier accepts an explicit `proxy` argument plus a
  `fallback_proxy_provider` callable that is consulted at send-time
  when no Telegram-specific proxy is set. main.py wires the scanner's
  current effective proxy (runtime override or SCANNER_PROXY env) as
  the fallback, so a single SCANNER_PROXY setup is enough.
- SOCKS proxies are routed via aiohttp_socks.ProxyConnector (already
  in requirements); HTTP(S) proxies via aiohttp's `proxy=` kwarg.
- New env var TELEGRAM_PROXY + runtime field on POST /api/telegram/settings.
- UI: new 'Прокси (опционально)' input in the Telegram card. Placeholder
  reflects the effective fallback. Hint line shows the chosen proxy and
  its source (TELEGRAM_PROXY / SCANNER_PROXY / без прокси).
- Credentials in proxy URLs are masked everywhere they're surfaced
  (/api/state, hint line, placeholder).

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Two related additions:

1. Orderbook depth is now mutable at runtime, not just via SCANNER_OB_LIMIT
   env. Form field 'Глубина стакана, уровней' on the Arbitrage tab; range
   5..500 levels, enforced by Pydantic on POST /api/settings.

2. New column 'Профит при $N' that answers 'how much USDT profit would I
   actually capture if I spent at most $N walking the buy book and selling
   into the sell book?'. The full intersection 'Профит, $' column is kept
   alongside it (best-case with unlimited capital).

Backend:

- ScannerConfig.orderbook_budget_usdt (env SCANNER_OB_BUDGET, default 1000).
- OrderbookAnalysis gains budget_usdt / profit_at_budget_usdt / filled_usdt.
- calc_arb_volume now returns (cost, avg_spread, profit_at_budget, filled)
  and truncates the last orderbook level proportionally when the budget is
  hit mid-level. Setting budget=0 keeps the old behaviour (those two return
  values are 0.0).
- analyze_orderbooks plumbs ScannerConfig.orderbook_budget_usdt through.
- _row_to_history_event now persists ob_budget_usdt / ob_profit_at_budget_usdt
  / ob_filled_usdt to history JSONL so the Stats tab survives restarts.
- ScanOverrides exposes orderbook_limit (5..500) and orderbook_budget_usdt
  (>=0); both auto-apply on input change in the UI.

Frontend:

- Two extra form fields; placeholder reflects the current backend value.
- 'Profit at $N' column shows '—' when the budget is 0 or when the
  orderbook didn't have enough profitable depth to fill anything.
- Column header dynamically shows the active budget (e.g. 'Профит при $1000')
  so the user always sees the N being used.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Mirror of the (now-reverted) Telegram notifier, retargeted at MAX
(platform-api.max.ru). MAX's bot API is described at
https://dev.max.ru/docs-api/methods/POST/messages — endpoint
POST /messages?chat_id=...&user_id=..., Authorization header with the
raw token from @Masterbot, markdown formatting, 4000-char text cap.

Behaviour:

- After each completed scan, every arbitrage row that passes the stats
  filter (and so is being written to history) becomes one bullet in a
  single MAX message — but only for pairs that weren't in the previous
  message (dedup key = 'pair | buy_ex -> sell_ex', FIFO ring of up to
  5000 keys, cleared by 'Clear history' / 'Reset dedup' buttons).
- Long lists are split across multiple messages (<=3800 chars each)
  with a '(продолжение)' header, paced ~1 msg/sec to stay polite.
- The MAX notifier shares the proxy story with the scanner: MAX_PROXY
  env (or runtime field) wins; if empty, falls back to whatever the
  scanner is currently using (SCANNER_PROXY env or runtime override).

Endpoints:

  POST /api/max/settings       (enable / token / chat_id / kind / proxy)
  POST /api/max/test           (probe — bypasses 'enabled' & dedup)
  POST /api/max/reset_dedup    (drop the memory)

DELETE /api/stats now also clears the MAX dedup so old situations can
re-trigger from scratch.

UI: collapsible 'Мессенджер MAX' card on the Arbitrage tab — enable
toggle, masked bot-token input (token never echoed back), chat/user
ID + recipient type select, optional proxy field, test + reset-dedup
buttons, status badge (готов / выкл / ошибка / не настроен),
counters (total_sent / dedup_skipped / last_sent_at).

Config / env: MAX_BOT_TOKEN, MAX_CHAT_ID, MAX_RECIPIENT_KIND,
MAX_ENABLED, MAX_PROXY.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
The block was rendered inside view-arbitrage, but I had hidden its body
with an inline 'display:none' and set the toggle to aria-expanded=false /
arrow='▸' — so the form was unreachable. Match the controls-/summary-card
defaults: aria-expanded=true, arrow='▾', no inline style.

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Per user request, undo everything since 8494c9e ('Gate history
persistence by Stats filter thresholds'). This commit sets the tree
back to the state of 8494c9e in a single revert commit — no force-push,
no rewritten history. The following commits are effectively dropped:

  ddba1af Telegram notifications for new arbitrage situations
  37dfe0a Telegram: route requests through proxy
  bbcced0 Revert "Telegram: route requests through proxy"
  f84c2e7 Revert "Telegram notifications for new arbitrage situations"
  411000b Orderbook: configurable depth + 'Profit at $N' column
  b85ba49 MAX messenger: notifications for new arbitrage situations
  097e706 MAX UI: show the messenger card expanded by default

Co-Authored-By: harlequincariotta <harlequincariotta@wshu.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant