<?php
// =============================================================
// DATA — агрегация статистики симулятора
// =============================================================

require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../logger.php';
require_once __DIR__ . '/../storage.php';
require_once __DIR__ . '/../balance_tracker.php';
require_once __DIR__ . '/../price_fetcher.php';

/**
 * Читает trades_closed.csv и возвращает массив сделок.
 * Поддерживает как новый формат (gross_pnl/net_pnl/fees/slippage),
 * так и старый формат CSV (pnl_usd/pnl_pct) — для обратной совместимости
 * со строками, записанными до перехода на симулятор.
 *
 * @param string|null $walletId если задан — только сделки этого кошелька
 */
function load_closed_trades(?string $walletId = null): array
{
    return storage_get_closed_trades($walletId);
}

/**
 * Приводит строку CSV (новый или старый формат) к единому набору ключей,
 * которым пользуется весь остальной dashboard-код:
 * gross_pnl, net_pnl, entry_fee, exit_fee, funding, pnl_pct,
 * entry_slippage_pct, exit_slippage_pct.
 *
 * Старые строки (записанные до симулятора) не содержат gross_pnl/net_pnl —
 * для них net_pnl = старое pnl_usd, fees/funding/slippage = 0.
 */
function normalize_trade_row(array $trade): array
{
    if (!isset($trade['net_pnl'])) {
        // Старый формат: pnl_usd, pnl_pct, size, без fees/slippage
        $legacyPnl = (float) ($trade['pnl_usd'] ?? 0);
        $trade['gross_pnl'] = $legacyPnl;
        $trade['net_pnl'] = $legacyPnl;
        $trade['entry_fee'] = 0.0;
        $trade['exit_fee'] = 0.0;
        $trade['funding'] = 0.0;
        $trade['entry_slippage_pct'] = '';
        $trade['exit_slippage_pct'] = '';
        $trade['trader_entry_price'] = '';
        $trade['size_usd'] = $trade['size'] ?? 0;
    }

    return $trade;
}

/**
 * Читает сигналы из SQLite и возвращает массив.
 * @param string|null $walletId если задан — только сигналы этого кошелька
 */
function load_signals(?string $walletId = null): array
{
    return storage_get_signals($walletId);
}

/**
 * Читает открытые позиции по всем кошелькам (или одному, если указан walletId).
 * Возвращает массив записей вида:
 *   ['wallet_id'=>..., 'wallet_label'=>..., 'symbol'=>..., 'side'=>..., 'entry_price'=>..., 'size_usd'=>..., ...]
 *
 * @param array       $wallets  список кошельков (из load_wallets_config()['wallets'])
 * @param string|null $walletId если задан — только позиции этого кошелька
 */
function load_open_positions(array $wallets, ?string $walletId = null): array
{
    $result = [];

    foreach ($wallets as $w) {
        if ($walletId !== null && $w['id'] !== $walletId) {
            continue;
        }

        $positions = storage_load_positions('wallet', $w['id']);
        foreach ($positions as $symbol => $pos) {
            $pos['symbol']        = $symbol;
            $pos['wallet_id']     = $w['id'];
            $pos['wallet_label']  = $w['label'] ?? $w['id'];
            $pos['wallet_rating'] = (int) ($w['rating'] ?? 0);
            $result[] = $pos;
        }
    }

    return $result;
}

/**
 * Плоский список активных watching-записей (отложенный вход по просадке)
 * по всем кошелькам — для отображения в dashboard. Аналог load_open_positions().
 * Каждая запись дополняется symbol, wallet_id, wallet_label и возрастом в секундах.
 */
function load_watching_positions(array $wallets, ?string $walletId = null): array
{
    $result = [];
    $now    = time();

    foreach ($wallets as $w) {
        if ($walletId !== null && $w['id'] !== $walletId) {
            continue;
        }

        $watching = storage_load_watching('wallet', $w['id']);
        foreach ($watching as $symbol => $entry) {
            $entry['symbol']        = $symbol;
            $entry['wallet_id']     = $w['id'];
            $entry['wallet_label']  = $w['label'] ?? $w['id'];
            $entry['wallet_rating'] = (int) ($w['rating'] ?? 0);
            $entry['age_seconds']   = max(0, $now - (int) ($entry['started_at'] ?? $now));
            $result[] = $entry;
        }
    }

    return $result;
}

/**
 * Читает виртуальный счёт (balance state) одного кошелька.
 * Если файла нет — возвращает баланс на основе стартового virtual_balance из settings.
 */
function load_wallet_balance_state(string $walletId, array $effectiveSettings): array
{
    return load_balance('wallet', $walletId, $effectiveSettings);
}

// ---------------------------------------------------------------
// Фильтр по периоду (dashboard: 7д / 30д / 90д / всё время)
// ---------------------------------------------------------------

/**
 * Timestamp начала периода для фильтра дашборда.
 * '7d'|'30d'|'90d' -> time() - N дней; 'all' (и всё прочее) -> null (без фильтра).
 */
function period_from_ts(string $period): ?int
{
    $days = ['7d' => 7, '30d' => 30, '90d' => 90][$period] ?? null;
    return $days !== null ? time() - $days * 86400 : null;
}

/**
 * Оставляет сделки, закрытые не раньше $fromTs. null = без фильтра.
 */
function filter_trades_since(array $trades, ?int $fromTs): array
{
    if ($fromTs === null) {
        return $trades;
    }
    return array_values(array_filter(
        $trades,
        fn($t) => strtotime($t['close_time'] ?? '') >= $fromTs
    ));
}

/**
 * Оставляет сигналы не старше $fromTs (по полю datetime). null = без фильтра.
 */
function filter_signals_since(array $signals, ?int $fromTs): array
{
    if ($fromTs === null) {
        return $signals;
    }
    return array_values(array_filter(
        $signals,
        fn($s) => strtotime($s['datetime'] ?? '') >= $fromTs
    ));
}

/**
 * Сводная таблица по всем кошелькам для главной страницы dashboard:
 * Wallet, Score, Balance, ROI, Net PnL, Winrate, Max Drawdown, Signals, Activity.
 *
 * Баланс/ROI/просадки — всегда за всё время (это состояние счёта);
 * Net PnL / Winrate / кол-во сделок и сигналов при заданном $fromTs
 * пересчитываются по сделкам, закрытым внутри периода.
 * Score и активность считаются по полной истории.
 *
 * @param array    $wallets        список кошельков (из load_wallets_config())
 * @param array    $globalSettings глобальные дефолты настроек
 * @param int|null $fromTs         начало периода (см. period_from_ts) или null = всё время
 */
function build_wallets_overview(array $wallets, array $globalSettings, ?int $fromTs = null): array
{
    $overview = [];

    foreach ($wallets as $w) {
        $settings = resolve_wallet_settings($w, $globalSettings);
        $balanceState = load_wallet_balance_state($w['id'], $settings);
        $metrics = compute_account_metrics($balanceState, $settings);

        $wTrades  = storage_get_closed_trades($w['id']);
        $score    = calculate_wallet_score($wTrades);
        $activity = check_wallet_activity($wTrades);

        $signals = load_signals($w['id']);

        $netPnl      = $metrics['total_net_pnl'];
        $winrate     = $metrics['winrate'];
        $tradesCount = $metrics['trades_count'];

        if ($fromTs !== null) {
            $periodTrades  = filter_trades_since($wTrades, $fromTs);
            $periodSummary = calculate_summary($periodTrades);
            $netPnl      = $periodSummary['total_net_pnl'];
            $winrate     = $periodSummary['win_rate'];
            $tradesCount = $periodSummary['total_trades'];
            $signals     = filter_signals_since($signals, $fromTs);
        }

        $overview[] = [
            'wallet_id'         => $w['id'],
            'address'           => $w['address'] ?? '',
            'label'             => $w['label'] ?? $w['id'],
            'enabled'           => !empty($w['enabled']),
            'rating'            => (int) ($w['rating'] ?? 0),
            'tags'              => $w['tags'] ?? [],
            'balance'           => $metrics['current_balance'],
            'roi_pct'           => $metrics['total_return_pct'],
            'net_pnl'           => $netPnl,
            'gross_pnl'         => $metrics['total_gross_pnl'],
            'winrate'           => $winrate,
            'max_drawdown_pct'  => $metrics['max_drawdown_pct'],
            'mtm_max_drawdown_pct' => $metrics['mtm_max_drawdown_pct'] ?? 0.0,
            'profit_factor'     => $metrics['profit_factor'],
            'trades_count'      => $tradesCount,
            'total_fees'        => $metrics['total_fees'],
            'signals_count'     => count($signals),
            'score'             => $score['score'],
            'grade'             => $score['grade'],
            'active'            => $activity['active'],
            'days_silent'       => $activity['days_silent'],
            'last_trade_at'     => $activity['last_trade_at'],
        ];
    }

    return $overview;
}

/**
 * Дневная серия Net PnL за последние $days дней (включая сегодня).
 * Возвращает ['labels' => ['MM-DD', ...], 'usd' => [float, ...]] — ровно
 * $days точек, дни без сделок = 0. Используется для спарклайнов
 * (дашборд, реестр трейдеров) и графика «Дневной PnL» карточки трейдера.
 */
function daily_pnl_series(array $trades, int $days = 14): array
{
    $byDay = [];
    foreach ($trades as $t) {
        $day = date('Y-m-d', strtotime($t['close_time'] ?? ''));
        $byDay[$day] = ($byDay[$day] ?? 0.0) + (float) ($t['net_pnl'] ?? 0);
    }

    $labels = [];
    $usd    = [];
    for ($i = $days - 1; $i >= 0; $i--) {
        $day      = date('Y-m-d', time() - $i * 86400);
        $labels[] = substr($day, 5); // MM-DD
        $usd[]    = round($byDay[$day] ?? 0.0, 2);
    }

    return ['labels' => $labels, 'usd' => $usd];
}

/**
 * Средний слиппедж (entry+exit) по набору сделок, в процентах.
 * Игнорирует сделки без данных о цене трейдера (slippage = '').
 */
function calculate_average_slippage(array $trades): float
{
    $sum = 0.0;
    $count = 0;

    foreach ($trades as $t) {
        foreach (['entry_slippage_pct', 'exit_slippage_pct'] as $field) {
            if (isset($t[$field]) && $t[$field] !== '') {
                $sum += (float) $t[$field];
                $count++;
            }
        }
    }

    return $count > 0 ? round($sum / $count, 4) : 0.0;
}

/**
 * Среднее время удержания позиции в секундах.
 */
function calculate_average_holding_time(array $trades): int
{
    if (empty($trades)) {
        return 0;
    }

    $sum = 0;
    foreach ($trades as $t) {
        $sum += strtotime($t['close_time']) - strtotime($t['open_time']);
    }

    return (int) round($sum / count($trades));
}

/**
 * Проверяет состояние cron и бота по двум отметкам:
 *  - CRON_TICK_FILE — факт запуска bot.php кроном (пишется даже когда запуск
 *    пропущен из-за lock) → жив ли сам cron;
 *  - HEARTBEAT_FILE — последний завершённый цикл → успевает ли бот работать.
 *
 * Возвращает:
 *  'ok'               => bool      бот завершает циклы (heartbeat свежий)
 *  'last_run'         => int|null  timestamp последнего завершённого цикла
 *  'seconds_ago'      => int|null
 *  'cron_ok'          => bool|null жив ли cron; null = отметка тика ещё не
 *                                  писалась (старая установка / первый запуск)
 *  'last_tick'        => int|null  timestamp последнего запуска bot.php
 *  'tick_seconds_ago' => int|null
 */
function check_cron_health(int $thresholdSeconds = 420, int $tickThresholdSeconds = 180): array
{
    $lastRun = $secondsAgo = null;
    if (file_exists(HEARTBEAT_FILE)) {
        $lastRun    = (int) trim(file_get_contents(HEARTBEAT_FILE));
        $secondsAgo = time() - $lastRun;
    }

    $lastTick = $tickSecondsAgo = $cronOk = null;
    if (defined('CRON_TICK_FILE') && file_exists(CRON_TICK_FILE)) {
        $lastTick       = (int) trim(file_get_contents(CRON_TICK_FILE));
        $tickSecondsAgo = time() - $lastTick;
        $cronOk         = $tickSecondsAgo <= $tickThresholdSeconds;
    }

    return [
        'ok'               => $secondsAgo !== null && $secondsAgo <= $thresholdSeconds,
        'last_run'         => $lastRun,
        'seconds_ago'      => $secondsAgo,
        'cron_ok'          => $cronOk,
        'last_tick'        => $lastTick,
        'tick_seconds_ago' => $tickSecondsAgo,
    ];
}

/**
 * Общая сводка по всем закрытым сделкам (gross/net PnL, winrate, средние метрики).
 * Используется для блока "Сводка" на dashboard (по выбранному кошельку или всем сразу).
 */
function calculate_summary(array $trades): array
{
    $totalGrossPnl = 0.0;
    $totalNetPnl = 0.0;
    $totalPnlPctSum = 0.0;
    $totalFees = 0.0;
    $totalFunding = 0.0;
    $wins = 0;
    $losses = 0;

    foreach ($trades as $t) {
        $grossPnl = (float) ($t['gross_pnl'] ?? 0);
        $netPnl = (float) ($t['net_pnl'] ?? 0);
        $pnlPct = (float) ($t['pnl_pct'] ?? 0);

        $totalGrossPnl += $grossPnl;
        $totalNetPnl += $netPnl;
        $totalPnlPctSum += $pnlPct;
        $totalFees += (float) ($t['entry_fee'] ?? 0) + (float) ($t['exit_fee'] ?? 0);
        $totalFunding += (float) ($t['funding'] ?? 0);

        // Победа считается по net (после комиссий), строго > 0 — единое
        // определение с balance_tracker (apply_metrics_to_balance_state),
        // иначе winrate в сводке расходился бы с таблицей кошельков.
        if ($netPnl > 0) {
            $wins++;
        } else {
            $losses++;
        }
    }

    $count = count($trades);
    // 2 знака — как compute_account_metrics, чтобы winrate в сводке и в таблице
    // кошельков совпадал не только по определению (net>0), но и по значению.
    $winRate = $count > 0 ? round(($wins / $count) * 100, 2) : 0;

    // Profit factor: валовая прибыль / валовой убыток по GROSS (до комиссий) —
    // классическое определение, единое с balance_tracker/compute_account_metrics.
    $grossProfit = 0.0;
    $grossLoss   = 0.0;
    foreach ($trades as $t) {
        $pnl = (float) ($t['gross_pnl'] ?? 0);
        if     ($pnl > 0) $grossProfit += $pnl;
        elseif ($pnl < 0) $grossLoss   += abs($pnl);
    }
    $profitFactor = $grossLoss > 0
        ? round($grossProfit / $grossLoss, 4)
        : ($grossProfit > 0 ? null : 0.0);

    return [
        'total_trades'    => $count,
        'total_gross_pnl' => round($totalGrossPnl, 2),
        'total_net_pnl'   => round($totalNetPnl, 2),
        'total_fees'      => round($totalFees, 2),
        'total_funding'   => round($totalFunding, 2),
        'avg_pnl_pct'     => $count > 0 ? round($totalPnlPctSum / $count, 2) : 0,
        'avg_slippage'    => calculate_average_slippage($trades),
        'avg_holding_sec' => calculate_average_holding_time($trades),
        'wins'            => $wins,
        'losses'          => $losses,
        'win_rate'        => $winRate,
        'profit_factor'   => $profitFactor,
    ];
}

/**
 * Группирует Net PnL по периодам (day / week / month).
 * Возвращает массив ['label' => string, 'pnl_usd' => float, 'pnl_pct' => float, 'trades' => int]
 * отсортированный по дате (старые → новые).
 */
function group_pnl_by_period(array $trades, string $period): array
{
    $groups = [];

    foreach ($trades as $t) {
        $closeTime = strtotime($t['close_time']);
        $netPnl = (float) ($t['net_pnl'] ?? 0);
        $pnlPct = (float) ($t['pnl_pct'] ?? 0);

        switch ($period) {
            case 'day':
                $key = date('Y-m-d', $closeTime);
                break;
            case 'week':
                // ISO неделя: год-неделя
                $key = date('o-\WW', $closeTime);
                break;
            case 'month':
                $key = date('Y-m', $closeTime);
                break;
            default:
                $key = date('Y-m-d', $closeTime);
        }

        if (!isset($groups[$key])) {
            $groups[$key] = ['pnl_usd' => 0.0, 'pnl_pct_sum' => 0.0, 'trades' => 0];
        }

        $groups[$key]['pnl_usd'] += $netPnl;
        $groups[$key]['pnl_pct_sum'] += $pnlPct;
        $groups[$key]['trades']++;
    }

    ksort($groups);

    $result = [];
    foreach ($groups as $label => $g) {
        $result[] = [
            'label'   => $label,
            'pnl_usd' => round($g['pnl_usd'], 2),
            'pnl_pct' => round($g['pnl_pct_sum'] / $g['trades'], 2), // средний ROI за период
            'trades'  => $g['trades'],
        ];
    }

    return $result;
}

/**
 * Накопительный Net PnL по всем сделкам в хронологическом порядке.
 * Используется для графика "за всё время" — каждая точка = одна закрытая сделка,
 * показывает суммарный Net PnL ($) и средний накопительный ROI (%) на этот момент.
 * Возвращает массив ['label' => string, 'pnl_usd' => float, 'pnl_pct' => float]
 */
function cumulative_pnl(array $trades): array
{
    // trades приходят уже в хронологическом порядке (как записаны в CSV)
    $result = [];
    $runningUsd = 0.0;
    $runningPctSum = 0.0;
    $count = 0;

    foreach ($trades as $t) {
        $count++;
        $runningUsd += (float) ($t['net_pnl'] ?? 0);
        $runningPctSum += (float) ($t['pnl_pct'] ?? 0);

        $result[] = [
            'label'   => date('Y-m-d H:i', strtotime($t['close_time'])),
            'pnl_usd' => round($runningUsd, 2),
            'pnl_pct' => round($runningPctSum / $count, 2), // средний ROI накопительно
            'trades'  => $count,
        ];
    }

    return $result;
}

// ---------------------------------------------------------------
// Автоскоринг кошелька (0-100)
// ---------------------------------------------------------------

/**
 * Считает автоматический скоринг кошелька на основе его торговой истории.
 * Возвращает ['score' => int, 'breakdown' => [...], 'grade' => 'A'|'B'|'C'|'D'|'F']
 */
function calculate_wallet_score(array $trades): array
{
    if (count($trades) < 5) {
        return ['score' => null, 'breakdown' => [], 'grade' => '?', 'reason' => 'Мало сделок (< 5)'];
    }

    $summary = calculate_summary($trades);
    $breakdown = [];
    $total = 0;

    // Win Rate (макс 25 баллов)
    $wr = $summary['win_rate'];
    $wrScore = min(25, (int) ($wr / 100 * 25));
    $breakdown['win_rate'] = ['value' => $wr . '%', 'score' => $wrScore, 'max' => 25];
    $total += $wrScore;

    // Profit Factor (макс 25 баллов)
    // null = убыточных сделок не было вовсе (валовой убыток = 0) при наличии прибыли —
    // это идеальный показатель, а не ноль. Раньше `?? 0` штрафовал такой кошелёк в 0 баллов.
    $pf = $summary['profit_factor'];
    if ($pf === null)     { $pfScore = 25; $pfLabel = '∞'; }
    elseif ($pf >= 2.0)   { $pfScore = 25; $pfLabel = number_format($pf, 2); }
    elseif ($pf >= 1.5)   { $pfScore = 18; $pfLabel = number_format($pf, 2); }
    elseif ($pf >= 1.2)   { $pfScore = 12; $pfLabel = number_format($pf, 2); }
    elseif ($pf >= 1.0)   { $pfScore = 6;  $pfLabel = number_format($pf, 2); }
    else                  { $pfScore = 0;  $pfLabel = number_format($pf, 2); }
    $breakdown['profit_factor'] = ['value' => $pfLabel, 'score' => $pfScore, 'max' => 25];
    $total += $pfScore;

    // Max Drawdown (макс 25 баллов) — чем меньше тем лучше
    // Считаем из кривой капитала
    $equityData = cumulative_pnl($trades);
    $peak = 0; $maxDd = 0;
    foreach ($equityData as $eq) {
        $val = $eq['pnl_usd'] ?? 0; // cumulative_pnl возвращает массив объектов
        if ($val > $peak) $peak = $val;
        if ($peak > 0) {
            $dd = ($peak - $val) / $peak * 100;
            if ($dd > $maxDd) $maxDd = $dd;
        }
    }
    if ($maxDd <= 10)      $ddScore = 25;
    elseif ($maxDd <= 20)  $ddScore = 18;
    elseif ($maxDd <= 35)  $ddScore = 10;
    elseif ($maxDd <= 50)  $ddScore = 4;
    else                   $ddScore = 0;
    $breakdown['max_drawdown'] = ['value' => number_format($maxDd, 1) . '%', 'score' => $ddScore, 'max' => 25];
    $total += $ddScore;

    // Количество сделок (макс 15 баллов) — опыт
    $cnt = count($trades);
    if ($cnt >= 100)      $cntScore = 15;
    elseif ($cnt >= 50)   $cntScore = 10;
    elseif ($cnt >= 20)   $cntScore = 6;
    elseif ($cnt >= 10)   $cntScore = 3;
    else                  $cntScore = 1;
    $breakdown['trades_count'] = ['value' => $cnt, 'score' => $cntScore, 'max' => 15];
    $total += $cntScore;

    // Среднее удержание (макс 10 баллов) — фильтр скальперов с высоким шумом
    $avgHold = calculate_average_holding_time($trades); // в секундах
    $avgHoldH = $avgHold / 3600;
    if ($avgHoldH >= 4)        $holdScore = 10;
    elseif ($avgHoldH >= 1)    $holdScore = 7;
    elseif ($avgHoldH >= 0.25) $holdScore = 4;
    else                       $holdScore = 1;
    $breakdown['avg_hold'] = ['value' => number_format($avgHoldH, 1) . 'h', 'score' => $holdScore, 'max' => 10];
    $total += $holdScore;

    // Грейд
    if ($total >= 80)      $grade = 'A';
    elseif ($total >= 65)  $grade = 'B';
    elseif ($total >= 45)  $grade = 'C';
    elseif ($total >= 25)  $grade = 'D';
    else                   $grade = 'F';

    return ['score' => $total, 'breakdown' => $breakdown, 'grade' => $grade, 'reason' => ''];
}

/**
 * Проверяет активность кошелька.
 * Возвращает ['active' => bool, 'days_silent' => int, 'last_trade_at' => string|null]
 */
function check_wallet_activity(array $trades, int $deadDays = 14): array
{
    if (empty($trades)) {
        return ['active' => false, 'days_silent' => 999, 'last_trade_at' => null];
    }

    // Берём последнюю сделку
    $lastTrade = end($trades);
    $lastTs    = strtotime($lastTrade['close_time']);
    $daysSilent = (int) ((time() - $lastTs) / 86400);

    return [
        'active'        => $daysSilent < $deadDays,
        'days_silent'   => $daysSilent,
        'last_trade_at' => $lastTrade['close_time'],
    ];
}

/**
 * Текущая нереализованная Gross PnL по открытым позициям (на текущей цене).
 * Делает ОДИН запрос allMids на весь набор позиций (один раз за цикл, кэш в price_fetcher).
 * Это gross-оценка (без вычета exit fee, которая ещё не наступила) — для dashboard
 * достаточно ориентировочного значения "сколько сейчас плывёт".
 *
 * @param array $positions результат load_open_positions() — плоский список
 */
function calculate_unrealized_pnl(array $positions): array
{
    $result = [];

    if (empty($positions)) {
        return $result;
    }

    $prices = fetch_all_mids() ?? [];

    foreach ($positions as $pos) {
        $symbol = $pos['symbol'];
        $currentPrice = $prices[$symbol] ?? null;

        if ($currentPrice === null) {
            $result[] = array_merge($pos, [
                'current_price' => null,
                'pnl_usd'       => null,
                'pnl_pct'       => null,
            ]);
            continue;
        }

        $entryPrice = (float) $pos['entry_price'];
        $sizeUsd    = (float) ($pos['size_usd'] ?? 0);
        $leverage   = (float) ($pos['leverage'] ?? 1);
        $direction  = ($pos['side'] === 'long') ? 1 : -1;

        $priceChangePct = $entryPrice > 0 ? ($currentPrice - $entryPrice) / $entryPrice : 0;
        $pnlUsd = $sizeUsd * $priceChangePct * $direction * $leverage;
        $pnlPct = $priceChangePct * $direction * $leverage * 100;

        // Изолированная маржа: убыток не может превысить отложенную маржу (size_usd).
        // Капируем gross-оценку как при закрытии (compute_trade_metrics_core), иначе
        // подводная позиция показывала бы < -100% маржи, расходясь с тем, как та же
        // позиция запишется в историю при фактическом закрытии.
        if ($pnlUsd < -$sizeUsd) {
            $pnlUsd = -$sizeUsd;
            $pnlPct = $sizeUsd > 0 ? -100.0 : 0.0;
        }

        $result[] = array_merge($pos, [
            'current_price' => $currentPrice,
            'pnl_usd'       => round($pnlUsd, 2),
            'pnl_pct'       => round($pnlPct, 2),
        ]);
    }

    return $result;
}
