[CL-AUDIT-TRADING] Аудит и фикс торговой логики Arnold (18 багов) #303

Merged
andrei merged 17 commits from feature/claude-trading-logic-fixes into master 2026-06-10 02:17:13 +00:00
Owner

Аудит и фикс торговой логики Arnold: 18 correctness-фиксов (TDD, ruff strict, full suite 5813 passed). Детали в TRADING-LOGIC-AUDIT.md. CRITICAL C1-C4, HIGH H1-H13, MED M1, LOW L1, H12 no-impact.

Аудит и фикс торговой логики Arnold: 18 correctness-фиксов (TDD, ruff strict, full suite 5813 passed). Детали в TRADING-LOGIC-AUDIT.md. CRITICAL C1-C4, HIGH H1-H13, MED M1, LOW L1, H12 no-impact.
check_exit использовал LONG-формулу pnl = (current-entry)/entry для всех
позиций. Для SHORT-копий профит (падение цены) считался убытком и срабатывал
-3% SL — прибыльные шорты закрывались как убыточные. Также возвращался
SignalType.CLOSE для шортов вместо COVER.

Теперь pnl_pct_for_position (side-aware) + close_long/close_short, как в
mean_reversion / scalping / momentum. Regression: test_copy_trading_exit.py.
При сбое futures-leg в _execute_delta_neutral после возможного размещения
ордера (post-order DB/risk failure) аварийный unwind реверсил только spot —
futures-позиция оставалась открытой = голое плечо. Теперь после spot-unwind
выполняется COVER по символу (флэтит long или short futures leg) best-effort.

Regression: test_audit_delta_neutral_futures_unwind.py.
analyze() передавал текущие EMA (e_f, e_s) на позиции ema_fast_prev/
ema_slow_prev в _check_short и ema_fast_prev в _check_long. fresh_cross
= ema_fast_prev > ema_slow_prev and ema_fast <= ema_slow вырождался в
x>y and x<=y = всегда False — детект свежего EMA-кросса для шортов был
полностью мёртв. Теперь извлекаем e_f_prev/e_s_prev = EMA[-2] и передаём их.

Regression: test_audit_trend_sniper_prev_ema.py.
compute_kill_switch_level сравнивал drawdown_pct >= L3_DRAWDOWN_PCT (10.0),
но compute_drawdown_pct возвращает ОТРИЦАТЕЛЬНЫЙ процент (-10.0 для 10%
просадки), как и ожидает drawdown_multiplier. Условие никогда не срабатывало
— L3 account kill (close all при >=10% DD) был мёртв. Теперь
drawdown_pct <= -L3_DRAWDOWN_PCT + abs() в сообщении. Тест переведён на
negative convention.
Функция падала в `return True`, когда ни один из ключей size/qty/contracts/
positionAmt не содержал положительного значения. BTC correlation guard
(_has_same_side_btc_position) принимал пустой/битый position dict за открытую
позицию и блокировал валидные сделки. Теперь возвращается False.
rsi() и rsi_series() возвращали 100.0 когда avg_loss==0, не проверяя avg_gain.
Плоская консолидация (нет ни роста, ни падения) — нейтраль RSI 50, не
перекупленность 100. Баг вызывал ложные преждевременные выходы (например
mean_reversion rsi_exit=55). Теперь 100 только если avg_gain>0, иначе 50.
execute_signal вычислял sl_pct/tp_pct через abs(), теряя направление. Сигнал
с перевёрнутыми SL/TP (LONG с SL выше / TP ниже входа, или зеркально для
шорта — из битого/вредоносного Telegram/TradingView вебхука) молча принимался,
а адаптер ставил SL на уровень профита и TP на уровень убытка =
гарантированный убыток/ликвидация. Логика вынесена в _resolve_sltp_pct,
которая валидирует геометрию (как futures_executor через validate_sltp_geometry)
и отклоняет инвертированный сигнал (reason=invalid_sltp_geometry).
_check_single_position пересчитывал TP из live vol24h на каждой проверке
(TAKE_PROFIT_BIG если vol>порог иначе TAKE_PROFIT_MEME), а entry-executor
уже считает tp_pct из vol24h в момент входа — но _build_position_entry его
выбрасывал. Если объём пересекал BIG-порог в течение позиции, TP менялся
(6%->3.5%) и позиция закрывалась раньше. Теперь tp_pct хранится при входе;
_resolve_tp использует залоченное значение (fallback на vol только для
legacy-позиций без tp_pct).
При нехватке USDT для парного buy после sell-fill код кредитовал
gross_profit - sell_fee в total_realized_pnl и инкрементил round_trips —
хотя round-trip не завершился и retry нет (record_fill финализирует).
gross_profit = price*qty*trailing_factor это маржа ожидаемого round-trip,
не реальная прибыль. Теперь реализуется только -sell_fee, round_trips не
растёт (консистентно с rejected-closer путём). Тест обновлён на корректное
поведение.
futures_engine software TP и SL close paths сохраняли Bybit cumExecFee
(round-trip entry+exit) как fee_usd закрывающей сделки, но entry fee уже
записан futures_executor при открытии → entry fee считался дважды,
занижая P&L каждого software-закрытия. Вынес _exit_only_fee (как
orphaned-close путь): из cumExecFee вычитается оценка entry fee, clamp >=0.
H5: _execute_close и _execute_factory_spot_close вызывали
_remove_live_position без side -> default "long". db_remove_position удаляет
WHERE side=$4, поэтому SHORT spot позиции не удалялись -> stale rows
триггерили дублирующие market sell каждый цикл. Теперь side выводится из
типа сигнала (_signal_to_position_side), как при сохранении позиции.

H6: _execute_close сохранял сделку с сырым балансом (qty) вместо
фактически отправленного округлённого qty_str -> расхождение audit/P&L/
reconciliation. Теперь сохраняется float(qty_str).
_execute_paper_sell и _execute_paper_close_short записывали сделку даже
когда удаление позиции вернуло pnl_qty=0 (позиции нет) — фантомная сделка
с qty=0 и success=True. Маскирует баги трекинга позиций и расходит paper
P&L с live (live отверг бы). Теперь при pnl_qty<=0 — warning + return False
(как qty-guard в _execute_paper_buy). Тест dispatch обновлён под наличие
позиции.
В TP-цикле position_poller mark_tp_hit вызывался при наличии позиции даже
когда close_qty<=0 (битый close_pct или нет остатка) и close пропускался —
уровень помечался "hit", позиция оставалась открытой, дашборд/следующий
poll считали её закрытой, ретрая не было. Теперь close_qty<=0 -> warning +
continue (без mark); mark_tp_hit достижим только после успешного close.
H13: build_workflow_intent ставил max_gas_usd = max(estimate, 25.0) —
$25 floor разрешал до ~5x overspend на дешёвых (L2) ротациях. Теперь
estimate or 25.0 (fallback только при отсутствии/нуле).
L1: APY-порог в сообщении выхода теперь min_absolute_apy*0.5*100 (явно
"половина минимума в процентах") вместо *50 — алгебраически идентично,
читается понятнее.
_generate_levels_arithmetic строил total_levels+1 точек на [lower,upper] и
делил по </> center. При trend-biased (асимметричных) границах центр не
середина -> неравный split (напр. 4 buy / 8 sell при GRID_LEVELS=6),
асимметричная экспозиция и перекос бюджета на ордер. Теперь генерируем
ровно GRID_LEVELS уровней на каждую сторону (как geometric); для
симметричных границ результат идентичен прежней линейной интерполяции.
Isolate cl551 overexposed-cooldown test from leaked engine position state
All checks were successful
Arnold Forgejo CI / frontend-audit (pull_request) Successful in 10s
Arnold Forgejo CI / secret-scan (pull_request) Successful in 2s
Forgejo Smoke Test / Smoke (pull_request) Successful in 1s
Arnold Forgejo CI / backend-tests (pull_request) Successful in 3m52s
d7f3602748
test_rebalance_grids_does_not_cooldown_underutilized_after_overexposed_skip
зависел от engine._get_linear_position_snapshot кэша — в полном suite другой
тест оставлял позицию BTCUSDT -> ветка "close-only managed" вызывала
save_state -> падение по порядку. Мокаем _linear_position_side + grid
maintenance helpers -> None/[] (как sibling-тесты), тест детерминирован.
andrei merged commit dadc08348b into master 2026-06-10 02:17:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
europa-tech-srl/arnold-trader-app!303
No description provided.