From 7e8178b6741a0f63b598a69df450095993c18d9a Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 19 Feb 2026 19:32:46 +0800 Subject: [PATCH] update --- .env.example | 137 ++++++++++- backend/app/config.py | 11 + .../app/crypto_agent/llm_signal_analyzer.py | 42 +++- backend/app/crypto_agent/signal_analyzer.py | 147 ++++++++++- backend/app/models/paper_trading.py | 6 + backend/app/services/binance_service.py | 14 +- backend/app/services/paper_trading_service.py | 231 ++++++++++++++++-- backend/migrate_db.py | 149 +++++++++++ 8 files changed, 694 insertions(+), 43 deletions(-) create mode 100644 backend/migrate_db.py diff --git a/.env.example b/.env.example index e1d9ebc..200dd12 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,147 @@ -# Tushare API +# ============================================================================ +# Stock Agent 环境变量配置文件 +# ============================================================================ +# 复制此文件为 .env 并填入你的实际配置值 +# ============================================================================ + +# ---------------------------------------------------------------------------- +# API 密钥配置 +# ---------------------------------------------------------------------------- + +# Tushare API(用于获取A股数据) TUSHARE_TOKEN=your_tushare_token_here # 智谱AI GLM-4 API ZHIPUAI_API_KEY=your_zhipuai_key_here -# Database (使用SQLite,无需额外配置) +# DeepSeek API(推荐用于 SmartAgent 和 CryptoAgent) +DEEPSEEK_API_KEY=your_deepseek_key_here + +# Brave Search API(用于搜索实时新闻和市场信息) +BRAVE_API_KEY=your_brave_api_key_here + +# Binance API(公开数据不需要,私有交易需要) +BINANCE_API_KEY= +BINANCE_API_SECRET= + +# ---------------------------------------------------------------------------- +# 数据库配置 +# ---------------------------------------------------------------------------- DATABASE_URL=sqlite:///./stock_agent.db -# API Settings +# ---------------------------------------------------------------------------- +# API 服务配置 +# ---------------------------------------------------------------------------- API_HOST=0.0.0.0 API_PORT=8000 DEBUG=True -# Security +# ---------------------------------------------------------------------------- +# 安全配置 +# ---------------------------------------------------------------------------- +# JWT 密钥(生产环境必须修改) SECRET_KEY=your_secret_key_here_change_in_production +# JWT 算法 +JWT_ALGORITHM=HS256 +# JWT 过期天数 +JWT_EXPIRE_DAYS=7 +# API 访问频率限制 RATE_LIMIT=100/minute -# CORS +# ---------------------------------------------------------------------------- +# 跨域配置 (CORS) +# ---------------------------------------------------------------------------- CORS_ORIGINS=http://localhost:8000,http://127.0.0.1:8000 + +# ---------------------------------------------------------------------------- +# 腾讯云短信配置 +# ---------------------------------------------------------------------------- +TENCENT_SMS_APP_ID=1400961527 +TENCENT_SMS_SECRET_ID=your_tencent_secret_id_here +TENCENT_SMS_SECRET_KEY=your_tencent_secret_key_here +TENCENT_SMS_SIGN_ID=629073 +TENCENT_SMS_TEMPLATE_ID=2353142 + +# 验证码配置 +CODE_EXPIRE_MINUTES=5 +CODE_RESEND_SECONDS=60 +CODE_MAX_PER_HOUR=10 + +# 白名单手机号(无需验证码即可登录,逗号分隔) +WHITELIST_PHONES=18583366860,18583926860 + +# ---------------------------------------------------------------------------- +# 通知配置 +# ---------------------------------------------------------------------------- +# 飞书机器人 +FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/8a1dcf69-6753-41e2-a393-edc4f7822db0 +FEISHU_ENABLED=true + +# Telegram 机器人 +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here +TELEGRAM_CHANNEL_ID=your_telegram_channel_id_here +TELEGRAM_ENABLED=true + +# ---------------------------------------------------------------------------- +# 加密货币交易智能体配置 +# ---------------------------------------------------------------------------- +# 监控的交易对(逗号分隔) +CRYPTO_SYMBOLS=BTCUSDT,ETHUSDT +# 分析间隔(秒) +CRYPTO_ANALYSIS_INTERVAL=60 +# 触发 LLM 分析的置信度阈值(0-1) +CRYPTO_LLM_THRESHOLD=0.70 + +# ---------------------------------------------------------------------------- +# 模拟交易配置 +# ---------------------------------------------------------------------------- +# 是否启用模拟交易 +PAPER_TRADING_ENABLED=true +# 初始本金 (USDT) +PAPER_TRADING_INITIAL_BALANCE=10000 +# 杠杆倍数(全仓模式下的最大杠杆) +PAPER_TRADING_LEVERAGE=20 +# 每单保证金 (USDT) +PAPER_TRADING_MARGIN_PER_ORDER=1000 +# 最大持仓+挂单总数 +PAPER_TRADING_MAX_ORDERS=10 +# 是否自动平掉反向持仓(智能策略) +PAPER_TRADING_AUTO_CLOSE_OPPOSITE=false +# 保本止损触发阈值(盈利百分比),0表示禁用 +PAPER_TRADING_BREAKEVEN_THRESHOLD=1 + +# ---------------------------------------------------------------------------- +# 移动止损配置 +# ---------------------------------------------------------------------------- +# 是否启用移动止损 +PAPER_TRADING_TRAILING_STOP_ENABLED=true +# 移动止损触发倍数(相对于保本阈值) +PAPER_TRADING_TRAILING_STOP_THRESHOLD_MULTIPLIER=2 +# 移动止损跟随比例(0-1之间,1表示完全跟随) +PAPER_TRADING_TRAILING_STOP_RATIO=0.5 + +# ---------------------------------------------------------------------------- +# 动态止盈配置(趋势过滤) +# ---------------------------------------------------------------------------- +# 是否启用动态止盈 +PAPER_TRADING_DYNAMIC_TP_ENABLED=true +# 强趋势时移动止损跟随比例(70%) +PAPER_TRADING_STRONG_TREND_RATIO=0.7 +# 弱趋势时移动止损跟随比例(30%) +PAPER_TRADING_WEAK_TREND_RATIO=0.3 +# 震荡市固定止盈百分比(3%) +PAPER_TRADING_SIDEWAYS_TP_PERCENT=3 + +# ---------------------------------------------------------------------------- +# 仓位配置(废弃,保留兼容性) +# ---------------------------------------------------------------------------- +PAPER_TRADING_POSITION_A=1000 +PAPER_TRADING_POSITION_B=500 +PAPER_TRADING_POSITION_C=200 + +# ---------------------------------------------------------------------------- +# Agent 模型配置 +# ---------------------------------------------------------------------------- +# 可选值: zhipu, deepseek +SMART_AGENT_MODEL=deepseek +CRYPTO_AGENT_MODEL=deepseek diff --git a/backend/app/config.py b/backend/app/config.py index 81d6ad1..c9b97e4 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -118,6 +118,17 @@ class Settings(BaseSettings): paper_trading_max_orders: int = 10 # 最大持仓+挂单总数 paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略) paper_trading_breakeven_threshold: float = 1 # 保本止损触发阈值(盈利百分比),0表示禁用 + + # 移动止损配置 + paper_trading_trailing_stop_enabled: bool = True # 是否启用移动止损 + paper_trading_trailing_stop_threshold_multiplier: float = 2 # 移动止损触发倍数(相对于保本阈值) + paper_trading_trailing_stop_ratio: float = 0.5 # 移动止损跟随比例(0-1之间,1表示完全跟随) + + # 动态止盈配置(趋势过滤) + paper_trading_dynamic_tp_enabled: bool = True # 是否启用动态止盈 + paper_trading_strong_trend_ratio: float = 0.7 # 强趋势时移动止损跟随比例(70%) + paper_trading_weak_trend_ratio: float = 0.3 # 弱趋势时移动止损跟随比例(30%) + paper_trading_sideways_tp_percent: float = 3 # 震荡市固定止盈百分比(3%) # 废弃的配置(保留兼容性) paper_trading_position_a: float = 1000 # A级信号仓位 (USDT) paper_trading_position_b: float = 500 # B级信号仓位 (USDT) diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py index 245cdcd..2ce7681 100644 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ b/backend/app/crypto_agent/llm_signal_analyzer.py @@ -155,14 +155,48 @@ class LLMSignalAnalyzer: - 单一交易对持仓不宜过大 - 如果当前持仓已经较重,即使有好机会也要控制仓位 +## 八、止损止盈策略(重要更新) + +### 止损设置原则(结构化止损) +**不要使用固定百分比或固定ATR倍数!止损必须基于关键价位:** + +1. **做多止损**: + - 优先放在最近支撑位(前低)下方 0.3-0.5% + - 如果有 MA20 支撑,可放在 MA20 下方 0.5% + - 如果最近低点距离过近(<1%),则使用 ATR 1.2-1.5倍 + - 避免止损距离过大(>4%ATR) + +2. **做空止损**: + - 优先放在最近阻力位(前高)上方 0.3-0.5% + - 如果有 MA20 阻力,可放在 MA20 上方 0.5% + - 如果最近高点距离过近(<1%),则使用 ATR 1.2-1.5倍 + - 避免止损距离过大(>4%ATR) + +### 止盈设置(移动止盈策略) +**不设固定止盈位,让利润奔跑!** + +1. **take_profit 设置为保险价位**: + - 做多:入场价 + 15%(作为极端情况的保险止盈) + - 做空:入场价 - 15% + - 这个价位只是"保险",正常情况下不会触及 + +2. **实际止盈靠移动止损**: + - 系统会通过移动止损自动锁定利润 + - 盈利 2% 后开始移动止损,锁定部分利润 + - 盈利越多,止损跟随移动,确保吃到趋势 + +### 风险收益比 +- 虽然不设固定止盈,但仍要确保止损合理 +- 理想情况下,潜在风险(止损距离)应控制在 2-3% 以内 + ## 重要原则 1. **量价优先** - 任何信号都必须有量能配合才可靠 2. **积极但不冒进** - 有合理依据就给出信号,不要过于保守 3. 每种类型最多输出一个信号 -4. 止损必须明确,风险收益比至少 1:1.5 -5. reason 字段必须包含量价分析(如"放量突破+RSI=45,量比1.8确认有效") -6. entry_type 必须明确:信号已触发用 market,等待更好价位用 limit -7. 短线信号止损控制在 1-2%,中线信号止损控制在 2-4% +4. 止损必须基于关键支撑/阻力位(前低前高、MA20),不要用固定百分比 +5. 止盈设置为保险价位(做多+15%,做空-15%),实际靠移动止损锁定利润 +6. reason 字段必须包含量价分析(如"放量突破+RSI=45,量比1.8确认有效") +7. entry_type 必须明确:信号已触发用 market,等待更好价位用 limit 8. **position_size 必须明确**:根据信号质量和持仓情况给出 heavy/medium/light""" def __init__(self): diff --git a/backend/app/crypto_agent/signal_analyzer.py b/backend/app/crypto_agent/signal_analyzer.py index 19183b0..a97ecfa 100644 --- a/backend/app/crypto_agent/signal_analyzer.py +++ b/backend/app/crypto_agent/signal_analyzer.py @@ -1249,29 +1249,156 @@ class SignalAnalyzer: return "\n".join(parts) def calculate_stop_loss_take_profit(self, price: float, action: str, - atr: float) -> Dict[str, float]: + atr: float, df: pd.DataFrame = None) -> Dict[str, float]: """ - 计算止损止盈位置 + 计算止损止盈位置 - 结构化止损 + 移动止盈策略 + + 策略: + - 止损:基于关键支撑/阻力位(前高前低、均线) + - 止盈:不设固定止盈,通过移动止损锁定利润 Args: price: 当前价格 action: 'buy' 或 'sell' atr: ATR 值 + df: K线数据(用于计算关键价位) Returns: - {'stop_loss': float, 'take_profit': float} + {'stop_loss': float, 'take_profit': float, 'method': str} """ + if df is not None and len(df) >= 20: + # 使用结构化止损 + result = self._calculate_structured_stops(price, action, atr, df) + if result['stop_loss'] > 0: + return result + + # 回退到 ATR 止损 if action == 'buy': - stop_loss = price - atr * 2 - take_profit = price + atr * 3 + stop_loss = price - atr * 1.5 # 降低到 1.5 倍 ATR + # 止盈设置得很远,主要靠移动止损 + take_profit = price * 1.15 # 15% 作为一个"保险"止盈位 + method = 'atr' elif action == 'sell': - stop_loss = price + atr * 2 - take_profit = price - atr * 3 + stop_loss = price + atr * 1.5 + # 止盈设置得很远,主要靠移动止损 + take_profit = price * 0.85 # 15% 作为一个"保险"止盈位 + method = 'atr' else: - stop_loss = 0 - take_profit = 0 + return {'stop_loss': 0, 'take_profit': 0, 'method': 'none'} return { 'stop_loss': round(stop_loss, 2), - 'take_profit': round(take_profit, 2) + 'take_profit': round(take_profit, 2), + 'method': method + } + + def _calculate_structured_stops(self, price: float, action: str, + atr: float, df: pd.DataFrame) -> Dict[str, float]: + """ + 基于关键价位计算结构化止损(无固定止盈,靠移动止损) + + 原则: + - 做多止损在前低/MA20下方 + - 做空止损在前高/MA20上方 + - 止损距离控制在 1.5-3% ATR 范围内 + - 止盈设置得很远,主要靠移动止损锁定利润 + """ + stop_loss = 0 + take_profit = 0 + + # 获取关键价位 + recent_data = df.tail(50) + + if action == 'buy': + # === 做多止损 === + # 1. 查找近期支撑位(前低) + recent_lows = recent_data['low'].values + # 找出低于当前价的低点 + valid_lows = [low for low in recent_lows if low < price * 0.99] + if valid_lows: + nearest_low = max(valid_lows) # 最近的低点 + # 止损放在前低下方 0.3% + sl_candidate = nearest_low * 0.997 + else: + sl_candidate = 0 + + # 2. 检查 MA20 作为支撑 + ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else 0 + if pd.notna(ma20) and ma20 < price * 0.98: + sl_ma20 = ma20 * 0.995 # MA20 下方 0.5% + # 选择更保守的止损(更高的止损位) + if sl_candidate > 0: + sl_candidate = max(sl_candidate, sl_ma20) + else: + sl_candidate = sl_ma20 + + # 3. 验证止损距离合理性(1.5%-3% ATR) + if sl_candidate > 0: + sl_distance = (price - sl_candidate) / price * 100 + atr_percent = atr / price * 100 + + # 如果止损距离太小(< 1%ATR),使用 ATR 止损 + if sl_distance < atr_percent * 1.0: + sl_candidate = price - atr * 1.2 + # 如果止损距离过大(> 4%ATR),限制在合理范围 + elif sl_distance > atr_percent * 4: + sl_candidate = price - atr * 2.5 + + # 4. 回退到 ATR 止损 + if sl_candidate <= 0: + sl_candidate = price - atr * 1.5 + + stop_loss = sl_candidate + + # === 做多止盈(设置得很远,主要靠移动止损)=== + take_profit = price * 1.15 # 15% 作为保险止盈位 + + elif action == 'sell': + # === 做空止损 === + # 1. 查找近期阻力位(前高) + recent_highs = recent_data['high'].values + # 找出高于当前价的高点 + valid_highs = [high for high in recent_highs if high > price * 1.01] + if valid_highs: + nearest_high = min(valid_highs) # 最近的高点 + # 止损放在前高上方 0.3% + sl_candidate = nearest_high * 1.003 + else: + sl_candidate = 0 + + # 2. 检查 MA20 作为阻力 + ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else 0 + if pd.notna(ma20) and ma20 > price * 1.02: + sl_ma20 = ma20 * 1.005 # MA20 上方 0.5% + # 选择更保守的止损(更低的止损位) + if sl_candidate > 0: + sl_candidate = min(sl_candidate, sl_ma20) + else: + sl_candidate = sl_ma20 + + # 3. 验证止损距离合理性 + if sl_candidate > 0: + sl_distance = (sl_candidate - price) / price * 100 + atr_percent = atr / price * 100 + + # 如果止损距离太小(< 1%ATR),使用 ATR 止损 + if sl_distance < atr_percent * 1.0: + sl_candidate = price + atr * 1.2 + # 如果止损距离过大(> 4%ATR),限制在合理范围 + elif sl_distance > atr_percent * 4: + sl_candidate = price + atr * 2.5 + + # 4. 回退到 ATR 止损 + if sl_candidate <= 0: + sl_candidate = price + atr * 1.5 + + stop_loss = sl_candidate + + # === 做空止盈(设置得很远,主要靠移动止损)=== + take_profit = price * 0.85 # 15% 作为保险止盈位 + + return { + 'stop_loss': round(stop_loss, 2), + 'take_profit': round(take_profit, 2), + 'method': 'structured' } diff --git a/backend/app/models/paper_trading.py b/backend/app/models/paper_trading.py index efd0832..c09f14e 100644 --- a/backend/app/models/paper_trading.py +++ b/backend/app/models/paper_trading.py @@ -80,6 +80,10 @@ class PaperOrder(Base): max_profit = Column(Float, default=0) # 持仓期间最大盈利 breakeven_triggered = Column(Integer, default=0) # 是否触发过保本止损(0=否,1=是) + # 移动止损相关 + trailing_stop_triggered = Column(Integer, default=0) # 是否触发过移动止损(0=否,1=是) + trailing_stop_base_profit = Column(Float, default=0) # 移动止损触发时的盈利百分比 + # 时间戳 created_at = Column(DateTime, default=datetime.utcnow) opened_at = Column(DateTime, nullable=True) # 开仓时间 @@ -114,6 +118,8 @@ class PaperOrder(Base): 'max_drawdown': self.max_drawdown, 'max_profit': self.max_profit, 'breakeven_triggered': self.breakeven_triggered, + 'trailing_stop_triggered': getattr(self, 'trailing_stop_triggered', 0), + 'trailing_stop_base_profit': getattr(self, 'trailing_stop_base_profit', 0), 'created_at': self.created_at.isoformat() if self.created_at else None, 'opened_at': self.opened_at.isoformat() if self.opened_at else None, 'closed_at': self.closed_at.isoformat() if self.closed_at else None, diff --git a/backend/app/services/binance_service.py b/backend/app/services/binance_service.py index bd65f73..b457ba5 100644 --- a/backend/app/services/binance_service.py +++ b/backend/app/services/binance_service.py @@ -76,9 +76,21 @@ class BinanceService: Returns: 包含 5m, 15m, 1h, 4h 数据的字典 """ + # 不同周期使用不同的数据量,平衡分析深度和性能 + # 5m: 200根 = 16.7小时(日内分析) + # 15m: 200根 = 2.1天(短线分析) + # 1h: 300根 = 12.5天(中线分析) + # 4h: 200根 = 33.3天(趋势分析) + limits = { + '5m': 200, + '15m': 200, + '1h': 300, + '4h': 200 + } + data = {} for interval in ['5m', '15m', '1h', '4h']: - df = self.get_klines(symbol, interval, limit=100) + df = self.get_klines(symbol, interval, limit=limits.get(interval, 100)) if not df.empty: df = self.calculate_indicators(df) data[interval] = df diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 8d324fc..92c685a 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -27,13 +27,28 @@ class PaperTradingService: self.auto_close_opposite = self.settings.paper_trading_auto_close_opposite # 是否自动平掉反向持仓 self.breakeven_threshold = self.settings.paper_trading_breakeven_threshold # 保本止损触发阈值 + # 移动止损配置 + self.trailing_stop_enabled = self.settings.paper_trading_trailing_stop_enabled + self.trailing_stop_threshold_multiplier = self.settings.paper_trading_trailing_stop_threshold_multiplier + self.trailing_stop_ratio = self.settings.paper_trailing_stop_ratio + + # 动态止盈配置 + self.dynamic_tp_enabled = self.settings.paper_trading_dynamic_tp_enabled + self.strong_trend_ratio = self.settings.paper_trading_strong_trend_ratio + self.weak_trend_ratio = self.settings.paper_trading_weak_trend_ratio + self.sideways_tp_percent = self.settings.paper_trading_sideways_tp_percent + # 确保表已创建 self._ensure_table_exists() # 加载活跃订单到内存 self._load_active_orders() - logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'},保本止损阈值: {self.breakeven_threshold}%)") + logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'}," + f"保本止损阈值: {self.breakeven_threshold}%," + f"移动止损: {'启用' if self.trailing_stop_enabled else '禁用'}," + f"触发倍数: {self.trailing_stop_threshold_multiplier}x," + f"跟随比例: {self.trailing_stop_ratio * 100}%)") def _ensure_table_exists(self): """确保数据表已创建,并迁移新字段""" @@ -42,19 +57,32 @@ class PaperTradingService: from sqlalchemy import text Base.metadata.create_all(bind=db_service.engine) - # 检查并添加新字段 breakeven_triggered db = db_service.get_session() try: - # 尝试查询新字段,如果失败则添加 - db.execute(text("SELECT breakeven_triggered FROM paper_orders LIMIT 1")) - except Exception: + # 检查并添加新字段 breakeven_triggered try: - db.execute(text("ALTER TABLE paper_orders ADD COLUMN breakeven_triggered INTEGER DEFAULT 0")) - db.commit() - logger.info("数据库迁移: 添加 breakeven_triggered 字段") - except Exception as e: - logger.warning(f"添加 breakeven_triggered 字段失败(可能已存在): {e}") - db.rollback() + db.execute(text("SELECT breakeven_triggered FROM paper_orders LIMIT 1")) + except Exception: + try: + db.execute(text("ALTER TABLE paper_orders ADD COLUMN breakeven_triggered INTEGER DEFAULT 0")) + db.commit() + logger.info("数据库迁移: 添加 breakeven_triggered 字段") + except Exception as e: + logger.warning(f"添加 breakeven_triggered 字段失败(可能已存在): {e}") + db.rollback() + + # 检查并添加移动止损相关字段 + try: + db.execute(text("SELECT trailing_stop_triggered FROM paper_orders LIMIT 1")) + except Exception: + try: + db.execute(text("ALTER TABLE paper_orders ADD COLUMN trailing_stop_triggered INTEGER DEFAULT 0")) + db.execute(text("ALTER TABLE paper_orders ADD COLUMN trailing_stop_base_profit REAL DEFAULT 0")) + db.commit() + logger.info("数据库迁移: 添加 trailing_stop_triggered 和 trailing_stop_base_profit 字段") + except Exception as e: + logger.warning(f"添加移动止损字段失败(可能已存在): {e}") + db.rollback() finally: db.close() @@ -541,8 +569,79 @@ class PaperTradingService: finally: db.close() + + def _assess_trend_strength(self, order: PaperOrder, current_price: float) -> str: + """ + 评估当前趋势强度(用于动态止盈) + + Returns: + 'strong' - 强趋势(移动止损跟随比例高) + 'weak' - 弱趋势(移动止损跟随比例低) + 'sideways' - 震荡市(考虑固定止盈) + """ + # 基于订单的最大盈利和当前盈亏判断趋势强度 + max_profit = order.max_profit + current_pnl_percent = 0 + + if order.side == OrderSide.LONG: + current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100 + else: + current_pnl_percent = ((order.filled_price - current_price) / order.filled_price) * 100 + + # 判断1:盈利是否持续创新高 + profit_streak = (current_pnl_percent >= max_profit * 0.8) # 当前盈利接近最大盈利 + + # 判断2:最大盈利是否足够大(说明有趋势) + significant_profit = max_profit >= 3 # 至少3%盈利 + + # 判断3:当前盈利是否回撤较多 + profit_pullback = (max_profit - current_pnl_percent) / max_profit if max_profit > 0 else 0 + + # 综合判断 + if profit_streak and significant_profit and profit_pullback < 0.3: + return 'strong' + elif significant_profit and profit_pullback >= 0.5: + # 盈利回撤超过50%,可能是震荡或反转 + return 'sideways' + elif max_profit < 1.5: + # 盈利不到1.5%,趋势尚未确立 + return 'weak' + else: + return 'weak' + + def _get_dynamic_ratio(self, order: PaperOrder, current_price: float) -> float: + """ + 根据趋势强度动态计算移动止损跟随比例 + + Returns: + 跟随比例(0-1之间) + """ + if not self.dynamic_tp_enabled: + return self.trailing_stop_ratio + + trend_strength = self._assess_trend_strength(order, current_price) + + if trend_strength == 'strong': + # 强趋势:高跟随比例,锁定更多利润 + return self.strong_trend_ratio + elif trend_strength == 'sideways': + # 震荡市:低跟随比例,避免被来回扫 + return self.weak_trend_ratio + else: + # 弱趋势:中等跟随比例 + return self.trailing_stop_ratio + def _update_order_extremes(self, order: PaperOrder, current_price: float) -> Optional[Dict[str, Any]]: - """更新订单的最大回撤和最大盈利,并检查是否需要移动止损到保本""" + """ + 更新订单的最大回撤和最大盈利,并检查是否需要移动止损 + + 止损策略(按优先级): + 1. 移动止损:盈利达到阈值倍数后,止损按比例跟随盈利移动 + 2. 保本止损:盈利达到阈值时,将止损移动到开仓价 + + Returns: + 如果触发了止损移动,返回通知字典;否则返回 None + """ if order.side == OrderSide.LONG: current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100 else: @@ -550,7 +649,9 @@ class PaperTradingService: # 检查是否需要更新极值 needs_update = False - breakeven_triggered = False + stop_moved = False + stop_move_type = "" # "breakeven" 或 "trailing" + new_stop_loss = None if current_pnl_percent > order.max_profit: order.max_profit = current_pnl_percent @@ -560,17 +661,93 @@ class PaperTradingService: order.max_drawdown = current_pnl_percent needs_update = True - # 保本止损逻辑:当盈利达到阈值时,将止损移动到开仓价 + # === 移动止损逻辑(优先级高于保本止损) === + if self.trailing_stop_enabled and self.trailing_stop_threshold_multiplier > 0: + trailing_threshold = self.breakeven_threshold * self.trailing_stop_threshold_multiplier + + # 检查是否达到移动止损触发阈值 + if current_pnl_percent >= trailing_threshold: + trailing_triggered = getattr(order, 'trailing_stop_triggered', 0) == 1 + + if not trailing_triggered: + # 首次触发移动止损:记录基准盈利,并移动止损 + order.trailing_stop_triggered = 1 + order.trailing_stop_base_profit = current_pnl_percent + needs_update = True + stop_moved = True + stop_move_type = "trailing_first" + + # 计算新的止损价(锁定部分利润) + if order.side == OrderSide.LONG: + # 做多:新止损价 = 开仓价 * (1 + 盈利 * 跟随比例) + locked_profit_percent = current_pnl_percent * self._get_dynamic_ratio(order, current_price) + new_stop_loss = order.filled_price * (1 + locked_profit_percent / 100) + order.stop_loss = new_stop_loss + else: + # 做空:新止损价 = 开仓价 * (1 - 盈利 * 跟随比例) + locked_profit_percent = current_pnl_percent * self._get_dynamic_ratio(order, current_price) + new_stop_loss = order.filled_price * (1 - locked_profit_percent / 100) + order.stop_loss = new_stop_loss + + logger.info(f"移动止损首次触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {trailing_threshold:.2f}% | " + f"锁定利润 {locked_profit_percent:.2f}% | 止损移至 ${order.stop_loss:,.2f}") + + elif getattr(order, 'trailing_stop_triggered', 0) == 1: + # 已触发过移动止损,持续跟随 + base_profit = getattr(order, 'trailing_stop_base_profit', 0) + additional_profit = current_pnl_percent - base_profit + + if additional_profit > 0: + # 有额外盈利,移动止损以锁定更多利润 + if order.side == OrderSide.LONG: + # 做多:当前止损对应的盈利 + current_stop_profit = ((order.stop_loss - order.filled_price) / order.filled_price) * 100 + # 新的止损盈利 = 基准 + 额外盈利 * 跟随比例 + new_stop_profit = base_profit * self._get_dynamic_ratio(order, current_price) + new_stop_loss = order.filled_price * (1 + new_stop_profit / 100) + + # 只有当新止损高于当前止损时才更新(做多止损只能上移) + if new_stop_loss > order.stop_loss: + old_stop = order.stop_loss + order.stop_loss = new_stop_loss + needs_update = True + stop_moved = True + stop_move_type = "trailing_update" + logger.info(f"移动止损更新: {order.order_id} | {order.symbol} | " + f"盈利 {current_pnl_percent:.2f}% | 止损 ${old_stop:,.2f} -> ${new_stop_loss:,.2f}") + else: + # 做空:当前止损对应的盈利 + current_stop_profit = ((order.filled_price - order.stop_loss) / order.filled_price) * 100 + # 新的止损盈利 = 基准 + 额外盈利 * 跟随比例 + new_stop_profit = base_profit * self._get_dynamic_ratio(order, current_price) + new_stop_loss = order.filled_price * (1 - new_stop_profit / 100) + + # 只有当新止损低于当前止损时才更新(做空止损只能下移) + if new_stop_loss < order.stop_loss: + old_stop = order.stop_loss + order.stop_loss = new_stop_loss + needs_update = True + stop_moved = True + stop_move_type = "trailing_update" + logger.info(f"移动止损更新: {order.order_id} | {order.symbol} | " + f"盈利 {current_pnl_percent:.2f}% | 止损 ${old_stop:,.2f} -> ${new_stop_loss:,.2f}") + + # === 保本止损逻辑(仅在未触发移动止损时生效) === if self.breakeven_threshold > 0 and current_pnl_percent >= self.breakeven_threshold: - # 检查止损是否还没有移动到保本位(通过标记判断) - if getattr(order, 'breakeven_triggered', 0) != 1: + # 检查是否还没有触发过保本止损,也没有触发过移动止损 + breakeven_not_triggered = getattr(order, 'breakeven_triggered', 0) != 1 + trailing_not_triggered = getattr(order, 'trailing_stop_triggered', 0) != 1 + + if breakeven_not_triggered and trailing_not_triggered: if order.side == OrderSide.LONG: # 做多:止损应该低于开仓价,如果止损还在开仓价下方,则移动到开仓价 if order.stop_loss < order.filled_price: order.stop_loss = order.filled_price order.breakeven_triggered = 1 needs_update = True - breakeven_triggered = True + stop_moved = True + stop_move_type = "breakeven" + new_stop_loss = order.stop_loss logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}") else: # 做空:止损应该高于开仓价,如果止损还在开仓价上方,则移动到开仓价 @@ -578,7 +755,9 @@ class PaperTradingService: order.stop_loss = order.filled_price order.breakeven_triggered = 1 needs_update = True - breakeven_triggered = True + stop_moved = True + stop_move_type = "breakeven" + new_stop_loss = order.stop_loss logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}") # 如果有更新,持久化到数据库 @@ -590,7 +769,12 @@ class PaperTradingService: db_order.max_profit = order.max_profit db_order.max_drawdown = order.max_drawdown db_order.stop_loss = order.stop_loss - db_order.breakeven_triggered = order.breakeven_triggered + db_order.breakeven_triggered = getattr(order, 'breakeven_triggered', 0) + # 更新移动止损相关字段 + if hasattr(order, 'trailing_stop_triggered'): + db_order.trailing_stop_triggered = order.trailing_stop_triggered + if hasattr(order, 'trailing_stop_base_profit'): + db_order.trailing_stop_base_profit = getattr(order, 'trailing_stop_base_profit', 0) db.commit() except Exception as e: logger.error(f"更新订单极值失败: {e}") @@ -598,15 +782,16 @@ class PaperTradingService: finally: db.close() - # 如果触发了保本止损,返回通知信息 - if breakeven_triggered: + # 如果触发了止损移动,返回通知信息 + if stop_moved and new_stop_loss is not None: return { - 'event_type': 'breakeven_triggered', + 'event_type': 'stop_loss_moved', 'order_id': order.order_id, 'symbol': order.symbol, 'side': order.side.value, + 'move_type': stop_move_type, 'filled_price': order.filled_price, - 'new_stop_loss': order.stop_loss, + 'new_stop_loss': new_stop_loss, 'current_pnl_percent': current_pnl_percent } diff --git a/backend/migrate_db.py b/backend/migrate_db.py new file mode 100644 index 0000000..08d110f --- /dev/null +++ b/backend/migrate_db.py @@ -0,0 +1,149 @@ +""" +数据库迁移脚本 - 添加移动止损字段 +用于为已有的 paper_trading 表添加新字段 +""" +import sqlite3 +import os +from pathlib import Path + +def migrate_database(): + """执行数据库迁移""" + + # 查找数据库文件 + db_paths = [ + "stock_agent.db", + "backend/stock_agent.db", + "../stock_agent.db" + ] + + db_path = None + for path in db_paths: + if os.path.exists(path): + db_path = path + break + + if not db_path: + print("❌ 未找到数据库文件 stock_agent.db") + print("请确保在项目根目录或 backend 目录下运行此脚本") + return False + + print(f"📁 找到数据库文件: {db_path}") + + # 连接数据库 + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 检查表是否存在 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='paper_orders'") + if not cursor.fetchone(): + print("⚠️ paper_orders 表不存在,将在首次启动时自动创建") + return False + + # 检查字段是否已存在 + cursor.execute("PRAGMA table_info(paper_orders)") + columns = [column[1] for column in cursor.fetchall()] + + # 需要添加的新字段 + new_columns = { + 'trailing_stop_triggered': 'INTEGER DEFAULT 0', + 'trailing_stop_base_profit': 'REAL DEFAULT 0' + } + + columns_to_add = [] + for col_name, col_type in new_columns.items(): + if col_name not in columns: + columns_to_add.append((col_name, col_type)) + + if not columns_to_add: + print("✅ 数据库已是最新版本,无需迁移") + return True + + # 执行迁移 + print(f"📝 开始迁移,将添加 {len(columns_to_add)} 个新字段...") + + for col_name, col_type in columns_to_add: + try: + sql = f"ALTER TABLE paper_orders ADD COLUMN {col_name} {col_type}" + cursor.execute(sql) + print(f" ✅ 添加字段: {col_name}") + except sqlite3.OperationalError as e: + print(f" ⚠️ 添加字段 {col_name} 失败: {e}") + + # 提交更改 + conn.commit() + print("✅ 数据库迁移完成!") + return True + + except sqlite3.Error as e: + print(f"❌ 数据库错误: {e}") + return False + finally: + if conn: + conn.close() + + +def verify_migration(): + """验证迁移结果""" + db_paths = [ + "stock_agent.db", + "backend/stock_agent.db", + "../stock_agent.db" + ] + + db_path = None + for path in db_paths: + if os.path.exists(path): + db_path = path + break + + if not db_path: + return + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + cursor.execute("PRAGMA table_info(paper_orders)") + columns = {column[1]: column[2] for column in cursor.fetchall()} + + print("\n📋 paper_orders 表字段列表:") + print("-" * 60) + + required_fields = ['trailing_stop_triggered', 'trailing_stop_base_profit'] + all_present = True + + for field in required_fields: + if field in columns: + print(f" ✅ {field}: {columns[field]}") + else: + print(f" ❌ {field}: 缺失") + all_present = False + + if all_present: + print("\n✅ 所有必需字段都已存在!") + else: + print("\n⚠️ 部分字段缺失,请检查迁移结果") + + except sqlite3.Error as e: + print(f"❌ 验证失败: {e}") + finally: + if conn: + conn.close() + + +if __name__ == "__main__": + print("=" * 60) + print("🔄 Stock Agent 数据库迁移工具") + print("=" * 60) + print() + + success = migrate_database() + + if success: + verify_migration() + else: + print("\n💡 如果数据库文件不存在,启动服务时会自动创建") + + print() + print("=" * 60)