diff --git a/backend/app/models/paper_trading.py b/backend/app/models/paper_trading.py index 363ae15..33229d2 100644 --- a/backend/app/models/paper_trading.py +++ b/backend/app/models/paper_trading.py @@ -60,7 +60,9 @@ class PaperOrder(Base): exit_price = Column(Float, nullable=True) # 出场价 # 仓位信息 - quantity = Column(Float, default=1000) # 仓位大小 (USDT) + quantity = Column(Float, default=1000) # 持仓价值 (USDT) + margin = Column(Float, default=50) # 保证金 (USDT) + leverage = Column(Integer, default=20) # 杠杆倍数 # 信号信息 signal_grade = Column(SQLEnum(SignalGrade), default=SignalGrade.D) @@ -107,7 +109,9 @@ class PaperOrder(Base): 'take_profit': self.take_profit, 'filled_price': self.filled_price, 'exit_price': self.exit_price, - 'quantity': self.quantity, + 'quantity': self.quantity, # 持仓价值 + 'margin': getattr(self, 'margin', self.quantity / 20), # 保证金 + 'leverage': getattr(self, 'leverage', 20), # 杠杆倍数 'signal_grade': self.signal_grade.value if self.signal_grade else None, 'signal_type': self.signal_type, 'confidence': self.confidence, diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index f09bd6f..12db623 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -304,7 +304,9 @@ class PaperTradingService: stop_loss=signal.get('stop_loss', 0), take_profit=signal.get('take_profit', 0), filled_price=filled_price, - quantity=quantity, + quantity=quantity, # 持仓价值 + margin=margin, # 保证金 + leverage=self.leverage, # 杠杆倍数 signal_grade=SignalGrade(grade), signal_type=signal.get('signal_type') or signal.get('type', 'swing'), confidence=signal.get('confidence', 0), @@ -352,10 +354,15 @@ class PaperTradingService: def _calculate_dynamic_position(self, position_size: str, symbol: str) -> tuple: """ - 根据 LLM 建议的仓位大小计算实际保证金和持仓价值 + 根据 LLM 建议的仓位大小计算实际保证金和持仓价值(复利策略) + + 计算逻辑: + - 可用保证金 = 余额 - (持仓 + 挂单占用保证金) + - 根据 position_size 按可用保证金的百分比分配 + - 持仓价值 = 保证金 × 杠杆 Args: - position_size: 'heavy' / 'medium' / 'light' + position_size: 'heavy' / 'medium' / 'light' / 'micro' symbol: 交易对 Returns: @@ -364,49 +371,45 @@ class PaperTradingService: # 获取当前账户状态 account = self.get_account_status() balance = account['current_balance'] - used_margin = account['used_margin'] - max_leverage = self.leverage # 最大杠杆 20x + used_margin = account['used_margin'] # 已用保证金(持仓+挂单) - # 计算可用保证金空间 - # 全仓模式下:最大持仓价值 = 余额 × 最大杠杆 - max_position_value = balance * max_leverage - current_position_value = account['total_position_value'] - available_position_value = max_position_value - current_position_value + # 计算可用保证金(复利核心) + available_margin = balance - used_margin - if available_position_value <= 0: - logger.warning(f"已达最大杠杆限制,无法开仓") + if available_margin <= 0: + logger.warning(f"可用保证金不足,无法开仓(余额: ${balance:.2f}, 已用: ${used_margin:.2f})") return 0, 0 - # 根据 position_size 确定仓位比例 - # heavy: 可用空间的 30% - # medium: 可用空间的 15% - # light: 可用空间的 5% + # 根据 position_size 确定保证金比例(按可用保证金百分比) + # micro: 5%, light: 10%, medium: 15%, heavy: 20% size_ratio = { - 'heavy': 0.30, + 'micro': 0.05, + 'light': 0.10, 'medium': 0.15, - 'light': 0.05 - }.get(position_size, 0.05) + 'heavy': 0.20 + }.get(position_size, 0.10) - # 计算目标持仓价值 - target_position_value = available_position_value * size_ratio + # 计算目标保证金(直接使用可用保证金的百分比) + target_margin = available_margin * size_ratio - # 设置最小和最大限制 - min_position_value = 1000 # 最小持仓价值 1000 USDT - max_single_position = balance * 5 # 单笔最大不超过 5x 杠杆 + # 设置最小和最大保证金限制 + min_margin = 50 # 最小保证金 50 USDT(对应 1000 USDT 持仓价值) + max_single_margin = balance * 0.25 # 单笔最大不超过余额的 25% - position_value = max(min_position_value, min(target_position_value, max_single_position)) + margin = max(min_margin, min(target_margin, max_single_margin)) - # 确保不超过可用空间 - position_value = min(position_value, available_position_value) + # 确保不超过可用保证金 + margin = min(margin, available_margin) # 修正浮点数精度问题,保留 2 位小数 - position_value = round(position_value, 2) + margin = round(margin, 2) - # 计算对应的保证金 - margin = round(position_value / max_leverage, 2) + # 计算持仓价值(保证金 × 杠杆) + position_value = round(margin * self.leverage, 2) - logger.info(f"动态仓位计算: {position_size} | 可用空间: ${available_position_value:,.0f} | " - f"目标仓位: ${position_value:,.0f} | 保证金: ${margin:,.0f}") + logger.info(f"动态仓位计算: {position_size} | 余额: ${balance:.2f} | 已用保证金: ${used_margin:.2f} | " + f"可用保证金: ${available_margin:.2f} | 目标保证金: ${margin:.2f} ({size_ratio*100:.0f}%) | " + f"持仓价值: ${position_value:.2f}") return margin, position_value diff --git a/backend/scripts/migrate_add_margin_leverage.py b/backend/scripts/migrate_add_margin_leverage.py new file mode 100644 index 0000000..65cbfb4 --- /dev/null +++ b/backend/scripts/migrate_add_margin_leverage.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +添加 margin 和 leverage 字段到 paper_orders 表 + +运行方式: + cd backend && python scripts/migrate_add_margin_leverage.py +""" +import sys +import os + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.services.db_service import db_service +from sqlalchemy import text, inspect +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def check_column_exists(table_name, column_name): + """检查列是否存在""" + inspector = inspect(db_service.engine) + columns = [col['name'] for col in inspector.get_columns(table_name)] + return column_name in columns + + +def migrate(): + """执行迁移""" + try: + with db_service.engine.connect() as conn: + # 检查表是否存在 + inspector = inspect(db_service.engine) + if 'paper_orders' not in inspector.get_table_names(): + logger.error("❌ paper_orders 表不存在") + return False + + logger.info("开始迁移 paper_orders 表...") + + # 检查并添加 margin 列 + if not check_column_exists('paper_orders', 'margin'): + logger.info("➕ 添加 margin 列...") + conn.execute(text(""" + ALTER TABLE paper_orders + ADD COLUMN margin FLOAT DEFAULT 50 + """)) + conn.commit() + logger.info("✅ margin 列添加成功") + + # 为现有记录计算并设置 margin 值 + logger.info("🔄 为现有记录计算 margin...") + conn.execute(text(""" + UPDATE paper_orders + SET margin = ROUND(quantity / 20.0, 2) + WHERE margin IS NULL OR margin = 50 + """)) + conn.commit() + logger.info("✅ 现有记录的 margin 已更新") + else: + logger.info("✓ margin 列已存在,跳过") + + # 检查并添加 leverage 列 + if not check_column_exists('paper_orders', 'leverage'): + logger.info("➕ 添加 leverage 列...") + conn.execute(text(""" + ALTER TABLE paper_orders + ADD COLUMN leverage INTEGER DEFAULT 20 + """)) + conn.commit() + logger.info("✅ leverage 列添加成功") + + # 为现有记录设置 leverage 值 + logger.info("🔄 为现有记录设置 leverage...") + conn.execute(text(""" + UPDATE paper_orders + SET leverage = 20 + WHERE leverage IS NULL OR leverage = 20 + """)) + conn.commit() + logger.info("✅ 现有记录的 leverage 已更新") + else: + logger.info("✓ leverage 列已存在,跳过") + + logger.info("\n" + "=" * 50) + logger.info("✅ 迁移完成!") + logger.info("=" * 50) + + # 显示更新后的表结构 + inspector = inspect(db_service.engine) + columns = inspector.get_columns('paper_orders') + logger.info("\n📊 paper_orders 表结构:") + for col in columns: + if col['name'] in ['quantity', 'margin', 'leverage']: + logger.info(f" - {col['name']}: {col['type']}") + + return True + + except Exception as e: + logger.error(f"❌ 迁移失败: {e}") + import traceback + logger.error(traceback.format_exc()) + return False + + +if __name__ == "__main__": + success = migrate() + sys.exit(0 if success else 1) diff --git a/frontend/trading.html b/frontend/trading.html index 8943d4d..16a1dd8 100644 --- a/frontend/trading.html +++ b/frontend/trading.html @@ -1294,7 +1294,9 @@