|
@@ -21,6 +21,24 @@ class PerformanceCalculator:
|
|
|
def __init__(self, db_manager):
|
|
|
"""Initialize with database manager."""
|
|
|
self.db = db_manager
|
|
|
+
|
|
|
+ def _safe_float(self, value, default=0.0):
|
|
|
+ """Safely convert a value to float, returning default if None or invalid."""
|
|
|
+ if value is None:
|
|
|
+ return default
|
|
|
+ try:
|
|
|
+ return float(value)
|
|
|
+ except (ValueError, TypeError):
|
|
|
+ return default
|
|
|
+
|
|
|
+ def _safe_divide(self, numerator, denominator, default=0.0):
|
|
|
+ """Safely divide two numbers, returning default if denominator is 0 or None."""
|
|
|
+ num = self._safe_float(numerator, 0.0)
|
|
|
+ den = self._safe_float(denominator, 0.0)
|
|
|
+
|
|
|
+ if den == 0:
|
|
|
+ return default
|
|
|
+ return num / den
|
|
|
|
|
|
def _format_duration(self, total_seconds: int) -> str:
|
|
|
"""Format duration from seconds to human-readable format."""
|
|
@@ -82,35 +100,38 @@ class PerformanceCalculator:
|
|
|
|
|
|
# Process token stats
|
|
|
for token in token_stats:
|
|
|
- if token.get('total_completed_cycles', 0) > 0:
|
|
|
- total_trades += token.get('total_completed_cycles', 0)
|
|
|
- total_wins += token.get('winning_cycles', 0)
|
|
|
- total_losses += token.get('losing_cycles', 0)
|
|
|
- total_pnl += token.get('total_realized_pnl', 0)
|
|
|
- total_entry_volume += token.get('total_entry_volume', 0)
|
|
|
- total_exit_volume += token.get('total_exit_volume', 0)
|
|
|
+ completed_cycles = self._safe_float(token.get('total_completed_cycles'), 0)
|
|
|
+ if completed_cycles > 0:
|
|
|
+ total_trades += completed_cycles
|
|
|
+ total_wins += self._safe_float(token.get('winning_cycles'), 0)
|
|
|
+ total_losses += self._safe_float(token.get('losing_cycles'), 0)
|
|
|
+ total_pnl += self._safe_float(token.get('total_realized_pnl'), 0)
|
|
|
+ total_entry_volume += self._safe_float(token.get('total_entry_volume'), 0)
|
|
|
+ total_exit_volume += self._safe_float(token.get('total_exit_volume'), 0)
|
|
|
|
|
|
# Track largest trades
|
|
|
- token_largest_win = token.get('largest_winning_cycle_pnl', 0)
|
|
|
- token_largest_loss = token.get('largest_losing_cycle_pnl', 0)
|
|
|
+ token_largest_win = self._safe_float(token.get('largest_winning_cycle_pnl'), 0)
|
|
|
+ token_largest_loss = self._safe_float(token.get('largest_losing_cycle_pnl'), 0)
|
|
|
|
|
|
if token_largest_win > largest_win:
|
|
|
largest_win = token_largest_win
|
|
|
largest_win_token = token['token']
|
|
|
- largest_win_pct = (token_largest_win / token.get('largest_winning_cycle_entry_volume', 1)) * 100
|
|
|
+ entry_volume = self._safe_float(token.get('largest_winning_cycle_entry_volume'), 1.0)
|
|
|
+ largest_win_pct = self._safe_divide(token_largest_win, entry_volume) * 100
|
|
|
|
|
|
# For losses, we want the most negative number
|
|
|
if token_largest_loss < 0 and (largest_loss == 0 or token_largest_loss < largest_loss):
|
|
|
largest_loss = token_largest_loss
|
|
|
largest_loss_token = token['token']
|
|
|
- largest_loss_pct = (token_largest_loss / token.get('largest_losing_cycle_entry_volume', 1)) * 100
|
|
|
+ entry_volume = self._safe_float(token.get('largest_losing_cycle_entry_volume'), 1.0)
|
|
|
+ largest_loss_pct = self._safe_divide(token_largest_loss, entry_volume) * 100
|
|
|
|
|
|
# Track best/worst tokens
|
|
|
- token_pnl = token.get('total_realized_pnl', 0)
|
|
|
- token_volume = token.get('total_entry_volume', 0)
|
|
|
+ token_pnl = self._safe_float(token.get('total_realized_pnl'), 0)
|
|
|
+ token_volume = self._safe_float(token.get('total_entry_volume'), 0)
|
|
|
|
|
|
if token_volume > 0:
|
|
|
- token_pnl_pct = (token_pnl / token_volume) * 100
|
|
|
+ token_pnl_pct = self._safe_divide(token_pnl, token_volume) * 100
|
|
|
|
|
|
if best_token_name == "N/A" or token_pnl > best_token_pnl_value:
|
|
|
best_token_name = token['token']
|
|
@@ -125,20 +146,27 @@ class PerformanceCalculator:
|
|
|
worst_token_volume = token_volume
|
|
|
|
|
|
# Calculate win rate and profit factor
|
|
|
- win_rate = (total_wins / total_trades * 100) if total_trades > 0 else 0
|
|
|
+ win_rate = self._safe_divide(total_wins, total_trades) * 100
|
|
|
+
|
|
|
+ # Calculate sum of winning and losing trades
|
|
|
+ sum_winning = sum(self._safe_float(token.get('sum_of_winning_pnl'), 0) for token in token_stats)
|
|
|
+ sum_losing = abs(sum(self._safe_float(token.get('sum_of_losing_pnl'), 0) for token in token_stats))
|
|
|
|
|
|
- # Calculate sum of winning and losing trades
|
|
|
- sum_winning = sum(token.get('sum_of_winning_pnl', 0) for token in token_stats)
|
|
|
- sum_losing = abs(sum(token.get('sum_of_losing_pnl', 0) for token in token_stats))
|
|
|
- profit_factor = (sum_winning / sum_losing) if sum_losing > 0 else float('inf') if sum_winning > 0 else 0
|
|
|
+ if sum_losing > 0:
|
|
|
+ profit_factor = self._safe_divide(sum_winning, sum_losing)
|
|
|
+ elif sum_winning > 0:
|
|
|
+ profit_factor = float('inf')
|
|
|
+ else:
|
|
|
+ profit_factor = 0
|
|
|
|
|
|
# Calculate average P&L stats
|
|
|
- avg_win_pnl = sum_winning / total_wins if total_wins > 0 else 0
|
|
|
- avg_loss_pnl = sum_losing / total_losses if total_losses > 0 else 0
|
|
|
- avg_trade_pnl = total_pnl / total_trades if total_trades > 0 else 0.0
|
|
|
+ avg_win_pnl = self._safe_divide(sum_winning, total_wins)
|
|
|
+ avg_loss_pnl = self._safe_divide(sum_losing, total_losses)
|
|
|
+ avg_trade_pnl = self._safe_divide(total_pnl, total_trades)
|
|
|
|
|
|
# Calculate expectancy
|
|
|
- expectancy = (avg_win_pnl * (win_rate/100)) - (avg_loss_pnl * (1 - win_rate/100))
|
|
|
+ win_rate_decimal = win_rate / 100
|
|
|
+ expectancy = (avg_win_pnl * win_rate_decimal) - (avg_loss_pnl * (1 - win_rate_decimal))
|
|
|
|
|
|
# Get max drawdown
|
|
|
max_drawdown, max_drawdown_pct, drawdown_start_date = self.get_live_max_drawdown()
|
|
@@ -198,25 +226,31 @@ class PerformanceCalculator:
|
|
|
)
|
|
|
|
|
|
for token in token_stats:
|
|
|
- total_cycles = token.get('total_completed_cycles', 0)
|
|
|
- winning_cycles = token.get('winning_cycles', 0)
|
|
|
+ total_cycles = self._safe_float(token.get('total_completed_cycles'), 0)
|
|
|
+ winning_cycles = self._safe_float(token.get('winning_cycles'), 0)
|
|
|
|
|
|
# Calculate win rate
|
|
|
- token['win_rate'] = (winning_cycles / total_cycles * 100) if total_cycles > 0 else 0
|
|
|
+ token['win_rate'] = self._safe_divide(winning_cycles, total_cycles) * 100
|
|
|
|
|
|
# Calculate profit factor
|
|
|
- sum_winning = token.get('sum_of_winning_pnl', 0)
|
|
|
- sum_losing = token.get('sum_of_losing_pnl', 0)
|
|
|
- token['profit_factor'] = sum_winning / sum_losing if sum_losing > 0 else float('inf') if sum_winning > 0 else 0
|
|
|
+ sum_winning = self._safe_float(token.get('sum_of_winning_pnl'), 0)
|
|
|
+ sum_losing = self._safe_float(token.get('sum_of_losing_pnl'), 0)
|
|
|
+
|
|
|
+ if sum_losing > 0:
|
|
|
+ token['profit_factor'] = self._safe_divide(sum_winning, sum_losing)
|
|
|
+ elif sum_winning > 0:
|
|
|
+ token['profit_factor'] = float('inf')
|
|
|
+ else:
|
|
|
+ token['profit_factor'] = 0
|
|
|
|
|
|
# Calculate ROE from realized PnL and entry volume
|
|
|
- total_pnl = token.get('total_realized_pnl', 0)
|
|
|
- entry_volume = token.get('total_entry_volume', 0)
|
|
|
- token['roe_percentage'] = (total_pnl / entry_volume * 100) if entry_volume > 0 else 0.0
|
|
|
+ total_pnl = self._safe_float(token.get('total_realized_pnl'), 0)
|
|
|
+ entry_volume = self._safe_float(token.get('total_entry_volume'), 0)
|
|
|
+ token['roe_percentage'] = self._safe_divide(total_pnl, entry_volume) * 100
|
|
|
|
|
|
# Format durations
|
|
|
- total_duration = token.get('total_duration_seconds', 0)
|
|
|
- avg_duration = total_duration / total_cycles if total_cycles > 0 else 0
|
|
|
+ total_duration = self._safe_float(token.get('total_duration_seconds'), 0)
|
|
|
+ avg_duration = self._safe_divide(total_duration, total_cycles)
|
|
|
token['average_trade_duration_formatted'] = self._format_duration(avg_duration)
|
|
|
|
|
|
# Token display name (use token as-is)
|
|
@@ -257,7 +291,7 @@ class PerformanceCalculator:
|
|
|
running_max = balance
|
|
|
|
|
|
drawdown = running_max - balance
|
|
|
- drawdown_percentage = (drawdown / running_max * 100) if running_max > 0 else 0
|
|
|
+ drawdown_percentage = self._safe_divide(drawdown, running_max) * 100
|
|
|
|
|
|
if drawdown > max_drawdown:
|
|
|
max_drawdown = drawdown
|
|
@@ -266,7 +300,7 @@ class PerformanceCalculator:
|
|
|
# Calculate period return
|
|
|
initial_balance_period = balances[0] if balances else 0
|
|
|
period_pnl = current_balance - initial_balance_period
|
|
|
- period_return_percentage = (period_pnl / initial_balance_period * 100) if initial_balance_period > 0 else 0
|
|
|
+ period_return_percentage = self._safe_divide(period_pnl, initial_balance_period) * 100
|
|
|
|
|
|
stats = {
|
|
|
'peak_balance': peak_balance,
|
|
@@ -317,7 +351,7 @@ class PerformanceCalculator:
|
|
|
else:
|
|
|
# Still in a drawdown, check if it's a new max
|
|
|
drawdown = peak_balance - current_balance
|
|
|
- drawdown_percentage = (drawdown / peak_balance * 100) if peak_balance > 0 else 0
|
|
|
+ drawdown_percentage = self._safe_divide(drawdown, peak_balance) * 100
|
|
|
|
|
|
if drawdown_percentage > max_drawdown_percentage:
|
|
|
self.db._set_metadata('drawdown_max_drawdown_pct', str(drawdown_percentage))
|
|
@@ -342,10 +376,10 @@ class PerformanceCalculator:
|
|
|
# Calculate daily returns
|
|
|
returns = []
|
|
|
for i in range(1, len(balance_history)):
|
|
|
- prev_balance = balance_history[i-1]['balance']
|
|
|
- curr_balance = balance_history[i]['balance']
|
|
|
+ prev_balance = self._safe_float(balance_history[i-1]['balance'])
|
|
|
+ curr_balance = self._safe_float(balance_history[i]['balance'])
|
|
|
if prev_balance > 0:
|
|
|
- daily_return = (curr_balance - prev_balance) / prev_balance
|
|
|
+ daily_return = self._safe_divide(curr_balance - prev_balance, prev_balance)
|
|
|
returns.append(daily_return)
|
|
|
|
|
|
if not returns or np.std(returns) == 0:
|
|
@@ -355,7 +389,7 @@ class PerformanceCalculator:
|
|
|
avg_daily_return = np.mean(returns)
|
|
|
std_dev_daily_return = np.std(returns)
|
|
|
|
|
|
- sharpe_ratio = (avg_daily_return - (risk_free_rate / 365)) / std_dev_daily_return
|
|
|
+ sharpe_ratio = self._safe_divide(avg_daily_return - (risk_free_rate / 365), std_dev_daily_return)
|
|
|
annualized_sharpe_ratio = sharpe_ratio * np.sqrt(365) # Annualize
|
|
|
|
|
|
return annualized_sharpe_ratio
|
|
@@ -417,12 +451,12 @@ class PerformanceCalculator:
|
|
|
'average_daily_pnl': 0
|
|
|
}
|
|
|
|
|
|
- total_pnl = sum(day.get('pnl', 0) or 0 for day in daily_stats)
|
|
|
- total_trades = sum(day.get('trades', 0) or 0 for day in daily_stats)
|
|
|
- total_volume = sum(day.get('volume', 0) or 0 for day in daily_stats)
|
|
|
- trading_days = len([day for day in daily_stats if (day.get('trades', 0) or 0) > 0])
|
|
|
+ total_pnl = sum(self._safe_float(day.get('pnl')) for day in daily_stats)
|
|
|
+ total_trades = sum(self._safe_float(day.get('trades')) for day in daily_stats)
|
|
|
+ total_volume = sum(self._safe_float(day.get('volume')) for day in daily_stats)
|
|
|
+ trading_days = len([day for day in daily_stats if self._safe_float(day.get('trades')) > 0])
|
|
|
|
|
|
- average_daily_pnl = total_pnl / trading_days if trading_days > 0 else 0
|
|
|
+ average_daily_pnl = self._safe_divide(total_pnl, trading_days)
|
|
|
|
|
|
return {
|
|
|
'period_start': start_date,
|