<?php
// =============================================================
// BALANCE TRACKER — виртуальный торговый счёт на кошелёк/пул
// =============================================================
//
// Бэкенд: SQLite (таблица wallet_balances через storage.php).

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/logger.php';
require_once __DIR__ . '/storage.php';
require_once __DIR__ . '/collector.php';

//
// State адресуется парой (entity_type, entity_id):
//   wallet → ('wallet', $walletId)
//   pool   → ('pool',   $poolId)
//
// Публичный API:
//   load_balance($entityType, $entityId, $settings)  -> array
//   save_balance($entityType, $entityId, $state)     -> void
//   apply_trade_to_balance($entityType, $entityId, $settings, $metrics) -> array
//   resolve_position_size_usd(...)                   -> float
//   calculate_fee_usd(...)                           -> float
//   compute_account_metrics(...)                     -> array
// =============================================================

// ---------------------------------------------------------------
// load_balance / save_balance
// ---------------------------------------------------------------

/**
 * Загружает баланс entity из SQLite. Если записи нет — дефолт со стартовым балансом.
 */
function load_balance(string $entityType, string $entityId, array $settings): array
{
    $startBalance = (float) ($settings['virtual_balance'] ?? 1000);
    return storage_load_balance($entityType, $entityId, $startBalance);
}

/**
 * Сохраняет баланс entity в SQLite.
 */
function save_balance(string $entityType, string $entityId, array $balanceState): void
{
    storage_save_balance($entityType, $entityId, $balanceState);
}

// ---------------------------------------------------------------
// Размер позиции и комиссия (логика не изменилась)
// ---------------------------------------------------------------

/**
 * Определяет размер позиции в USD исходя из sizing_mode.
 *
 * Поддерживаемые режимы:
 *   fixed            — фиксированная сумма (sizing_fixed_usd)
 *   percent_balance  — % от текущего виртуального баланса (sizing_percent)
 *   proportional     — зеркало доли трейдера: если трейдер держит X% капитала
 *                      в этой позиции, мы тоже вкладываем X% от нашего баланса.
 *                      Требует $signal['trader_size_usd'] и запроса equity трейдера.
 *
 * @param array       $settings        эффективные настройки кошелька
 * @param float       $currentBalance  текущий виртуальный баланс в USD
 * @param array       $signal          сигнал (используется только для proportional)
 * @param string|null $traderAddress   адрес трейдера (нужен только для proportional,
 *                    чтобы запросить его equity). Передаётся явно вызывающим кодом;
 *                    как фолбэк всё ещё читается из $settings['_trader_address'].
 * @return float                  размер позиции в USD
 */
function resolve_position_size_usd(array $settings, float $currentBalance, array $signal = [], ?string $traderAddress = null): float
{
    $mode = $settings['sizing_mode'] ?? 'fixed';

    // --- percent_balance ---
    if ($mode === 'percent_balance') {
        $pct = (float) ($settings['sizing_percent'] ?? 10);
        return apply_position_notional_limits(round($currentBalance * ($pct / 100), 4), $settings, $signal);
    }

    // --- proportional ---
    if ($mode === 'proportional') {
        $traderSizeUsd  = $signal['trader_size_usd'] ?? null;
        $traderAddress  = $traderAddress ?? ($settings['_trader_address'] ?? null);

        if ($traderSizeUsd === null || $traderAddress === null) {
            bot_log('PROPORTIONAL: нет trader_size_usd или адреса трейдера — fallback на fixed');
            return apply_position_notional_limits((float) ($settings['sizing_fixed_usd'] ?? 100), $settings, $signal);
        }

        $traderEquity = fetch_trader_equity($traderAddress);

        if ($traderEquity === null || $traderEquity <= 0) {
            bot_log('PROPORTIONAL: не удалось получить equity трейдера — fallback на fixed');
            return apply_position_notional_limits((float) ($settings['sizing_fixed_usd'] ?? 100), $settings, $signal);
        }

        // trader_size_usd — это NOTIONAL позиции трейдера (size*price), а equity —
        // его капитал. Значит traderRatio — это доля НОТИОНАЛА к капиталу (по сути
        // плечо трейдера в этой сделке), а НЕ доля маржи.
        $traderRatio = $traderSizeUsd / $traderEquity;

        // Зеркалим именно ЭКСПОЗИЦИЮ (notional): целевой notional = баланс * ratio.
        // Раньше эта величина бралась как наша МАРЖА, а сверху ещё умножалась на
        // наше плечо в PnL — двойной учёт плеча. Теперь маржа = notional / плечо.
        $targetNotional = $currentBalance * $traderRatio;
        $leverage       = max(1, (int) ($signal['leverage'] ?? $settings['default_leverage'] ?? 1));
        $sizeUsd        = round($targetNotional / $leverage, 4);

        // Ограничиваем: минимум $1, максимум 95% баланса как маржа (защита от перегруза)
        $sizeUsd = max(1.0, min($sizeUsd, $currentBalance * 0.95));

        bot_log(sprintf(
            'PROPORTIONAL: trader_notional=$%.2f equity=$%.2f exposure=%.4f%% lev=%d our_balance=$%.2f → margin=$%.2f',
            $traderSizeUsd, $traderEquity, $traderRatio * 100, $leverage, $currentBalance, $sizeUsd
        ));

        return apply_position_notional_limits($sizeUsd, $settings, $signal);
    }

    // --- fixed (default) ---
    return apply_position_notional_limits((float) ($settings['sizing_fixed_usd'] ?? 100), $settings, $signal);
}

// ---------------------------------------------------------------
// Лимиты объёма НАШЕЙ позиции (position_min_usd / position_max_usd)
// ---------------------------------------------------------------

/**
 * Наше плечо для пересчёта нотионал↔маржа: из сигнала (кошельки — engine уже
 * учёл symbol_leverage/default_leverage), иначе из настроек (пулы передают
 * плечо трейдера через default_leverage в лимит-настройках).
 */
function position_limits_leverage(array $settings, array $signal = []): int
{
    return max(1, (int) ($signal['leverage'] ?? $settings['default_leverage'] ?? 1));
}

/**
 * Применяет лимиты объёма НАШЕЙ позиции к рассчитанной марже. Лимиты заданы в
 * НОТИОНАЛЕ (маржа × наше плечо) — это размер ордера, как его видит биржа.
 * Приоритет над любым sizing_mode: меньше минимума — поднимаем до минимума,
 * больше максимума — срезаем. Max применяется последним (жёсткий потолок
 * риска побеждает при min > max). 0/отсутствие = лимит выключен.
 *
 * @param float $sizeUsd маржа в USD (результат сайзинга)
 * @return float         маржа в USD после лимитов
 */
function apply_position_notional_limits(float $sizeUsd, array $settings, array $signal = []): float
{
    $minNotional = (float) ($settings['position_min_usd'] ?? 0);
    $maxNotional = (float) ($settings['position_max_usd'] ?? 0);
    if ($minNotional <= 0 && $maxNotional <= 0) {
        return $sizeUsd;
    }

    $leverage = position_limits_leverage($settings, $signal);
    $notional = $sizeUsd * $leverage;
    $original = $notional;

    if ($minNotional > 0 && $notional < $minNotional) {
        $notional = $minNotional;
    }
    if ($maxNotional > 0 && $notional > $maxNotional) {
        $notional = $maxNotional;
    }

    if ($notional !== $original) {
        bot_log(sprintf('POSITION LIMITS: notional %.2f → %.2f USD (min=%.2f max=%.2f lev=%d)',
            $original, $notional, $minNotional, $maxNotional, $leverage));
    }

    return round($notional / $leverage, 4);
}

/**
 * Минимальная МАРЖА (USD), эквивалентная position_min_usd (нотионал) при данном
 * плече. Нужна трекерам: если свободного баланса меньше — сделка пропускается
 * (открывать мельче минимума нельзя, биржа отклонит такой ордер).
 * 0 = минимум не задан.
 */
function position_min_margin_usd(array $settings, int $leverage): float
{
    $minNotional = (float) ($settings['position_min_usd'] ?? 0);
    return $minNotional > 0 ? round($minNotional / max(1, $leverage), 4) : 0.0;
}

/**
 * Рассчитывает комиссию в USD для одной стороны сделки.
 */
function calculate_fee_usd(float $notional, array $settings): float
{
    $feePct = (float) ($settings['fee_pct_per_side'] ?? COST_DEFAULT_FEE_PCT);
    return round($notional * ($feePct / 100), 4);
}

// ---------------------------------------------------------------
// Резервирование маржи (единый источник для кошельков И пулов)
// ---------------------------------------------------------------

/**
 * Суммарная зарезервированная маржа = сумма size_usd всех открытых позиций
 * (margin без плеча). size_usd — единственный источник правды для резерва,
 * отдельного поля reserved_margin намеренно нет (чтобы не рассинхронить).
 *
 * @param array $positions карта позиций [symbol => ['size_usd' => ..., ...], ...]
 */
function get_reserved_margin(array $positions): float
{
    $reserved = 0.0;
    foreach ($positions as $pos) {
        $reserved += (float) ($pos['size_usd'] ?? 0);
    }
    return round($reserved, 4);
}

/**
 * Свободный баланс = текущий баланс − зарезервированная маржа открытых позиций.
 * Только из этой суммы можно открывать новые позиции — иначе одновременные
 * позиции (особенно при percent_balance/proportional, где каждая считается от
 * полного баланса) дали бы суммарную экспозицию кратно больше депозита и
 * нереалистично завышали бы и доходность, и риск-ёмкость симулятора.
 */
function get_free_balance(float $balance, array $positions): float
{
    return round($balance - get_reserved_margin($positions), 4);
}

// ---------------------------------------------------------------
// apply_metrics_to_balance_state — ЕДИНЫЙ источник правды
// ---------------------------------------------------------------

/**
 * Применяет метрики закрытой сделки к balance-состоянию В ПАМЯТИ.
 * Чистая функция: не делает I/O, принимает старый state + метрики,
 * возвращает новый state. Используется И position_tracker, И pool_tracker,
 * И apply_trade_to_balance() — чтобы пересчёт баланса жил в одном месте
 * и не расходился между трекерами.
 *
 * @param array $balanceState текущее состояние (balance, peak_balance, ...)
 * @param array $tradeMetrics ['net_pnl', 'gross_pnl', 'entry_fee', 'exit_fee', 'funding']
 * @return array обновлённое и округлённое состояние
 */
function apply_metrics_to_balance_state(array $balanceState, array $tradeMetrics): array
{
    $balanceState['balance']         += $tradeMetrics['net_pnl'];
    $balanceState['balance']          = max(0, $balanceState['balance']); // баланс не может быть < 0
    $balanceState['total_gross_pnl'] += $tradeMetrics['gross_pnl'];
    $balanceState['total_net_pnl']   += $tradeMetrics['net_pnl'];
    $balanceState['total_fees']      += ($tradeMetrics['entry_fee'] + $tradeMetrics['exit_fee']);
    $balanceState['total_funding']   += ($tradeMetrics['funding'] ?? 0.0);
    $balanceState['trades_count']    += 1;

    // Winrate считаем по net (выиграл ли я после комиссий).
    // Profit Factor — по gross (классическое определение: валовая прибыль / валовой убыток,
    // до вычета комиссий). Раньше здесь ошибочно использовался net_pnl для обеих сумм.
    if ($tradeMetrics['net_pnl'] > 0) {
        $balanceState['wins_count'] += 1;
    }
    if ($tradeMetrics['gross_pnl'] > 0) {
        $balanceState['gross_profit_sum'] += $tradeMetrics['gross_pnl'];
    } elseif ($tradeMetrics['gross_pnl'] < 0) {
        $balanceState['gross_loss_sum']   += abs($tradeMetrics['gross_pnl']);
    }

    if ($balanceState['balance'] > $balanceState['peak_balance']) {
        $balanceState['peak_balance'] = $balanceState['balance'];
    }

    if ($balanceState['peak_balance'] > 0) {
        $currentDrawdownPct = (($balanceState['peak_balance'] - $balanceState['balance'])
                                / $balanceState['peak_balance']) * 100;
        if ($currentDrawdownPct > $balanceState['max_drawdown_pct']) {
            $balanceState['max_drawdown_pct'] = round($currentDrawdownPct, 4);
        }
    }

    $balanceState['balance']         = round($balanceState['balance'],         4);
    $balanceState['total_gross_pnl'] = round($balanceState['total_gross_pnl'], 4);
    $balanceState['total_net_pnl']   = round($balanceState['total_net_pnl'],   4);
    $balanceState['total_fees']      = round($balanceState['total_fees'],      4);
    $balanceState['total_funding']   = round($balanceState['total_funding'],   4);

    return $balanceState;
}

// ---------------------------------------------------------------
// apply_trade_to_balance
// ---------------------------------------------------------------

/**
 * Применяет результат закрытой сделки к виртуальному балансу.
 * Читает текущий баланс, пересчитывает метрики, сохраняет обратно.
 * Возвращает обновлённый balanceState.
 *
 * Примечание: при закрытии позиции через position_tracker рекомендуется
 * использовать storage_close_trade_atomic() — тогда save_balance() внутри
 * не вызывается отдельно, а идёт внутри транзакции.
 */
function apply_trade_to_balance(string $entityType, string $entityId, array $settings, array $tradeMetrics): array
{
    $balanceState = load_balance($entityType, $entityId, $settings);
    $balanceState = apply_metrics_to_balance_state($balanceState, $tradeMetrics);

    // Сохраняем только если вызывается НЕ из транзакции (например, watching_tracker).
    // При закрытии из position_tracker/pool_tracker сохранение идёт через
    // storage_close_trade_atomic() — вызывающий код сам решает когда сохранять.
    save_balance($entityType, $entityId, $balanceState);

    return $balanceState;
}

// ---------------------------------------------------------------
// compute_account_metrics (логика не изменилась)
// ---------------------------------------------------------------

/**
 * Производные метрики виртуального счёта для dashboard.
 */
function compute_account_metrics(array $balanceState, array $settings): array
{
    $startBalance   = (float) ($settings['virtual_balance'] ?? 1000);
    $totalReturnPct = $startBalance > 0
        ? round((($balanceState['balance'] - $startBalance) / $startBalance) * 100, 4)
        : 0.0;

    $winrate = $balanceState['trades_count'] > 0
        ? round(($balanceState['wins_count'] / $balanceState['trades_count']) * 100, 2)
        : 0.0;

    $profitFactor = $balanceState['gross_loss_sum'] > 0
        ? round($balanceState['gross_profit_sum'] / $balanceState['gross_loss_sum'], 4)
        : ($balanceState['gross_profit_sum'] > 0 ? null : 0.0);

    return [
        'current_balance'  => $balanceState['balance'],
        'total_return_pct' => $totalReturnPct,
        'max_drawdown_pct' => $balanceState['max_drawdown_pct'],
        // Mark-to-market просадка (по эквити с нереализованным PnL открытых позиций) —
        // отражает реальный риск глубже, чем реализованный max_drawdown_pct.
        'mtm_max_drawdown_pct' => $balanceState['mtm_max_drawdown_pct'] ?? 0.0,
        'peak_balance'     => $balanceState['peak_balance'],
        'profit_factor'    => $profitFactor,
        'winrate'          => $winrate,
        'trades_count'     => $balanceState['trades_count'],
        'total_gross_pnl'  => $balanceState['total_gross_pnl'],
        'total_net_pnl'    => $balanceState['total_net_pnl'],
        'total_fees'       => $balanceState['total_fees'],
        'total_funding'    => $balanceState['total_funding'],
    ];
}
