# Чек-лист рефакторинга

Архитектурный разбор: база здоровая, переписывать не нужно. Рефакторинг —
точечный, по убыванию ROI, инкрементально и под тестами. Финансовый путь
трогаем только под характеризационными тестами.

Базовое правило: после каждого пункта `php tests/run.php` должен быть зелёным.

---

## Этап 0 — безопасные, не трогают финансовую логику

- [x] **C. Webhook-обвязка.** Вынесены хелперы `webhook_notify_open/close/
  partial_close()` в `webhook_notifier.php` (самоохраняются по пустому списку,
  context-форма в одном месте). Заменены все 11 точек вызова в position_tracker,
  pool_tracker, watching_tracker, executor. Прямых `dispatch_webhooks` вне
  webhook_notifier больше нет; тесты 136/0.

- [x] **Единый резолвер cost-дефолтов.** Литералы `0.035` / `0` сведены в
  `COST_DEFAULT_FEE_PCT` / `COST_DEFAULT_SLIPPAGE_PCT` / `COST_DEFAULT_FUNDING_PCT`
  (config.php), на них же опирается `GLOBAL_SETTINGS_DEFAULTS`. Добавлен
  `resolve_cost_settings($src)` (config.php) для кошельков и пулов; `pool_cost_settings`
  делегирует ему. Все ~14 разрозненных `?? 0.035/?? 0` заменены на константы. Тесты 136/0.

- [x] **Общий http-хелпер.** `executor.php::execute_live` переведён на
  `webhook_http_request()` (тот же SSRF-пиннинг + запрет редиректов/протоколов;
  добавлен параметр таймаута, executor передаёт `API_TIMEOUT_SEC`) — убран
  собственный дублирующий curl-блок и повтор логики пиннинга. Два API-вызова в
  `collector.php` (api_post + batch) вынесены в общий `hl_api_curl_opts()`. Тесты 136/0.

- [x] **Убрать беспричинную дивергенцию метрик.** Пул в close-пути переведён на
  обёртку `calculate_trade_metrics($existing, $exitPrice, pool_cost_settings($pool))` —
  убраны дублирующие `compute_funding_usd` + `compute_trade_metrics_core` инлайном.
  Результат идентичный, тесты 136/0.

- [x] **Мелочь: лишний алиас.** `normalize_storage_trade()` оказался мёртвым кодом
  (нигде не вызывался) — удалён. Остался единственный `storage_normalize_trade_row()`.

---

## Этап 1 — контейнерный proof-of-concept (низкий риск)

- [x] **D. Слить `sweep_wallet_risk` + `sweep_pool_risk`.** Выделено общее ядро
  `sweep_entity_risk(entityType, entityId, costSettings, balanceSettings, webhooks,
  webhookLabel, metaForPosition)` в position_tracker.php; обе функции стали тонкими
  обёртками. Оси вариации (cost-настройки, стартовый баланс, webhook-label,
  per-position label истории с owner-трейдером пула) переданы параметрами/замыканием —
  поведение строго сохранено (webhook пула шлёт label пула, история — label с
  трейдером). Добавлены тесты на `compute_unrealized_metrics` (решение sweep:
  ликвидация/выживание/слиппедж выхода). Тесты 143/0.

  Полная оркестрация sweep (цикл + атомарное закрытие + MTM) пока не покрыта
  тестами — нужен DI для `fetch_mid_price` (см. Этап 2).

---

## Этап 2 — характеризационные тесты ПЕРЕД объединением оркестрации

- [x] Зафиксировано текущее поведение `track_position` (open / close / same-side /
  reverse), `track_pool_position` (ownership open/busy/close) и `sweep_wallet_risk`
  (ликвидация под водой) характеризационными тестами против временной БД.
  Страховочная сетка для Этапа 3. Тесты 161/0.

- [x] Тестовый шов вместо DI (по решению — лёгкие seed-хуки): `fetch_seed_mids()`
  (price_fetcher) и `fetch_seed_equity()` (collector) предзаполняют существующие
  статические кэши, позволяя гонять оркестрацию без сети. Сигнатуры трекеров не
  тронуты. Полноценный DI остаётся опциональным пунктом Этапа 4.

---

## Этап 3 — объединение оркестрации (главный выигрыш, главный риск)

Только после Этапа 2 (зелёные характеризационные тесты).

> Сетка расширена (176/0): помимо happy-path, end-to-end покрыты partial close,
> обрезка размера по свободной марже, proportional sizing (equity через шов) и
> drawdown-watching триггер. Можно браться за A и B/E.

Минимальная абстракция:
```
AccountCtx { entityType, entityId, costSettings, webhooks, meta }
sizingFn:  fn(float $balance): float           // прячет wallet/pool/trader различия
ownership: ?fn($existing, $traderId): Decision // null для кошелька; busy-check для пула
```
Остаётся один набор: `open_position(ctx, signal, mid, sizingFn)`,
`close_position(ctx, existing, signal, mid)`. Кошелёк и пул — тонкие адаптеры.

Единственный настоящий блокер — **ownership** (только владелец закрывает, чужой
сигнал игнорируется): это другой control flow, нужен hook-политика, не данные.
Остальные оси вариации сводятся к параметрам/замыканиям:
- доступ к состоянию — кошелёк уже обобщён на `(entityType, entityId)`; пул хардкодит `'pool'`;
- сайзинг — `resolve_position_size_usd` vs `resolve_pool_position_size` → замыкание `fn(balance)`;
- плечо — `signal['leverage']` vs `resolve_trader_leverage`;
- источник cost-настроек — `$settings` vs `$pool` (решается Этапом 0).

- [x] **B. Литерал «массив позиции»** — добавлен `build_open_position($fields)`
  (position_tracker.php): централизует форму записи позиции, дефолты `opened_at`/
  `entry_reason` и опциональные owner_* (пул). Все 6 копий (оба трекера вход+реверс,
  watching кошелёк+пул) переведены на билдер. Новое поле позиции теперь добавляется
  в одном месте (+ storage). Тесты 161/0.

- [x] **A. Оркестрация открытия (4 копии).** Все 4 (`track_position`,
  `track_pool_position`, `check_wallet_watching`, `check_pool_watching`) сведены на
  общий `open_tracked_position(...)`: sizing→free margin→build→save→(опц.) open-webhook.
  Оси различия — замыкание сайзинга, плечо, cost-настройки, owner, кто шлёт
  open-вебхук, entry_reason ('drawdown' для watching). Watching-специфика (UPNL,
  снятие watch, keep-on-insufficient) осталась в трекерах. Тесты 181/0.

- [x] **B/E. Оркестрация close/reverse/partial (2 копии).** Выделен общий
  `close_tracked_position(...)` (полное/реверс/частичное закрытие + метрики +
  атомарная запись + close/open/partial-вебхуки). `track_position` и
  `track_pool_position` close-ветки стали вызовами-адаптерами. Оси различия —
  cost/balance-настройки, partial-режим, meta истории (label с трейдером у пула),
  webhook-label, замыкание/плечо/owner реверса; статус 'closed_only' пул трактует
  как 'closed'. Предварительно дозакрыта характеризация (pool reverse + pool
  partial). Поведение сохранено, тесты 187/0.

**Этап 3 завершён.** Обе оркестрации (открытие и закрытие) сведены к общим
`open_tracked_position` / `close_tracked_position`; трекеры остались тонкими
адаптерами, в которых живёт только то, что действительно различается —
ownership/same-side/watching-решения и сборка контекста.

---

## Этап 4 — на будущее / при росте масштаба

- [ ] **Версионированные миграции.** `storage_migrate()` — `CREATE IF NOT EXISTS`
  + один `ALTER` через `PRAGMA table_info` (`storage.php:219`), нет `schema_version`.
  Для финансового приложения безопаснее версионированный раннер.

- [x] **DI для сетевого слоя.** Введён инъектируемый HTTP-транспорт Hyperliquid
  `/info` (`hl_set_http_transport` / `hl_http_transport` в collector.php); `api_post`
  использует его вместо curl, если установлен. Тестовые швы `fetch_seed_mids` /
  `fetch_seed_equity` удалены из прод-кода — тесты подменяют сеть фейковым
  транспортом (`test_set_mids` / `test_set_equity` в run.php), проходя реальный
  парсинг ответов. В проде транспорт не установлен → обычный curl.

- [x] **Разбить `storage.php`** на repository-модули за фасадом: `storage/`
  (db / hl_orders / trades / balances / positions / state / watching /
  transactions / wallet_sync / wallet_log). `storage.php` — тонкий фасад,
  публичный API не изменился.

- [x] **Убрать протаскивание контекста через настройки.** Адрес трейдера
  передаётся явным параметром `resolve_position_size_usd(..., $traderAddress)`
  через walletMeta вместо мутации `$settings['_trader_address']` (fallback на
  старый ключ сохранён).

- [x] **Dashboard handler/view split.** Логика вынесена в `dashboard/lib/`
  (`forms.php` — парсеры; `render.php` — HTML-хелперы; `wallets_handler.php` /
  `settings_handler.php` / `pools_handler.php` — POST-обработчики как чистые
  функции). Страницы стали тонкими контроллерами (wallets 1176→499, settings
  372→238, pools 928→477), CSRF остался в web-слое. Добавлены смок-тесты
  `tests/dashboard_test.php` (90) на data-агрегацию и все handler'ы против
  временных БД/конфигов; ими же пойманы и исправлены два pre-existing бага
  (создание пула падало из-за отсутствия `total_funding`; `sizing_mode` мог
  стать `null` при отсутствии поля). `index.php` остаётся view-над-data.php
  (логика data.php уже покрыта тестами).
</content>
</invoke>
