Przeglądaj źródła

Update bot version to 2.1.2 and refactor type imports across modules - Removed unused imports in alarm_manager, hyperliquid_client, logging_config, and telegram_bot. Enhanced trading_stats to calculate completed trade cycles, improving performance metrics and reporting accuracy.

Carles Sentis 5 dni temu
rodzic
commit
65b089e7f9

+ 1 - 2
src/alarm_manager.py

@@ -9,8 +9,7 @@ import json
 import os
 import logging
 from datetime import datetime
-from typing import Dict, List, Any, Optional, Tuple
-from config import Config
+from typing import Dict, List, Any, Optional
 
 logger = logging.getLogger(__name__)
 

+ 1 - 2
src/hyperliquid_client.py

@@ -1,7 +1,6 @@
-import asyncio
 import logging
 from typing import Optional, Dict, Any, List
-from hyperliquid import HyperliquidSync, HyperliquidAsync
+from hyperliquid import HyperliquidSync
 from config import Config
 
 # Use existing logger setup (will be configured by main application)

+ 0 - 1
src/logging_config.py

@@ -5,7 +5,6 @@ Logging configuration module with rotation and cleanup support.
 
 import logging
 import logging.handlers
-import os
 from pathlib import Path
 from datetime import datetime, timedelta
 from typing import Optional

+ 13 - 6
src/telegram_bot.py

@@ -6,13 +6,11 @@ This module provides a Telegram interface for manual Hyperliquid trading
 with comprehensive statistics tracking and phone-friendly controls.
 """
 
-import logging
 import asyncio
-import re
 import json
 import os
 from datetime import datetime, timedelta
-from typing import Optional, Dict, Any, List, Tuple
+from typing import Optional, Dict, Any
 from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
 from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters
 from hyperliquid_client import HyperliquidClient
@@ -617,10 +615,19 @@ Tap any button below for instant access to bot functions:
                     symbol = position.get('symbol', 'Unknown')
                     contracts = float(position.get('contracts', 0))
                     unrealized_pnl = float(position.get('unrealizedPnl', 0))
-                    entry_price = float(position.get('entryPx', 0))
                     
-                    # Calculate position value and P&L percentage
-                    position_value = abs(contracts) * entry_price
+                    # Use the correct field name for entry price
+                    entry_price = float(position.get('entryPrice', 0))  # Changed from 'entryPx' to 'entryPrice'
+                    
+                    # Try to get position value from notional first, then calculate if not available
+                    notional = position.get('notional')
+                    if notional is not None:
+                        position_value = float(notional)
+                    else:
+                        # Fallback calculation
+                        position_value = abs(contracts) * entry_price
+                    
+                    # Calculate P&L percentage based on position value
                     pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
                     
                     pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"

+ 187 - 85
src/trading_stats.py

@@ -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 

+ 1 - 1
trading_bot.py

@@ -15,7 +15,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.1.1"
+BOT_VERSION = "2.1.2"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))