Browse Source

Refactor stop-market order placement in HyperliquidClient and enhance daily stats reporting

- Updated the stop-market order placement logic in HyperliquidClient to pass the slippage price as the price parameter, ensuring compliance with exchange requirements.
- Improved logging for successful order placement, providing clearer feedback on order details.
- Enhanced daily stats calculations in DailyCommands to include total entry volume and updated ROE calculations for better performance reporting.
- Added drawdown monitoring updates in StatsCommands to reflect the latest balance, improving risk management insights.
Carles Sentis 1 day ago
parent
commit
967d42aae0

+ 3 - 13
src/clients/hyperliquid_client.py

@@ -469,20 +469,10 @@ class HyperliquidClient:
 
             logger.info(f"🛑 Placing STOP-MARKET order: {side} {amount} {symbol} with trigger @ ${stop_price_arg:.4f}")
             
-            # For a stop-market order, the 'price' argument to create_order is None.
-            # The 'type' argument to create_order is 'market'.
-            order = self.sync_client.create_order(
-                symbol, 
-                'market',  # Order type to execute when triggered
-                side, 
-                amount, 
-                None, # For stop-market, price is None; execution price determined by market when triggered
-                params=trigger_params
-            )
-            
-            logger.info(f"✅ Successfully placed stop-market order for {amount} {symbol}, trigger @ ${stop_price_arg:.4f}")
-            logger.debug(f"📄 Stop-market order details: {order}")
+            # Pass stop_price_arg as the price parameter for slippage calculation, as required by the exchange for market orders.
+            order = self.sync_client.create_order(symbol, 'market', side, amount, stop_price_arg, trigger_params)
             
+            logger.info(f"✅ Stop-market order placed successfully: {order}")
             return order, None
         except Exception as e:
             error_message = self._extract_error_message(e)

+ 5 - 2
src/commands/info/daily.py

@@ -33,13 +33,14 @@ class DailyCommands(InfoCommandsBase):
             total_trades_all_days = 0
             total_roe_all_days = 0
             trading_days_count = 0
+            total_entry_volume_all_days = 0
 
             period_lines = []
             for day_stats_item in daily_stats_list:
                 if day_stats_item.get('has_trades'):
                     pnl_emoji = "🟢" if day_stats_item.get('pnl', 0) >= 0 else "🔴"
                     pnl_str = await formatter.format_price_with_symbol(day_stats_item.get('pnl', 0))
-                    roe = day_stats_item.get('roe', 0.0)  # Get ROE from stats
+                    roe = day_stats_item.get('roe', 0.0)
                     roe_str = f"ROE: {roe:+.1f}%" if roe != 0 else ""
                     day_str = day_stats_item.get('date_formatted', 'Unknown')
                     period_lines.append(f"📅 <b>{day_str}</b>: {pnl_emoji} {pnl_str} ({day_stats_item.get('pnl_pct', 0):+.1f}%) {roe_str} | Trades: {day_stats_item.get('trades', 0)}")
@@ -47,6 +48,7 @@ class DailyCommands(InfoCommandsBase):
                     total_trades_all_days += day_stats_item.get('trades', 0)
                     total_roe_all_days += roe
                     trading_days_count += 1
+                    total_entry_volume_all_days += day_stats_item.get('entry_volume', 0.0)
                 else:
                     day_str = day_stats_item.get('date_formatted', 'Unknown')
                     period_lines.append(f"📅 <b>{day_str}</b>: 📭 No trading activity")
@@ -60,10 +62,11 @@ class DailyCommands(InfoCommandsBase):
                 avg_pnl_emoji = "🟢" if avg_daily_pnl >= 0 else "🔴"
                 total_pnl_all_days_str = await formatter.format_price_with_symbol(total_pnl_all_days)
                 avg_daily_pnl_str = await formatter.format_price_with_symbol(avg_daily_pnl)
+                total_period_roe = (total_pnl_all_days / total_entry_volume_all_days * 100) if total_entry_volume_all_days > 0 else 0.0
 
                 daily_text_parts.append(f"\n\n📈 <b>Period Summary:</b>")
                 daily_text_parts.append(f"  Total P&L: {avg_pnl_emoji} {total_pnl_all_days_str} | Avg Daily: {avg_daily_pnl_str}")
-                daily_text_parts.append(f"  Total ROE: {total_roe_all_days:+.1f}% | Avg Daily ROE: {avg_daily_roe:+.1f}%")
+                daily_text_parts.append(f"  Total ROE: {total_period_roe:+.1f}% | Avg Daily ROE: {avg_daily_roe:+.1f}%")
                 daily_text_parts.append(f"  Trading Days: {trading_days_count}/10 | Total Trades: {total_trades_all_days}")
             else:
                 if not period_lines:

+ 5 - 0
src/commands/info/stats.py

@@ -33,6 +33,11 @@ class StatsCommands(InfoCommandsBase):
             balance_info = self.trading_engine.get_balance()
             current_balance = float(balance_info.get('total', {}).get('USDC', 0.0))
             
+            # Manually update the drawdown monitor with the latest balance
+            if self.trading_engine.stats and hasattr(self.trading_engine.stats, 'drawdown_monitor'):
+                logger.debug(f"Updating drawdown monitor with current balance: {current_balance}")
+                self.trading_engine.stats.drawdown_monitor.update_balance(current_balance)
+            
             report = self.trading_engine.stats.get_summary_report()
             if not report or not report.get('performance_stats'):
                 await self._reply(update, "❌ Trading stats not available yet.")

+ 24 - 2
src/stats/aggregation_manager.py

@@ -160,6 +160,26 @@ class AggregationManager:
             ))
             logger.info(f"Successfully aggregated closed trade for {token}. P&L: {realized_pnl}, ROE: {current_roe:.2f}%")
 
+            # 4. Update daily aggregated stats
+            try:
+                if closed_at:
+                    date_str = datetime.fromisoformat(closed_at).strftime('%Y-%m-%d')
+                    daily_upsert_query = """
+                        INSERT INTO daily_aggregated_stats (date, token, realized_pnl, completed_cycles, entry_volume, exit_volume)
+                        VALUES (?, ?, ?, 1, ?, ?)
+                        ON CONFLICT(date, token) DO UPDATE SET
+                            realized_pnl = realized_pnl + excluded.realized_pnl,
+                            completed_cycles = completed_cycles + 1,
+                            entry_volume = entry_volume + excluded.entry_volume,
+                            exit_volume = exit_volume + excluded.exit_volume
+                    """
+                    self.db._execute_query(daily_upsert_query, (
+                        date_str, token, realized_pnl, entry_value, exit_value
+                    ))
+                    logger.info(f"Successfully aggregated daily stats for {token} on {date_str}.")
+            except Exception as e:
+                logger.error(f"Error migrating trade to daily aggregated stats for token {token}: {e}", exc_info=True)
+
         except Exception as e:
             logger.error(f"Error migrating closed position to aggregated stats for token {token}: {e}", exc_info=True)
             raise
@@ -239,7 +259,7 @@ class AggregationManager:
             date_formatted = target_date.strftime('%m/%d')
 
             day_aggregated_data = self.db._fetch_query(
-                "SELECT SUM(realized_pnl) as pnl, SUM(completed_cycles) as trades, SUM(exit_volume) as volume FROM daily_aggregated_stats WHERE date = ?",
+                "SELECT SUM(realized_pnl) as pnl, SUM(completed_cycles) as trades, SUM(exit_volume) as volume, SUM(entry_volume) as entry_volume FROM daily_aggregated_stats WHERE date = ?",
                 (date_str,)
             )
             
@@ -248,7 +268,9 @@ class AggregationManager:
                 stats_for_day = day_aggregated_data[0]
                 pnl = stats_for_day.get('pnl', 0.0) or 0.0
                 volume = stats_for_day.get('volume', 0.0) or 0.0
+                entry_volume = stats_for_day.get('entry_volume', 0.0) or 0.0
                 stats_for_day['pnl_pct'] = (pnl / volume * 100) if volume > 0 else 0.0
+                stats_for_day['roe'] = (pnl / entry_volume * 100) if entry_volume > 0 else 0.0
                 stats_for_day['trades'] = int(stats_for_day.get('trades', 0) or 0)
 
             if stats_for_day and stats_for_day['trades'] > 0:
@@ -259,7 +281,7 @@ class AggregationManager:
             else:
                 daily_stats_list.append({
                     'date': date_str, 'date_formatted': date_formatted, 'has_trades': False,
-                    'trades': 0, 'pnl': 0.0, 'volume': 0.0, 'pnl_pct': 0.0
+                    'trades': 0, 'pnl': 0.0, 'volume': 0.0, 'pnl_pct': 0.0, 'roe': 0.0
                 })
         return daily_stats_list
 

+ 5 - 1
src/stats/trading_stats.py

@@ -99,7 +99,7 @@ class TradingStats:
             symbol, side, order_type, amount_requested, price, 
             bot_order_ref_id, exchange_order_id, status
         )
-        return result is not None
+        return result
     
     def update_order_exchange_id(self, bot_order_ref_id: str, exchange_order_id: str) -> bool:
         """Update order with exchange ID."""
@@ -141,6 +141,10 @@ class TradingStats:
         """Get order by bot reference ID."""
         return self.order_manager.get_order_by_bot_ref_id(bot_order_ref_id)
 
+    def cancel_pending_stop_losses_by_symbol(self, symbol: str, reason: str) -> int:
+        """Cancel pending stop losses for a symbol. Delegates to OrderManager."""
+        return self.order_manager.cancel_pending_stop_losses_by_symbol(symbol, new_status=reason)
+
     def has_exchange_fill_been_processed(self, exchange_fill_id: str) -> bool:
         """Check if an exchange fill ID has already been processed."""
         try:

+ 1 - 1
trading_bot.py

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