Files
myapp/analytics.php

543 lines
23 KiB
PHP

<?php
/**
* Forex Trading Analytics Engine (Myfxbook Grade Calculations)
* Precision metrics, risk ratios, drawdown curves, and performance breakdowns.
*/
class AnalyticsEngine {
private PDO $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function getFullAnalytics(int $accountId, ?string $from = null, ?string $to = null, ?string $symbol = null, ?string $strategy = null): array {
// 1. Fetch Account Details
$stmtAcc = $this->db->prepare("SELECT * FROM accounts WHERE id = ?");
$stmtAcc->execute([$accountId]);
$account = $stmtAcc->fetch();
if (!$account) {
return ['error' => 'Account not found'];
}
$initialBalance = (float)$account['initial_balance'];
// 2. Fetch Trades Query Filtered
$sql = "SELECT * FROM trades WHERE account_id = ?";
$params = [$accountId];
if ($from) {
$sql .= " AND open_time >= ?";
$params[] = $from . ' 00:00:00';
}
if ($to) {
$sql .= " AND open_time <= ?";
$params[] = $to . ' 23:59:59';
}
if ($symbol && $symbol !== 'ALL') {
$sql .= " AND symbol = ?";
$params[] = $symbol;
}
if ($strategy && $strategy !== 'ALL') {
$sql .= " AND strategy = ?";
$params[] = $strategy;
}
$sql .= " ORDER BY open_time ASC, id ASC";
$stmtTrades = $this->db->prepare($sql);
$stmtTrades->execute($params);
$trades = $stmtTrades->fetchAll();
// 3. Process Metrics & Aggregations
$totalTrades = count($trades);
$closedTrades = [];
$openTrades = [];
$grossProfit = 0.0;
$grossLoss = 0.0;
$totalCommission = 0.0;
$totalSwap = 0.0;
$totalPips = 0.0;
$wonTradesCount = 0;
$lostTradesCount = 0;
$beTradesCount = 0;
$longTradesCount = 0;
$longWonCount = 0;
$longProfit = 0.0;
$shortTradesCount = 0;
$shortWonCount = 0;
$shortProfit = 0.0;
$bestTradeProfit = 0.0;
$worstTradeProfit = 0.0;
$bestTradePips = 0.0;
$worstTradePips = 0.0;
$winPipsSum = 0.0;
$lossPipsSum = 0.0;
$currentBalance = $initialBalance;
$peakBalance = $initialBalance;
$maxDrawdownAmount = 0.0;
$maxDrawdownPercent = 0.0;
$growthCurve = [];
$drawdownCurve = [];
$consecutiveWins = 0;
$consecutiveLosses = 0;
$maxConsecutiveWins = 0;
$maxConsecutiveLosses = 0;
$totalDurationSec = 0;
$winDurationSec = 0;
$lossDurationSec = 0;
$dailyReturns = []; // date => pnl
$symbolStats = [];
$strategyStats = [];
$emotionStats = [];
$sessionStats = ['London' => ['count' => 0, 'profit' => 0, 'wins' => 0], 'New York' => ['count' => 0, 'profit' => 0, 'wins' => 0], 'Asian' => ['count' => 0, 'profit' => 0, 'wins' => 0], 'Overlap' => ['count' => 0, 'profit' => 0, 'wins' => 0]];
$dayOfWeekStats = [
1 => ['name' => 'Monday', 'fa' => 'دوشنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
2 => ['name' => 'Tuesday', 'fa' => 'سه‌شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
3 => ['name' => 'Wednesday', 'fa' => 'چهارشنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
4 => ['name' => 'Thursday', 'fa' => 'پنج‌شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
5 => ['name' => 'Friday', 'fa' => 'جمعه', 'count' => 0, 'profit' => 0, 'wins' => 0],
6 => ['name' => 'Saturday', 'fa' => 'شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
7 => ['name' => 'Sunday', 'fa' => 'یک‌شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
];
$hourlyStats = [];
for ($h = 0; $h < 24; $h++) {
$hourlyStats[$h] = ['hour' => $h, 'count' => 0, 'profit' => 0, 'wins' => 0];
}
// Add starting point to growth curve
$growthCurve[] = [
'date' => date('Y-m-d H:i', strtotime($account['created_at'] ?? 'now')),
'trade_index' => 0,
'balance' => round($initialBalance, 2),
'equity' => round($initialBalance, 2),
'profit' => 0.0,
'gain_percent' => 0.0,
'symbol' => 'START',
'ticket' => '-'
];
$tradeIndex = 0;
foreach ($trades as $t) {
$profit = (float)$t['profit'];
$pips = (float)$t['pips'];
$comm = (float)$t['commission'];
$swap = (float)$t['swap'];
$sym = $t['symbol'];
$strat = $t['strategy'] ?: 'Price Action';
$emo = $t['emotion'] ?: 'Disciplined';
$sess = $t['session'] ?: 'London';
if ($t['status'] === 'open') {
$openTrades[] = $t;
continue;
}
$closedTrades[] = $t;
$tradeIndex++;
$totalCommission += $comm;
$totalSwap += $swap;
$totalPips += $pips;
// Balance & Equity Tracking
$netTradeProfit = $profit + $comm + $swap;
$currentBalance += $netTradeProfit;
if ($currentBalance > $peakBalance) {
$peakBalance = $currentBalance;
}
$currentDDAmount = $peakBalance - $currentBalance;
$currentDDPercent = $peakBalance > 0 ? ($currentDDAmount / $peakBalance) * 100 : 0;
if ($currentDDAmount > $maxDrawdownAmount) {
$maxDrawdownAmount = $currentDDAmount;
}
if ($currentDDPercent > $maxDrawdownPercent) {
$maxDrawdownPercent = $currentDDPercent;
}
$gainPercent = $initialBalance > 0 ? (($currentBalance - $initialBalance) / $initialBalance) * 100 : 0;
$closeDate = $t['close_time'] ?: $t['open_time'];
$growthCurve[] = [
'date' => date('Y-m-d H:i', strtotime($closeDate)),
'trade_index' => $tradeIndex,
'balance' => round($currentBalance, 2),
'equity' => round($currentBalance, 2),
'profit' => round($netTradeProfit, 2),
'gain_percent' => round($gainPercent, 2),
'symbol' => $sym,
'ticket' => $t['ticket'] ?: "#$tradeIndex"
];
$drawdownCurve[] = [
'date' => date('Y-m-d H:i', strtotime($closeDate)),
'trade_index' => $tradeIndex,
'drawdown_percent' => round(-$currentDDPercent, 2),
'drawdown_amount' => round($currentDDAmount, 2)
];
// Win / Loss classification
if ($profit > 0) {
$wonTradesCount++;
$grossProfit += $profit;
$winPipsSum += $pips;
$consecutiveWins++;
$consecutiveLosses = 0;
if ($consecutiveWins > $maxConsecutiveWins) {
$maxConsecutiveWins = $consecutiveWins;
}
if ($profit > $bestTradeProfit) {
$bestTradeProfit = $profit;
}
if ($pips > $bestTradePips) {
$bestTradePips = $pips;
}
} elseif ($profit < 0) {
$lostTradesCount++;
$grossLoss += abs($profit);
$lossPipsSum += abs($pips);
$consecutiveLosses++;
$consecutiveWins = 0;
if ($consecutiveLosses > $maxConsecutiveLosses) {
$maxConsecutiveLosses = $consecutiveLosses;
}
if ($profit < $worstTradeProfit) {
$worstTradeProfit = $profit;
}
if ($pips < $worstTradePips) {
$worstTradePips = $pips;
}
} else {
$beTradesCount++;
}
// Long / Short stats
if (strtolower($t['trade_type']) === 'buy') {
$longTradesCount++;
$longProfit += $netTradeProfit;
if ($profit > 0) $longWonCount++;
} else {
$shortTradesCount++;
$shortProfit += $netTradeProfit;
if ($profit > 0) $shortWonCount++;
}
// Duration calculation
if ($t['open_time'] && $t['close_time']) {
$dur = max(0, strtotime($t['close_time']) - strtotime($t['open_time']));
$totalDurationSec += $dur;
if ($profit > 0) $winDurationSec += $dur;
elseif ($profit < 0) $lossDurationSec += $dur;
}
// Daily return aggregation
$dayKey = date('Y-m-d', strtotime($t['open_time']));
if (!isset($dailyReturns[$dayKey])) {
$dailyReturns[$dayKey] = ['profit' => 0.0, 'pips' => 0.0, 'trades' => 0, 'wins' => 0, 'losses' => 0];
}
$dailyReturns[$dayKey]['profit'] += $netTradeProfit;
$dailyReturns[$dayKey]['pips'] += $pips;
$dailyReturns[$dayKey]['trades']++;
if ($profit > 0) $dailyReturns[$dayKey]['wins']++;
elseif ($profit < 0) $dailyReturns[$dayKey]['losses']++;
// By Symbol Breakdown
if (!isset($symbolStats[$sym])) {
$symbolStats[$sym] = ['symbol' => $sym, 'count' => 0, 'wins' => 0, 'losses' => 0, 'profit' => 0.0, 'pips' => 0.0, 'longs' => 0, 'shorts' => 0];
}
$symbolStats[$sym]['count']++;
$symbolStats[$sym]['profit'] += $netTradeProfit;
$symbolStats[$sym]['pips'] += $pips;
if ($profit > 0) $symbolStats[$sym]['wins']++;
elseif ($profit < 0) $symbolStats[$sym]['losses']++;
if (strtolower($t['trade_type']) === 'buy') $symbolStats[$sym]['longs']++;
else $symbolStats[$sym]['shorts']++;
// By Strategy Breakdown
if (!isset($strategyStats[$strat])) {
$strategyStats[$strat] = ['strategy' => $strat, 'count' => 0, 'wins' => 0, 'losses' => 0, 'profit' => 0.0, 'gross_profit' => 0.0, 'gross_loss' => 0.0];
}
$strategyStats[$strat]['count']++;
$strategyStats[$strat]['profit'] += $netTradeProfit;
if ($profit > 0) {
$strategyStats[$strat]['wins']++;
$strategyStats[$strat]['gross_profit'] += $profit;
} elseif ($profit < 0) {
$strategyStats[$strat]['losses']++;
$strategyStats[$strat]['gross_loss'] += abs($profit);
}
// By Emotion Breakdown
if (!isset($emotionStats[$emo])) {
$emotionStats[$emo] = ['emotion' => $emo, 'count' => 0, 'wins' => 0, 'profit' => 0.0, 'pips' => 0.0];
}
$emotionStats[$emo]['count']++;
$emotionStats[$emo]['profit'] += $netTradeProfit;
$emotionStats[$emo]['pips'] += $pips;
if ($profit > 0) $emotionStats[$emo]['wins']++;
// By Session Breakdown
if (isset($sessionStats[$sess])) {
$sessionStats[$sess]['count']++;
$sessionStats[$sess]['profit'] += $netTradeProfit;
if ($profit > 0) $sessionStats[$sess]['wins']++;
}
// By Day of Week & Hour
$openTs = strtotime($t['open_time']);
$dow = (int)date('N', $openTs); // 1 = Monday .. 7 = Sunday
if (isset($dayOfWeekStats[$dow])) {
$dayOfWeekStats[$dow]['count']++;
$dayOfWeekStats[$dow]['profit'] += $netTradeProfit;
if ($profit > 0) $dayOfWeekStats[$dow]['wins']++;
}
$hr = (int)date('G', $openTs); // 0 .. 23
if (isset($hourlyStats[$hr])) {
$hourlyStats[$hr]['count']++;
$hourlyStats[$hr]['profit'] += $netTradeProfit;
if ($profit > 0) $hourlyStats[$hr]['wins']++;
}
}
// Open floating calculation
$openFloatingProfit = 0.0;
foreach ($openTrades as $ot) {
$openFloatingProfit += (float)$ot['profit'] + (float)$ot['commission'] + (float)$ot['swap'];
}
$currentEquity = $currentBalance + $openFloatingProfit;
// Total Net Closed Profit
$totalClosedProfit = $currentBalance - $initialBalance;
$totalGainPercent = $initialBalance > 0 ? (($currentEquity - $initialBalance) / $initialBalance) * 100 : 0;
$closedGainPercent = $initialBalance > 0 ? ($totalClosedProfit / $initialBalance) * 100 : 0;
$closedCount = count($closedTrades);
$winRate = $closedCount > 0 ? ($wonTradesCount / $closedCount) * 100 : 0;
$lossRate = $closedCount > 0 ? ($lostTradesCount / $closedCount) * 100 : 0;
$profitFactor = $grossLoss > 0 ? round($grossProfit / $grossLoss, 2) : ($grossProfit > 0 ? 99.9 : 0.0);
$avgWin = $wonTradesCount > 0 ? $grossProfit / $wonTradesCount : 0.0;
$avgLoss = $lostTradesCount > 0 ? $grossLoss / $lostTradesCount : 0.0;
$winLossRatio = $avgLoss > 0 ? round($avgWin / $avgLoss, 2) : 0.0;
$expectancy = ($winRate / 100 * $avgWin) - ($lossRate / 100 * $avgLoss);
$avgPipsWin = $wonTradesCount > 0 ? $winPipsSum / $wonTradesCount : 0.0;
$avgPipsLoss = $lostTradesCount > 0 ? $lossPipsSum / $lostTradesCount : 0.0;
$longWinRate = $longTradesCount > 0 ? ($longWonCount / $longTradesCount) * 100 : 0;
$shortWinRate = $shortTradesCount > 0 ? ($shortWonCount / $shortTradesCount) * 100 : 0;
$avgTradeDuration = $closedCount > 0 ? round($totalDurationSec / $closedCount) : 0;
$avgWinDuration = $wonTradesCount > 0 ? round($winDurationSec / $wonTradesCount) : 0;
$avgLossDuration = $lostTradesCount > 0 ? round($lossDurationSec / $lostTradesCount) : 0;
// Sharpe & Sortino Ratio estimation
$dailyPnLValues = array_column($dailyReturns, 'profit');
$sharpeRatio = 0.0;
$sortinoRatio = 0.0;
if (count($dailyPnLValues) > 2) {
$meanDaily = array_sum($dailyPnLValues) / count($dailyPnLValues);
$variance = 0.0;
$downsideVariance = 0.0;
foreach ($dailyPnLValues as $dp) {
$diff = $dp - $meanDaily;
$variance += ($diff * $diff);
if ($dp < 0) {
$downsideVariance += ($dp * $dp);
}
}
$stdDev = sqrt($variance / count($dailyPnLValues));
$downsideStdDev = sqrt($downsideVariance / count($dailyPnLValues));
if ($stdDev > 0) {
$sharpeRatio = round(($meanDaily / $stdDev) * sqrt(252), 2); // Annualized (252 trading days)
}
if ($downsideStdDev > 0) {
$sortinoRatio = round(($meanDaily / $downsideStdDev) * sqrt(252), 2);
}
}
// Today, This Week, This Month profit
$todayStr = date('Y-m-d');
$thisWeekStart = date('Y-m-d', strtotime('monday this week'));
$thisMonthStart = date('Y-m-01');
$todayProfit = $dailyReturns[$todayStr]['profit'] ?? 0.0;
$thisWeekProfit = 0.0;
$thisMonthProfit = 0.0;
foreach ($dailyReturns as $dDate => $dData) {
if ($dDate >= $thisWeekStart) {
$thisWeekProfit += $dData['profit'];
}
if ($dDate >= $thisMonthStart) {
$thisMonthProfit += $dData['profit'];
}
}
// Monthly Returns Table (Year x Months Heatmap)
$monthlyReturns = [];
foreach ($dailyReturns as $dDate => $dData) {
$yr = (int)date('Y', strtotime($dDate));
$mo = (int)date('n', strtotime($dDate));
if (!isset($monthlyReturns[$yr])) {
$monthlyReturns[$yr] = [
'year' => $yr,
'months' => array_fill(1, 12, ['profit' => 0.0, 'gain_percent' => 0.0, 'trades' => 0]),
'total_profit' => 0.0,
'total_gain_percent' => 0.0,
'total_trades' => 0
];
}
$monthlyReturns[$yr]['months'][$mo]['profit'] += $dData['profit'];
$monthlyReturns[$yr]['months'][$mo]['trades'] += $dData['trades'];
$monthlyReturns[$yr]['total_profit'] += $dData['profit'];
$monthlyReturns[$yr]['total_trades'] += $dData['trades'];
}
foreach ($monthlyReturns as $yr => &$yData) {
foreach ($yData['months'] as $mo => &$mVal) {
if ($initialBalance > 0) {
$mVal['gain_percent'] = round(($mVal['profit'] / $initialBalance) * 100, 2);
}
}
if ($initialBalance > 0) {
$yData['total_gain_percent'] = round(($yData['total_profit'] / $initialBalance) * 100, 2);
}
}
unset($yData, $mVal);
// Format Breakdown Lists
$symbolsList = array_values($symbolStats);
usort($symbolsList, fn($a, $b) => $b['profit'] <=> $a['profit']);
foreach ($symbolsList as &$sItem) {
$sItem['win_rate'] = $sItem['count'] > 0 ? round(($sItem['wins'] / $sItem['count']) * 100, 1) : 0;
$sItem['avg_profit'] = $sItem['count'] > 0 ? round($sItem['profit'] / $sItem['count'], 2) : 0;
}
$strategiesList = array_values($strategyStats);
usort($strategiesList, fn($a, $b) => $b['profit'] <=> $a['profit']);
foreach ($strategiesList as &$stItem) {
$stItem['win_rate'] = $stItem['count'] > 0 ? round(($stItem['wins'] / $stItem['count']) * 100, 1) : 0;
$stItem['profit_factor'] = $stItem['gross_loss'] > 0 ? round($stItem['gross_profit'] / $stItem['gross_loss'], 2) : ($stItem['gross_profit'] > 0 ? 99.9 : 0.0);
}
$emotionsList = array_values($emotionStats);
usort($emotionsList, fn($a, $b) => $b['profit'] <=> $a['profit']);
foreach ($emotionsList as &$emItem) {
$emItem['win_rate'] = $emItem['count'] > 0 ? round(($emItem['wins'] / $emItem['count']) * 100, 1) : 0;
}
$sessionsList = [];
foreach ($sessionStats as $sessKey => $sessData) {
$sessionsList[] = [
'session' => $sessKey,
'count' => $sessData['count'],
'profit' => round($sessData['profit'], 2),
'win_rate' => $sessData['count'] > 0 ? round(($sessData['wins'] / $sessData['count']) * 100, 1) : 0
];
}
$daysOfWeekList = [];
foreach ($dayOfWeekStats as $dowKey => $dowData) {
$daysOfWeekList[] = [
'day_num' => $dowKey,
'name' => $dowData['name'],
'name_fa' => $dowData['fa'],
'count' => $dowData['count'],
'profit' => round($dowData['profit'], 2),
'win_rate' => $dowData['count'] > 0 ? round(($dowData['wins'] / $dowData['count']) * 100, 1) : 0
];
}
$hourlyList = array_values($hourlyStats);
foreach ($hourlyList as &$hrItem) {
$hrItem['profit'] = round($hrItem['profit'], 2);
$hrItem['win_rate'] = $hrItem['count'] > 0 ? round(($hrItem['wins'] / $hrItem['count']) * 100, 1) : 0;
}
return [
'account' => $account,
'summary' => [
'initial_balance' => round($initialBalance, 2),
'current_balance' => round($currentBalance, 2),
'current_equity' => round($currentEquity, 2),
'open_floating_profit' => round($openFloatingProfit, 2),
'total_closed_profit' => round($totalClosedProfit, 2),
'total_gain_percent' => round($totalGainPercent, 2),
'closed_gain_percent' => round($closedGainPercent, 2),
'today_profit' => round($todayProfit, 2),
'this_week_profit' => round($thisWeekProfit, 2),
'this_month_profit' => round($thisMonthProfit, 2),
'total_trades' => $totalTrades,
'closed_trades' => $closedCount,
'open_trades' => count($openTrades),
'won_trades' => $wonTradesCount,
'lost_trades' => $lostTradesCount,
'breakeven_trades' => $beTradesCount,
'win_rate' => round($winRate, 1),
'loss_rate' => round($lossRate, 1),
'profit_factor' => $profitFactor,
'gross_profit' => round($grossProfit, 2),
'gross_loss' => round($grossLoss, 2),
'avg_win' => round($avgWin, 2),
'avg_loss' => round($avgLoss, 2),
'win_loss_ratio' => $winLossRatio,
'expectancy' => round($expectancy, 2),
'total_pips' => round($totalPips, 1),
'avg_pips_win' => round($avgPipsWin, 1),
'avg_pips_loss' => round($avgPipsLoss, 1),
'best_trade_profit' => round($bestTradeProfit, 2),
'worst_trade_profit' => round($worstTradeProfit, 2),
'best_trade_pips' => round($bestTradePips, 1),
'worst_trade_pips' => round($worstTradePips, 1),
'max_consecutive_wins' => $maxConsecutiveWins,
'max_consecutive_losses' => $maxConsecutiveLosses,
'max_drawdown_amount' => round($maxDrawdownAmount, 2),
'max_drawdown_percent' => round($maxDrawdownPercent, 2),
'sharpe_ratio' => $sharpeRatio,
'sortino_ratio' => $sortinoRatio,
'total_commission' => round($totalCommission, 2),
'total_swap' => round($totalSwap, 2),
'long_trades' => $longTradesCount,
'long_won' => $longWonCount,
'long_win_rate' => round($longWinRate, 1),
'long_profit' => round($longProfit, 2),
'short_trades' => $shortTradesCount,
'short_won' => $shortWonCount,
'short_win_rate' => round($shortWinRate, 1),
'short_profit' => round($shortProfit, 2),
'avg_trade_duration_seconds' => $avgTradeDuration,
'avg_win_duration_seconds' => $avgWinDuration,
'avg_loss_duration_seconds' => $avgLossDuration,
],
'growth_curve' => $growthCurve,
'drawdown_curve' => $drawdownCurve,
'monthly_returns' => array_values($monthlyReturns),
'daily_pnl' => $dailyReturns,
'by_symbol' => $symbolsList,
'by_strategy' => $strategiesList,
'by_emotion' => $emotionsList,
'by_session' => $sessionsList,
'by_day_of_week' => $daysOfWeekList,
'by_hour' => $hourlyList,
'open_trades' => $openTrades
];
}
}