Дата: 2026-05-30
Метод: 4 параллельных агента по модулям + 1 adversarial-верификатор (8 находок перепроверены по коду).
Покрытие: executor.py, config.py, resolver.py, analyzer.py, risk.py, scanner.py, bot.py, db.py, forecaster.py (4199 строк).
Статусы: ✅ CONFIRMED (верифицировано вторым агентом) · ◻️ не проверялось вторым агентом (single-source, обоснование в коде) · ❌ FALSE POSITIVE (проверено и отклонено).
resolver.py:765-770 — °C between/eq маркеты резолвятся с °F-округлением → неверный win/lossДля between/eq границы конвертируются в °F (13–14°C → 55.4–57.2°F) и сравниваются с round(actual_F). Polymarket резолвит C-маркеты округлением фактической температуры в °C. День 55°F (12.8°C → round = 13°C = YES) скорится как NO. Бьёт по каждому Celsius-маркету = почти все не-US города. Искажает P&L и калибровку.
Fix: при unit=="C" округлять факт в °C и сравнивать с целыми °C-границами: actual_c=(actual-32)*5/9; yes = low_c <= round(actual_c) <= high_c (использовать оригинальные threshold_low/high до конвертации).
executor.py:184-218 — retry-цикл post_order может разместить ордер дважды (double-spend)Цикл ретраит client.post_order() до 3 раз на любом исключении. Если первый POST дошёл и ордер принят, но HTTP-ответ потерян (timeout/proxy drop — ровно то, на что рассчитан ретрай), вторая попытка размещает вторую live-позицию. Idempotency-гарды (get_active_trades, has_cancelled_for_market) работают до цикла и не видят дубль внутри него.
Fix: не ретраить вслепую. Ретраить только ошибки, доказывающие, что запрос не дошёл (connect-timeout/DNS). При read-timeout/unknown — пометить unverified и отдать reconciliation, как уже сделано на loss-пути.
executor.py:107-120 — idempotency-гард не атомарен, параллельные сканы оба проходятplace_bet идёт через asyncio.to_thread в цикле по брекетам. Последовательность check (get_active_trades) → insert (save_trade) без лока и без UNIQUE-констрейнта. Два сигнала на один market+side, обработанные близко (или скан, пересёкший предыдущий — scan-лока нет, только _resolve_lock), оба читают «нет активного трейда» и размещают live-ордера.
Fix: partial UNIQUE index на (market_id, side) WHERE status IN ('pending','filled') AND dry_run=0 + scan-level asyncio lock по аналогии с _resolve_lock.
executor.py:355-375 — redeem перебирает [balance,balance], шлёт лишние on-chain tx, жжёт газДля neg-risk redeem цикл [[balance,0],[0,balance],[balance,balance]] шлёт реальную tx на каждой итерации. [balance,balance] просит редимить оба исхода, имея только один → revert (потраченный газ). На позициях $3–6 три неудачные tx стоят дороже позиции. Нет cap газа против стоимости позиции.
Fix: определять индекс исхода из side/token_id (известно, что держим) и слать ровно один redeem. Убрать [balance,balance]. Скип redeem, если стоимость позиции < ожидаемого газа.
forecaster.py:299, 448 — control-run считается членом ансамбля → смещение вероятностиmember_keys = [k for k in hourly if k.startswith("temperature_2m")] ловит и детерминированный temperature_2m (control), и ..._memberNN. Control получает равный вес с возмущённым членом (~1/31 на модель, 2 control-рана из ~82). Смещает вероятность каждой ставки к среднему. (config.py:150 подтверждает ENSEMBLE_MEMBERS=31 = control + member01..30.)
Fix: startswith("temperature_2m_member") либо явно выкинуть "temperature_2m". Проверить, что гейт ≥10 членов всё ещё проходит.
db.py:306-319 (get_today_stats) — UTC-день вместо Vancouver ослабляет kill switchtoday = datetime.now(timezone.utc) бакетит дневной P&L по UTC-дню. С ~17:00 PDT (00:00 UTC) UTC уходит на следующий день, пока в Ванкувере ещё «сегодня». Убыточная вечерняя серия рвётся на два бакета → MAX_DAILY_LOSS (читается из risk.py:32) может не сработать. Доп: pnl = sum(t["pnl"] for t in trades if t["pnl"]) отбрасывает легитимный $0.
Fix: считать день в ZoneInfo("America/Vancouver") консистентно с записью created_at; sum((t["pnl"] or 0) for ...).
risk.py:23-35 — kill switch проверяется только внутри can_trade()Проверка дневного убытка живёт только в can_trade(), вызываемой при появлении нового сигнала. Если новых сигналов нет, уже превышенный убыток по открытым позициям не триггерит стоп.
Fix: отдельный check_daily_loss() в начале каждого скан-цикла, до цикла по брекетам.
bot.py:372-386 — Basic Auth fail-open при пустом DASH_USERif not DASH_USER: return await call_next(request) — при незаданном/пустом DASH_USER (PM2 env не пробросился — известные грабли) все эндпоинты, включая мутирующие POST /api/scan, /api/resolve, /api/kill-switch/*, открыты.
Fix: fail closed — отказывать в старте или денаить мутирующие POST, если креды не заданы; bind на localhost + nginx auth.
analyzer.py:112 — финальная price может упасть ниже только что проверенного MIN_BUY_PRICEprice = min(buy_price + 0.01, win_prob - 0.02). Band-проверка (MIN/MAX_BUY_PRICE) сделана на buy_price, но торгуется price. NO-бет при buy_price 0.73, model NO-prob 0.74 → price = min(0.74, 0.72) = 0.72 — ниже пола 0.72 и ниже аска (не зафилится).
Fix: перепроверять финальную price против MIN/MAX_BUY_PRICE (или валидировать price, а не buy_price).
config.py:18, 26 — парсинг bool: BLOCK_YES_SIDE fail-dangerousBLOCK_YES_SIDE = os.getenv(...).lower() == "true". Любое искажённое значение ("True " с пробелом, "1", "yes", дроп при env-reload) → False, разблокируя YES-сторону (42.9% WR, −$13.49). DRY_RUN парсится так же, но там дефолт fail-safe.
Fix: строгий хелпер val.strip().lower() in ("true","1","yes") + лог фактических DRY_RUN/BLOCK_YES_SIDE на старте.
executor.py:189 — ответ без orderID принимается как успехorder_id = response.get("orderID") or response.get("id","unknown"). 2xx-тело с реальным реджектом ({"success":false} без orderID) → order_id="unknown", success=True, пишется pending-трейд. Фантомная позиция занимает слот MAX_OPEN_POSITIONS и списывает actual_cost из running balance.
Fix: отсутствие orderID/id или falsy success = провал; успех только при реальном id.
executor.py:157-172 + bot.py:287,305 — три разные формулы стоимости ордера расходятсяexecutor.actual_cost = round(shares*price,2) (shares = round(size/price,2)), а bot.py дважды считает max(signal["size"], 5*price), executor применяет MIN_SHARES по своему правилу. Running-balance гейт в bot.py мис-предсказывает фактическую стоимость → преждевременный balance_exhausted или over-deploy.
Fix: единый источник правды — переиспользовать возвращаемый executor actual_cost; округлять shares к точности венчика.
executor.py:227 — классификация ошибок по подстроке хрупкаяis_definite_reject = "not enough balance" in err or "allowance" in err. Смена формулировки SDK ("insufficient balance", локализация) → definite reject уходит в unverified; посторонняя ошибка с "allowance" → ошибочно cancelled (реоткрывает ghost-position риск).
Fix: структурные коды/типы исключений py_clob_client_v2 вместо подстрок; неоднозначное → unverified.
executor.py:440-481 — Safe nonce читается один раз, в retry не обновляетсяsafe_nonce = wc.safe.functions.nonce().call() читается до подписи; retry-цикл обновляет только EOA nonce, не Safe nonce и не подпись. Если redeem-tx через Safe майнится между чтением и execTransaction → Safe nonce устарел → revert, ретрай той же устаревшей подписью жжёт до 3× газа при gas=500000.
Fix: перечитывать safe.nonce() и переподписывать внутри retry; либо сериализовать redeem и wrap.
executor.py:271+ — захардкоженные адреса без checksum; get_ctf_balance()==-1 не обрабатываетсяCTF/USDC_E/ONRAMP/USDC_NATIVE — строковые литералы, используются raw в eth_call. get_ctf_balance возвращает -1 при ошибке RPC, но redeem_position различает только ==0; -1 идёт в elif balance_before > 0 → False → redeem молча ничего не делает или действует на устаревших данных. Бесплатные RPC отдают stale.
Fix: checksummed-константы; при -1 (unknown) — abort redeem-пути; retry по 2 RPC до действия.
bot.py:346-348 — APScheduler без max_instances/coalesce/misfire_grace_timeСкан дольше SCAN_INTERVAL_SEC (1800s) → следующий ран дропается с дефолтным misfire_grace_time=1, дашборд показывает stale (дропнутый путь не обновляет last_scan_result). Внутренние локи частично прикрывают, но не таймстемпы.
Fix: add_job(..., max_instances=1, coalesce=True, misfire_grace_time=300).
forecaster.py:155 — Gaussian-fallback торгует overconfidentКогда ансамбль недоступен (rate-limit/sparse), fallback на calculate_probability с SIGMA_BY_DAYS. Узкий between-брекет (±0.5°F) при σ=3.0 даёт ненулевую вероятность, проходящую MIN_EDGE после Kelly — ровно там, где модель наименее надёжна.
Fix: блокировать live при prob_method=="gaussian" для between/eq, либо раздувать sigma / поднимать edge при n_members==0.
resolver.py:331-333 — поиск YES-токена без break, побеждает последнийЦикл по токенам не делает break после матча, перезаписывает yes_winner на каждой итерации. При >1 токене с outcome=="yes" или неожиданном порядке результат определяет последняя итерация.
Fix: break после первого outcome=="yes".
resolver.py:486, 575 — дублирующий redeem за один цикл_auto_redeem_resolved(trades) и затем безусловный _retry_unredeemed_wins() оба берут только что зарезолвленные wins и шлют redeem (time.sleep(2) каждый). Не теряет средства (redeem идемпотентен, _mark_redeemed гард), но шлёт избыточные on-chain tx до коммита redeemed=1.
Fix: убрать _auto_redeem_resolved, полагаться на _retry_unredeemed_wins (покрывает все unredeemed wins).
analyzer.py:55 — tie-break берёт YES, потом блокирует → пропуск торгуемого NOif edge_yes >= edge_no на равенстве выбирает YES, который при BLOCK_YES_SIDE=true дропается (строка 73) — хотя идентичный по edge NO был бы торгуем. Редко (точное равенство), но молча теряет валидные NO.
Fix: на ничье предпочитать NO при BLOCK_YES_SIDE: if edge_yes > edge_no and ....
resolver.py:766-767 — between fallback ±0.5 при отсутствии low/highКогда low_f/high_f отсутствуют, fallback threshold_f ± 0.5 + сравнение с round(actual) (int). Для CLOB-sourced трейдов без low/high окно молча сужается; в связке с °F-округлением (#1) резолвит неверно.
Fix: требовать явные threshold_low/high; при отсутствии для between/eq — скипать резолюцию (return None, 0), не угадывать.
| # | Файл | Проблема | Fix |
|---|---|---|---|
| 22 | bot.py:454-457 ✅ |
total_capital = exchange + total_deployed двойной счёт pending (display-only, дашборд врёт о капитале) |
выбрать одну модель: при exchange-excludes-pending деплоить только filled |
| 23 | bot.py:224-268 ✅ |
between/eq cache-hit не вызывает db.save_forecast → только первый брекет на город+дату пишется в forecasts (теряется калибровка) |
вызвать db.save_forecast(forecast) в ветке cache-reuse |
| 24 | scanner.py:95-208 ❌→🟢 |
нет фильтра closed/active/acceptingOrders. Импакт FALSE POSITIVE (скан только future-date + _is_past_peak_time скип) — но latent robustness gap |
скипать closed==true/acceptingOrders==false |
| 25 | scanner.py:142 ◻️ |
Gamma 429 глотается как «нет рынка», без backoff/retry | детектить status==429, ретраить, логировать отдельно от 404 |
| 26 | scanner.py:188 ◻️ |
volume or-цепочка путает 0 и missing |
детерминированно брать volumeNum |
| 27 | risk.py:60-64 ◻️ |
per-market deployed/cooldown не видят cancelled-but-filled ghost shares (status cancelled исключён) → MAX_PER_MARKET обходится |
учитывать has_cancelled_for_market и в risk |
| 28 | risk.py:43 ◻️ |
cooldown по market_id без side |
ключ (market_id, side) для консистентности с executor |
| 29 | risk.py:91-93 ◻️ |
get_status() считает dry-run трейды в live (bot.py частично оверрайдит) |
live_only=not DRY_RUN |
| 30 | resolver.py:438 ◻️ |
Open-Meteo cutoff = date-string compare с truncated datetime → off-by-one, резолв до финализации архива | сравнивать полные datetime по city-TZ end-of-day |
| 31 | analyzer.py:288 ◻️ |
KELLY_BANKROLL фикс $25, при просадке банка mild over-betting |
подавать live-баланс (известный TODO «dynamic sizing») |
| 32 | config.py:20-23 ◻️ |
дефолты MAX_OPEN_POSITIONS=20, MAX_PER_MARKET=6 против $25 кошелька — при потере .env снимают потолок риска |
дефолты = документированные live-safe (7/3) |
| 33 | executor.py:48-49 ◻️ |
proxy-config fail только логируется («IP may be exposed»), ордера идут un-proxied из geoblocked-региона | в live — hard-stop ордеров, если _apply_proxy упал при заданном TRADE_PROXY |
| 34 | executor.py:298,403 ◻️ |
redeem_position/wrap читают module-level DRY_RUN, игнорируя override place_bet(dry_run=...) |
добавить опц. dry_run-параметр |
scanner.py:226 — брекеты «56°F to 57°F» не парсятся. Реальный regex требует слова «between» + дефис; процитированного агентом regex в коде нет. Главное: bot.py:212 скипает любой брекет с threshold is None до торговли. Неспарсенный брекет не торгуется._is_past_peak_time скипает прошедший peak. Future-date по определению не зарезолвлен. (Сам отсутствующий флаг-фильтр оставлен как 🟢 #24.)Brier NO-side inversion (убран), резолюция строго m.date < date('now'), auto-redeem фильтр outcome='win' AND dry_run=0, CLOB proxy→direct fallback, thread-local без conn.close(), cleanup_old_forecasts(7), forecaster retry 3× для 429/5xx, async balance + 60s cache, FK-off при ALTER. P&L/fee-математика (size*(1-price)/price - fee / -(size+fee), fee shares*0.02*price*(1-price)) — без sign-ошибок для YES и NO. gte/lte сравнения и actual-temp fetch с city-TZ — корректны.
BLOCK_YES_SIDE.Находки 🔴#2–4, 🟡#9–21, 🟢#25–34 — single-source (один аудитор), обоснованы по коду, но не прогонялись через второй агент. Верифицированы (✅): #1, #5, #6, #7, #8, #22, #23, #24-отклонение.