|
@@ -15,9 +15,8 @@ import json
|
|
|
import os
|
|
|
import logging
|
|
|
from datetime import datetime, timedelta
|
|
|
-from typing import Dict, List, Any, Optional, Tuple
|
|
|
+from typing import Dict, List, Any
|
|
|
import numpy as np
|
|
|
-from config import Config
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
@@ -345,17 +344,16 @@ class TradingStats:
|
|
|
'last_trade': None
|
|
|
}
|
|
|
|
|
|
- trades_with_pnl = self.calculate_trade_pnl()
|
|
|
- completed_trades = [t for t in trades_with_pnl if t.get('pnl', 0) != 0]
|
|
|
- total_pnl = sum(trade.get('pnl', 0) for trade in completed_trades)
|
|
|
+ completed_cycles = self.calculate_completed_trade_cycles()
|
|
|
+ total_pnl = sum(cycle['total_pnl'] for cycle in completed_cycles)
|
|
|
|
|
|
# Calculate days active
|
|
|
start_date = datetime.fromisoformat(self.data['start_time'])
|
|
|
days_active = (datetime.now() - start_date).days + 1
|
|
|
|
|
|
return {
|
|
|
- 'total_trades': len(self.data['trades']), # Total orders placed
|
|
|
- 'completed_trades': len(completed_trades), # Actual completed trade cycles with P&L
|
|
|
+ 'total_trades': len(self.data['trades']), # Total individual orders placed
|
|
|
+ 'completed_trades': len(completed_cycles), # Actual completed trade cycles
|
|
|
'buy_trades': len([t for t in self.data['trades'] if t['side'] == 'buy']),
|
|
|
'sell_trades': len([t for t in self.data['trades'] if t['side'] == 'sell']),
|
|
|
'initial_balance': self.data.get('initial_balance', 0),
|
|
@@ -367,10 +365,9 @@ class TradingStats:
|
|
|
|
|
|
def get_performance_stats(self) -> Dict[str, Any]:
|
|
|
"""Calculate advanced performance statistics."""
|
|
|
- trades_with_pnl = self.calculate_trade_pnl()
|
|
|
- completed_trades = [t for t in trades_with_pnl if t.get('pnl', 0) != 0]
|
|
|
+ completed_cycles = self.calculate_completed_trade_cycles()
|
|
|
|
|
|
- if not completed_trades:
|
|
|
+ if not completed_cycles:
|
|
|
return {
|
|
|
'win_rate': 0,
|
|
|
'profit_factor': 0,
|
|
@@ -385,9 +382,9 @@ class TradingStats:
|
|
|
'expectancy': 0
|
|
|
}
|
|
|
|
|
|
- # Separate wins and losses
|
|
|
- wins = [t['pnl'] for t in completed_trades if t['pnl'] > 0]
|
|
|
- losses = [abs(t['pnl']) for t in completed_trades if t['pnl'] < 0]
|
|
|
+ # Separate wins and losses by trade cycles
|
|
|
+ wins = [c['total_pnl'] for c in completed_cycles if c['total_pnl'] > 0]
|
|
|
+ losses = [abs(c['total_pnl']) for c in completed_cycles if c['total_pnl'] < 0]
|
|
|
|
|
|
# Basic metrics
|
|
|
total_wins = len(wins)
|
|
@@ -407,14 +404,14 @@ class TradingStats:
|
|
|
largest_win = max(wins) if wins else 0
|
|
|
largest_loss = max(losses) if losses else 0
|
|
|
|
|
|
- # Consecutive wins/losses
|
|
|
+ # Consecutive wins/losses (by trade cycles)
|
|
|
consecutive_wins = 0
|
|
|
consecutive_losses = 0
|
|
|
current_wins = 0
|
|
|
current_losses = 0
|
|
|
|
|
|
- for trade in completed_trades:
|
|
|
- if trade['pnl'] > 0:
|
|
|
+ for cycle in completed_cycles:
|
|
|
+ if cycle['total_pnl'] > 0:
|
|
|
current_wins += 1
|
|
|
current_losses = 0
|
|
|
consecutive_wins = max(consecutive_wins, current_wins)
|
|
@@ -616,77 +613,81 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
|
|
|
def get_token_performance(self) -> Dict[str, Dict[str, Any]]:
|
|
|
"""Get performance statistics grouped by token."""
|
|
|
- trades_with_pnl = self.calculate_trade_pnl()
|
|
|
+ completed_cycles = self.calculate_completed_trade_cycles()
|
|
|
|
|
|
- # Group trades by token
|
|
|
- token_trades = {}
|
|
|
+ # Group cycles by token
|
|
|
+ token_cycles = {}
|
|
|
|
|
|
- for trade in trades_with_pnl:
|
|
|
- symbol = trade['symbol']
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ for cycle in completed_cycles:
|
|
|
+ token = cycle['token']
|
|
|
|
|
|
- if token not in token_trades:
|
|
|
- token_trades[token] = []
|
|
|
- token_trades[token].append(trade)
|
|
|
+ if token not in token_cycles:
|
|
|
+ token_cycles[token] = []
|
|
|
+ token_cycles[token].append(cycle)
|
|
|
|
|
|
# Calculate performance for each token
|
|
|
token_performance = {}
|
|
|
|
|
|
- for token, trades in token_trades.items():
|
|
|
- completed_trades = [t for t in trades if t.get('pnl', 0) != 0]
|
|
|
+ for token, cycles in token_cycles.items():
|
|
|
+ # Only include tokens that have actual completed trade cycles
|
|
|
+ if not cycles:
|
|
|
+ continue # Skip tokens with no completed cycles
|
|
|
|
|
|
- # Only include tokens that have actual completed trades with P&L
|
|
|
- if not completed_trades:
|
|
|
- continue # Skip tokens with no completed trades
|
|
|
-
|
|
|
- # Calculate metrics for completed trades
|
|
|
- total_pnl = sum(t['pnl'] for t in completed_trades)
|
|
|
- total_volume = sum(t['value'] for t in completed_trades)
|
|
|
+ # Calculate metrics for completed cycles
|
|
|
+ total_pnl = sum(c['total_pnl'] for c in cycles)
|
|
|
+ total_volume = sum(c['sell_value'] for c in cycles) # Use sell value for volume
|
|
|
pnl_percentage = (total_pnl / total_volume * 100) if total_volume > 0 else 0.0
|
|
|
|
|
|
# Win/loss analysis
|
|
|
- wins = [t['pnl'] for t in completed_trades if t['pnl'] > 0]
|
|
|
- losses = [abs(t['pnl']) for t in completed_trades if t['pnl'] < 0]
|
|
|
+ wins = [c['total_pnl'] for c in cycles if c['total_pnl'] > 0]
|
|
|
+ losses = [abs(c['total_pnl']) for c in cycles if c['total_pnl'] < 0]
|
|
|
|
|
|
total_wins = len(wins)
|
|
|
total_losses = len(losses)
|
|
|
total_completed = total_wins + total_losses
|
|
|
|
|
|
- win_rate = (total_wins / total_completed * 100) if total_completed > 0 else 0
|
|
|
- avg_win = sum(wins) / total_wins if wins else 0
|
|
|
- avg_loss = sum(losses) / total_losses if losses else 0
|
|
|
- largest_win = max(wins) if wins else 0
|
|
|
- largest_loss = max(losses) if losses else 0
|
|
|
+ win_rate = (total_wins / total_completed * 100) if total_completed > 0 else 0.0
|
|
|
|
|
|
+ # Calculate profit factor
|
|
|
total_profit = sum(wins) if wins else 0
|
|
|
- total_loss_amount = sum(losses) if losses else 0
|
|
|
- profit_factor = (total_profit / total_loss_amount) if total_loss_amount > 0 else float('inf') if total_profit > 0 else 0
|
|
|
+ total_loss = sum(losses) if losses else 0
|
|
|
+ profit_factor = (total_profit / total_loss) if total_loss > 0 else float('inf') if total_profit > 0 else 0
|
|
|
+
|
|
|
+ # Calculate expectancy
|
|
|
+ avg_win = total_profit / total_wins if total_wins > 0 else 0
|
|
|
+ avg_loss = total_loss / total_losses if total_losses > 0 else 0
|
|
|
+ expectancy = avg_win * (win_rate / 100) - avg_loss * ((100 - win_rate) / 100)
|
|
|
+
|
|
|
+ # Largest win/loss
|
|
|
+ largest_win = max(wins) if wins else 0
|
|
|
+ largest_loss = max(losses) if losses else 0
|
|
|
|
|
|
token_performance[token] = {
|
|
|
'total_pnl': total_pnl,
|
|
|
'pnl_percentage': pnl_percentage,
|
|
|
- 'total_trades': len(trades),
|
|
|
- 'completed_trades': total_completed,
|
|
|
- 'buy_trades': len([t for t in trades if t['side'] == 'buy']),
|
|
|
- 'sell_trades': len([t for t in trades if t['side'] == 'sell']),
|
|
|
+ 'completed_trades': len(cycles), # Now correctly counts trade cycles
|
|
|
+ 'total_volume': total_volume,
|
|
|
'win_rate': win_rate,
|
|
|
- 'avg_win': avg_win,
|
|
|
- 'avg_loss': avg_loss,
|
|
|
+ 'total_wins': total_wins,
|
|
|
+ 'total_losses': total_losses,
|
|
|
+ 'profit_factor': profit_factor,
|
|
|
+ 'expectancy': expectancy,
|
|
|
'largest_win': largest_win,
|
|
|
'largest_loss': largest_loss,
|
|
|
- 'total_volume': total_volume,
|
|
|
- 'profit_factor': profit_factor,
|
|
|
- 'total_wins': total_wins,
|
|
|
- 'total_losses': total_losses
|
|
|
+ 'avg_win': avg_win,
|
|
|
+ 'avg_loss': avg_loss,
|
|
|
+ 'cycles': cycles # Include cycle details for further analysis
|
|
|
}
|
|
|
|
|
|
return token_performance
|
|
|
|
|
|
def get_token_detailed_stats(self, token: str) -> Dict[str, Any]:
|
|
|
"""Get detailed statistics for a specific token."""
|
|
|
+ completed_cycles = self.calculate_completed_trade_cycles()
|
|
|
trades_with_pnl = self.calculate_trade_pnl()
|
|
|
|
|
|
- # Filter trades for this token
|
|
|
+ # Filter cycles and trades for this token
|
|
|
+ token_cycles = [c for c in completed_cycles if c['token'].upper() == token.upper()]
|
|
|
token_trades = []
|
|
|
for trade in trades_with_pnl:
|
|
|
symbol = trade['symbol']
|
|
@@ -703,40 +704,38 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
'message': f"No trading history found for {token.upper()}"
|
|
|
}
|
|
|
|
|
|
- completed_trades = [t for t in token_trades if t.get('pnl', 0) != 0]
|
|
|
-
|
|
|
- # Basic trade info
|
|
|
+ # Basic trade info (individual orders)
|
|
|
total_trades = len(token_trades)
|
|
|
buy_trades = len([t for t in token_trades if t['side'] == 'buy'])
|
|
|
sell_trades = len([t for t in token_trades if t['side'] == 'sell'])
|
|
|
total_volume = sum(t['value'] for t in token_trades)
|
|
|
|
|
|
- if not completed_trades:
|
|
|
+ if not token_cycles:
|
|
|
return {
|
|
|
'token': token.upper(),
|
|
|
'total_trades': total_trades,
|
|
|
'buy_trades': buy_trades,
|
|
|
'sell_trades': sell_trades,
|
|
|
'total_volume': total_volume,
|
|
|
- 'completed_trades': 0,
|
|
|
+ 'completed_trades': 0, # No completed cycles
|
|
|
'total_pnl': 0.0,
|
|
|
'pnl_percentage': 0.0,
|
|
|
'win_rate': 0.0,
|
|
|
- 'message': f"{token.upper()} has open positions but no completed trades yet"
|
|
|
+ 'message': f"{token.upper()} has open positions but no completed trade cycles yet"
|
|
|
}
|
|
|
|
|
|
- # Performance calculations
|
|
|
- total_pnl = sum(t['pnl'] for t in completed_trades)
|
|
|
- completed_volume = sum(t['value'] for t in completed_trades)
|
|
|
+ # Performance calculations based on completed cycles
|
|
|
+ total_pnl = sum(c['total_pnl'] for c in token_cycles)
|
|
|
+ completed_volume = sum(c['sell_value'] for c in token_cycles)
|
|
|
pnl_percentage = (total_pnl / completed_volume * 100) if completed_volume > 0 else 0.0
|
|
|
|
|
|
- # Win/loss analysis
|
|
|
- wins = [t['pnl'] for t in completed_trades if t['pnl'] > 0]
|
|
|
- losses = [abs(t['pnl']) for t in completed_trades if t['pnl'] < 0]
|
|
|
+ # Win/loss analysis (by trade cycles)
|
|
|
+ wins = [c['total_pnl'] for c in token_cycles if c['total_pnl'] > 0]
|
|
|
+ losses = [abs(c['total_pnl']) for c in token_cycles if c['total_pnl'] < 0]
|
|
|
|
|
|
total_wins = len(wins)
|
|
|
total_losses = len(losses)
|
|
|
- completed_count = total_wins + total_losses
|
|
|
+ completed_count = len(token_cycles) # Number of completed cycles
|
|
|
|
|
|
win_rate = (total_wins / completed_count * 100) if completed_count > 0 else 0
|
|
|
avg_win = sum(wins) / total_wins if wins else 0
|
|
@@ -749,13 +748,13 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
profit_factor = (total_profit / total_loss_amount) if total_loss_amount > 0 else float('inf') if total_profit > 0 else 0
|
|
|
expectancy = avg_win * (win_rate/100) - avg_loss * ((100-win_rate)/100)
|
|
|
|
|
|
- # Recent trades
|
|
|
- recent_trades = token_trades[-5:] # Last 5 trades
|
|
|
+ # Recent trades (still show individual orders for detail)
|
|
|
+ recent_trades = token_trades[-5:] # Last 5 individual orders
|
|
|
|
|
|
return {
|
|
|
'token': token.upper(),
|
|
|
- 'total_trades': total_trades,
|
|
|
- 'completed_trades': completed_count,
|
|
|
+ 'total_trades': total_trades, # Total individual orders
|
|
|
+ 'completed_trades': completed_count, # Number of completed cycles
|
|
|
'buy_trades': buy_trades,
|
|
|
'sell_trades': sell_trades,
|
|
|
'total_volume': total_volume,
|
|
@@ -771,25 +770,25 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
'expectancy': expectancy,
|
|
|
'total_wins': total_wins,
|
|
|
'total_losses': total_losses,
|
|
|
- 'recent_trades': recent_trades
|
|
|
+ 'recent_trades': recent_trades,
|
|
|
+ 'cycles': token_cycles # Include cycle details
|
|
|
}
|
|
|
|
|
|
def _update_period_stats(self):
|
|
|
"""Update daily, weekly, and monthly stats."""
|
|
|
- # Recalculate all period stats from completed trades
|
|
|
- trades_with_pnl = self.calculate_trade_pnl()
|
|
|
- completed_trades = [t for t in trades_with_pnl if t.get('pnl', 0) != 0]
|
|
|
+ # Recalculate all period stats from completed trade cycles
|
|
|
+ completed_cycles = self.calculate_completed_trade_cycles()
|
|
|
|
|
|
# Reset period stats
|
|
|
self.data['daily_stats'] = {}
|
|
|
self.data['weekly_stats'] = {}
|
|
|
self.data['monthly_stats'] = {}
|
|
|
|
|
|
- # Group completed trades by periods and calculate stats
|
|
|
- for trade in completed_trades:
|
|
|
- timestamp = trade['timestamp']
|
|
|
- pnl = trade['pnl']
|
|
|
- value = trade['value']
|
|
|
+ # Group completed cycles by periods and calculate stats
|
|
|
+ for cycle in completed_cycles:
|
|
|
+ timestamp = cycle['cycle_end'] # Use cycle end time
|
|
|
+ pnl = cycle['total_pnl']
|
|
|
+ value = cycle['sell_value'] # Use sell value for volume
|
|
|
|
|
|
try:
|
|
|
trade_date = datetime.fromisoformat(timestamp)
|
|
@@ -799,7 +798,7 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
if day_key not in self.data['daily_stats']:
|
|
|
self.data['daily_stats'][day_key] = {'trades': 0, 'pnl': 0.0, 'volume': 0.0}
|
|
|
|
|
|
- self.data['daily_stats'][day_key]['trades'] += 1
|
|
|
+ self.data['daily_stats'][day_key]['trades'] += 1 # Count as 1 completed cycle
|
|
|
self.data['daily_stats'][day_key]['pnl'] += pnl
|
|
|
self.data['daily_stats'][day_key]['volume'] += value
|
|
|
|
|
@@ -808,7 +807,7 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
if week_key not in self.data['weekly_stats']:
|
|
|
self.data['weekly_stats'][week_key] = {'trades': 0, 'pnl': 0.0, 'volume': 0.0}
|
|
|
|
|
|
- self.data['weekly_stats'][week_key]['trades'] += 1
|
|
|
+ self.data['weekly_stats'][week_key]['trades'] += 1 # Count as 1 completed cycle
|
|
|
self.data['weekly_stats'][week_key]['pnl'] += pnl
|
|
|
self.data['weekly_stats'][week_key]['volume'] += value
|
|
|
|
|
@@ -817,12 +816,12 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
if month_key not in self.data['monthly_stats']:
|
|
|
self.data['monthly_stats'][month_key] = {'trades': 0, 'pnl': 0.0, 'volume': 0.0}
|
|
|
|
|
|
- self.data['monthly_stats'][month_key]['trades'] += 1
|
|
|
+ self.data['monthly_stats'][month_key]['trades'] += 1 # Count as 1 completed cycle
|
|
|
self.data['monthly_stats'][month_key]['pnl'] += pnl
|
|
|
self.data['monthly_stats'][month_key]['volume'] += value
|
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
- # Skip trades with invalid timestamps
|
|
|
+ # Skip cycles with invalid timestamps
|
|
|
continue
|
|
|
|
|
|
# Calculate percentages for all periods
|
|
@@ -1056,4 +1055,107 @@ Please try again in a moment. If the issue persists, contact support.
|
|
|
'net_adjustment': net_adjustment,
|
|
|
'adjustment_count': len(adjustments),
|
|
|
'last_adjustment': latest_adjustment['timestamp'] if latest_adjustment else None
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ def calculate_completed_trade_cycles(self) -> List[Dict[str, Any]]:
|
|
|
+ """Calculate completed trade cycles (full position open to close) using FIFO method."""
|
|
|
+ completed_cycles = []
|
|
|
+ positions = {} # Track open positions by symbol
|
|
|
+
|
|
|
+ for trade in self.data['trades']:
|
|
|
+ symbol = trade['symbol']
|
|
|
+ side = trade['side']
|
|
|
+ amount = trade['amount']
|
|
|
+ price = trade['price']
|
|
|
+ timestamp = trade['timestamp']
|
|
|
+
|
|
|
+ if symbol not in positions:
|
|
|
+ positions[symbol] = {
|
|
|
+ 'amount': 0,
|
|
|
+ 'avg_price': 0,
|
|
|
+ 'total_cost': 0,
|
|
|
+ 'cycle_trades': [], # Track all trades in current cycle
|
|
|
+ 'cycle_start': None
|
|
|
+ }
|
|
|
+
|
|
|
+ pos = positions[symbol]
|
|
|
+
|
|
|
+ if side == 'buy':
|
|
|
+ # Add to position
|
|
|
+ was_zero = pos['amount'] == 0
|
|
|
+ pos['total_cost'] += amount * price
|
|
|
+ pos['amount'] += amount
|
|
|
+ pos['avg_price'] = pos['total_cost'] / pos['amount'] if pos['amount'] > 0 else 0
|
|
|
+
|
|
|
+ # If starting a new cycle, record start time
|
|
|
+ if was_zero:
|
|
|
+ pos['cycle_start'] = timestamp
|
|
|
+ pos['cycle_trades'] = []
|
|
|
+
|
|
|
+ # Add trade to current cycle
|
|
|
+ pos['cycle_trades'].append({
|
|
|
+ 'side': side,
|
|
|
+ 'amount': amount,
|
|
|
+ 'price': price,
|
|
|
+ 'timestamp': timestamp,
|
|
|
+ 'value': amount * price
|
|
|
+ })
|
|
|
+
|
|
|
+ elif side == 'sell':
|
|
|
+ # Reduce position and calculate PnL
|
|
|
+ if pos['amount'] > 0:
|
|
|
+ sold_amount = min(amount, pos['amount'])
|
|
|
+ pnl = sold_amount * (price - pos['avg_price'])
|
|
|
+
|
|
|
+ # Add trade to current cycle
|
|
|
+ pos['cycle_trades'].append({
|
|
|
+ 'side': side,
|
|
|
+ 'amount': sold_amount,
|
|
|
+ 'price': price,
|
|
|
+ 'timestamp': timestamp,
|
|
|
+ 'value': sold_amount * price,
|
|
|
+ 'pnl': pnl
|
|
|
+ })
|
|
|
+
|
|
|
+ # Update position
|
|
|
+ pos['amount'] -= sold_amount
|
|
|
+ if pos['amount'] > 0:
|
|
|
+ pos['total_cost'] = pos['amount'] * pos['avg_price']
|
|
|
+ else:
|
|
|
+ # Position fully closed - complete trade cycle
|
|
|
+ pos['total_cost'] = 0
|
|
|
+ pos['avg_price'] = 0
|
|
|
+
|
|
|
+ # Calculate cycle totals
|
|
|
+ cycle_buys = [t for t in pos['cycle_trades'] if t['side'] == 'buy']
|
|
|
+ cycle_sells = [t for t in pos['cycle_trades'] if t['side'] == 'sell']
|
|
|
+
|
|
|
+ total_buy_value = sum(t['value'] for t in cycle_buys)
|
|
|
+ total_sell_value = sum(t['value'] for t in cycle_sells)
|
|
|
+ total_cycle_pnl = sum(t.get('pnl', 0) for t in cycle_sells)
|
|
|
+
|
|
|
+ # Create completed cycle record
|
|
|
+ completed_cycle = {
|
|
|
+ 'symbol': symbol,
|
|
|
+ 'token': symbol.split('/')[0] if '/' in symbol else symbol,
|
|
|
+ 'cycle_start': pos['cycle_start'],
|
|
|
+ 'cycle_end': timestamp,
|
|
|
+ 'buy_orders': len(cycle_buys),
|
|
|
+ 'sell_orders': len(cycle_sells),
|
|
|
+ 'total_orders': len(pos['cycle_trades']),
|
|
|
+ 'total_amount': sum(t['amount'] for t in cycle_buys),
|
|
|
+ 'avg_entry_price': total_buy_value / sum(t['amount'] for t in cycle_buys) if cycle_buys else 0,
|
|
|
+ 'avg_exit_price': total_sell_value / sum(t['amount'] for t in cycle_sells) if cycle_sells else 0,
|
|
|
+ 'total_pnl': total_cycle_pnl,
|
|
|
+ 'buy_value': total_buy_value,
|
|
|
+ 'sell_value': total_sell_value,
|
|
|
+ 'cycle_trades': pos['cycle_trades'].copy()
|
|
|
+ }
|
|
|
+
|
|
|
+ completed_cycles.append(completed_cycle)
|
|
|
+
|
|
|
+ # Reset for next cycle
|
|
|
+ pos['cycle_trades'] = []
|
|
|
+ pos['cycle_start'] = None
|
|
|
+
|
|
|
+ return completed_cycles
|