From fd661d11d423481765405541ad6a15fab912d8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Mon, 19 Jan 2026 15:04:51 +0800 Subject: [PATCH] a --- backend/api/routes/config.py | 89 ++++++- trading_system/config.py | 22 ++ trading_system/position_manager.py | 415 ++++++++++++++++++++--------- trading_system/strategy.py | 3 + 4 files changed, 388 insertions(+), 141 deletions(-) diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index bb3b9f4..61792a8 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -18,6 +18,70 @@ from database.models import TradingConfig logger = logging.getLogger(__name__) router = APIRouter() +# 智能入场(方案C)配置:为了“配置页可见”,即使数据库尚未创建,也在 GET /api/config 返回默认项 +SMART_ENTRY_CONFIG_DEFAULTS = { + "SMART_ENTRY_ENABLED": { + "value": True, + "type": "boolean", + "category": "strategy", + "description": "智能入场开关(方案C):趋势时减少错过,震荡时避免追价打损。", + }, + "SMART_ENTRY_STRONG_SIGNAL": { + "value": 8, + "type": "number", + "category": "strategy", + "description": "强信号阈值(0-10)。≥该值且4H趋势明确时,允许更积极的入场(可控市价兜底)。", + }, + "ENTRY_SYMBOL_COOLDOWN_SEC": { + "value": 120, + "type": "number", + "category": "strategy", + "description": "同一交易对入场冷却时间(秒)。避免短时间内反复挂单/重入导致高频噪音单。", + }, + "ENTRY_TIMEOUT_SEC": { + "value": 180, + "type": "number", + "category": "strategy", + "description": "智能入场总预算时间(秒)。超过预算仍未成交将根据规则取消/兜底。", + }, + "ENTRY_STEP_WAIT_SEC": { + "value": 15, + "type": "number", + "category": "strategy", + "description": "每次追价/调整前等待成交时间(秒)。", + }, + "ENTRY_CHASE_MAX_STEPS": { + "value": 4, + "type": "number", + "category": "strategy", + "description": "最大追价步数(逐步减小限价回调幅度,靠近当前价)。", + }, + "ENTRY_MARKET_FALLBACK_AFTER_SEC": { + "value": 45, + "type": "number", + "category": "strategy", + "description": "趋势强时:超过该时间仍未成交,会在偏离不超过上限时转市价兜底(减少错过)。", + }, + "ENTRY_CONFIRM_TIMEOUT_SEC": { + "value": 30, + "type": "number", + "category": "strategy", + "description": "下单后确认成交等待时间(秒)。", + }, + "ENTRY_MAX_DRIFT_PCT_TRENDING": { + "value": 0.006, # 0.6% + "type": "number", + "category": "strategy", + "description": "趋势强时最大追价偏离(%)。例如 0.6 表示 0.6%。越小越保守。", + }, + "ENTRY_MAX_DRIFT_PCT_RANGING": { + "value": 0.003, # 0.3% + "type": "number", + "category": "strategy", + "description": "震荡/弱趋势最大追价偏离(%)。例如 0.3 表示 0.3%。越小越保守。", + }, +} + @router.get("") @router.get("/") @@ -36,6 +100,11 @@ async def get_all_configs(): 'category': config['category'], 'description': config['description'] } + + # 合并“默认但未入库”的配置项(用于新功能上线时直接在配置页可见) + for k, meta in SMART_ENTRY_CONFIG_DEFAULTS.items(): + if k not in result: + result[k] = meta return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -357,12 +426,16 @@ async def update_config(key: str, item: ConfigUpdate): try: # 获取现有配置以确定类型和分类 existing = TradingConfig.get(key) - if not existing: - raise HTTPException(status_code=404, detail="Config not found") - - config_type = item.type or existing['config_type'] - category = item.category or existing['category'] - description = item.description or existing['description'] + if existing: + config_type = item.type or existing['config_type'] + category = item.category or existing['category'] + description = item.description or existing['description'] + else: + # 允许创建新配置(用于新功能首次上线,DB 里还没有 key 的情况) + meta = SMART_ENTRY_CONFIG_DEFAULTS.get(key) + config_type = item.type or (meta.get("type") if meta else "string") + category = item.category or (meta.get("category") if meta else "strategy") + description = item.description or (meta.get("description") if meta else f"{key}配置") # 验证配置值 if config_type == 'number': @@ -429,8 +502,8 @@ async def update_configs_batch(configs: list[ConfigItem]): errors.append(f"{item.key}: Invalid number value") continue - # 特殊验证:百分比配置 - if 'PERCENT' in item.key and item.type == 'number': + # 特殊验证:百分比配置(兼容 PERCENT / PCT) + if ('PERCENT' in item.key or 'PCT' in item.key) and item.type == 'number': if not (0 <= float(item.value) <= 1): errors.append(f"{item.key}: Must be between 0 and 1") continue diff --git a/trading_system/config.py b/trading_system/config.py index fece9a1..f5aa4fd 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -207,6 +207,18 @@ def _get_trading_config(): 'TRAILING_STOP_ACTIVATION': 0.10, # 移动止损激活提高到10%(盈利10%后激活,给趋势更多空间) 'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%(保护5%利润,更合理) 'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔(秒),缩短到1分钟,确保状态及时同步 + + # ===== 智能入场(方案C:趋势少错过,震荡不追价)===== + 'SMART_ENTRY_ENABLED': True, + 'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值:≥8 更倾向趋势模式(允许市价兜底) + 'ENTRY_SYMBOL_COOLDOWN_SEC': 120, # 同一symbol两次入场尝试的冷却时间(避免反复挂单/重入) + 'ENTRY_TIMEOUT_SEC': 180, # 智能入场总预算(秒)(限价/追价逻辑内部使用) + 'ENTRY_STEP_WAIT_SEC': 15, # 每步等待成交时间(秒) + 'ENTRY_CHASE_MAX_STEPS': 4, # 最多追价步数(逐步减少 offset) + 'ENTRY_MARKET_FALLBACK_AFTER_SEC': 45, # 趋势强时:超过该秒数仍未成交 -> 评估是否市价兜底 + 'ENTRY_CONFIRM_TIMEOUT_SEC': 30, # 下单后最终确认成交的等待时间(秒) + 'ENTRY_MAX_DRIFT_PCT_TRENDING': 0.6, # 趋势强时允许的最大追价偏离(相对初始限价) + 'ENTRY_MAX_DRIFT_PCT_RANGING': 0.3, # 震荡/弱趋势时允许的最大追价偏离 } # 币安API配置(优先从数据库,回退到环境变量和默认值) @@ -227,6 +239,16 @@ defaults = { 'CONFIRM_INTERVAL': '4h', 'ENTRY_INTERVAL': '15m', 'LIMIT_ORDER_OFFSET_PCT': 0.5, # 限价单偏移百分比(默认0.5%) + # 智能入场默认值(即使DB里没配置,也能用) + 'SMART_ENTRY_ENABLED': True, + 'ENTRY_SYMBOL_COOLDOWN_SEC': 120, + 'ENTRY_TIMEOUT_SEC': 180, + 'ENTRY_STEP_WAIT_SEC': 15, + 'ENTRY_CHASE_MAX_STEPS': 4, + 'ENTRY_MARKET_FALLBACK_AFTER_SEC': 45, + 'ENTRY_CONFIRM_TIMEOUT_SEC': 30, + 'ENTRY_MAX_DRIFT_PCT_TRENDING': 0.6, + 'ENTRY_MAX_DRIFT_PCT_RANGING': 0.3, } for key, value in defaults.items(): if key not in TRADING_CONFIG: diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index d9257af..39b784b 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -5,6 +5,7 @@ import asyncio import logging import json import aiohttp +import time from typing import Dict, List, Optional from datetime import datetime try: @@ -69,6 +70,80 @@ class PositionManager: self.active_positions: Dict[str, Dict] = {} self._monitor_tasks: Dict[str, asyncio.Task] = {} # WebSocket监控任务字典 self._monitoring_enabled = True # 是否启用实时监控 + self._pending_entry_orders: Dict[str, Dict] = {} # 未成交的入场订单(避免重复挂单) + self._last_entry_attempt_ms: Dict[str, int] = {} # 每个symbol的最近一次尝试(冷却/去抖) + + @staticmethod + def _pct_like_to_ratio(v: float) -> float: + """ + 将“看起来像百分比”的值转换为比例(0~1)。 + + 兼容两种来源: + - 前端/后端按“比例”存储:0.006 表示 0.6% + - 历史/默认值按“百分比数值”存储:0.6 表示 0.6% + + 经验规则: + - v > 1: 认为是 60/100 这种百分数,除以100 + - 0.05 < v <= 1: 也更可能是“0.6% 这种写法”,除以100 + - v <= 0.05: 更可能已经是比例(<=5%) + """ + try: + x = float(v or 0.0) + except Exception: + x = 0.0 + if x <= 0: + return 0.0 + if x > 1.0: + return x / 100.0 + if x > 0.05: + return x / 100.0 + return x + + def _calc_limit_entry_price(self, current_price: float, side: str, offset_ratio: float) -> float: + """根据当前价与偏移比例,计算限价入场价(BUY: 下方回调;SELL: 上方回调)""" + try: + cp = float(current_price) + except Exception: + cp = 0.0 + try: + off = float(offset_ratio or 0.0) + except Exception: + off = 0.0 + if cp <= 0: + return 0.0 + if (side or "").upper() == "BUY": + return cp * (1 - off) + return cp * (1 + off) + + async def _wait_for_order_filled( + self, + symbol: str, + order_id: int, + timeout_sec: int = 30, + poll_sec: float = 1.0, + ) -> Dict: + """ + 等待订单成交(FILLED),返回: + { ok, status, avg_price, executed_qty, raw } + """ + deadline = time.time() + max(1, int(timeout_sec or 1)) + last_status = None + while time.time() < deadline: + try: + info = await self.client.client.futures_get_order(symbol=symbol, orderId=order_id) + status = info.get("status") + last_status = status + if status == "FILLED": + avg_price = float(info.get("avgPrice", 0) or 0) or float(info.get("price", 0) or 0) + executed_qty = float(info.get("executedQty", 0) or 0) + return {"ok": True, "status": status, "avg_price": avg_price, "executed_qty": executed_qty, "raw": info} + if status in ("CANCELED", "REJECTED", "EXPIRED"): + return {"ok": False, "status": status, "avg_price": 0, "executed_qty": float(info.get("executedQty", 0) or 0), "raw": info} + except Exception: + # 忽略单次失败,继续轮询 + pass + await asyncio.sleep(max(0.2, float(poll_sec or 1.0))) + return {"ok": False, "status": last_status or "TIMEOUT", "avg_price": 0, "executed_qty": 0, "raw": None} async def open_position( self, @@ -77,6 +152,9 @@ class PositionManager: leverage: int = 10, trade_direction: Optional[str] = None, entry_reason: str = '', + signal_strength: Optional[int] = None, + market_regime: Optional[str] = None, + trend_4h: Optional[str] = None, atr: Optional[float] = None, klines: Optional[List] = None, bollinger: Optional[Dict] = None @@ -93,6 +171,22 @@ class PositionManager: 订单信息,失败返回None """ try: + # 0) 防止同一 symbol 重复挂入场单/快速重复尝试(去抖 + 冷却) + now_ms = int(time.time() * 1000) + cooldown_sec = int(config.TRADING_CONFIG.get("ENTRY_SYMBOL_COOLDOWN_SEC", 120) or 0) + last_ms = self._last_entry_attempt_ms.get(symbol) + if last_ms and cooldown_sec > 0 and now_ms - last_ms < cooldown_sec * 1000: + logger.info(f"{symbol} [入场] 冷却中({cooldown_sec}s),跳过本次自动开仓") + return None + + pending = self._pending_entry_orders.get(symbol) + if pending and pending.get("order_id"): + logger.info(f"{symbol} [入场] 已有未完成入场订单(orderId={pending.get('order_id')}),跳过重复开仓") + return None + + # 标记本次尝试(无论最终是否成交,都避免短时间内反复开仓/挂单) + self._last_entry_attempt_ms[symbol] = now_ms + # 判断是否应该交易 if not await self.risk_manager.should_trade(symbol, change_percent): return None @@ -166,149 +260,170 @@ class PositionManager: except Exception as e: logger.debug(f"从Redis重新加载配置失败: {e}") - # 使用基于保证金的止损止盈(结合技术分析) - # 计算保证金和仓位价值 - position_value = entry_price * quantity - margin = position_value / leverage if leverage > 0 else position_value - - # 获取止损止盈百分比(相对于保证金) - stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03) - take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.05) - - # 计算基于保证金的止损止盈 - stop_loss_price = self.risk_manager.get_stop_loss_price( - entry_price, side, quantity, leverage, - stop_loss_pct=stop_loss_pct_margin, - klines=klines, - bollinger=bollinger, - atr=atr - ) - - # 计算止损距离(用于盈亏比计算止盈) - stop_distance_for_tp = None - if side == 'BUY': - stop_distance_for_tp = entry_price - stop_loss_price - else: # SELL - stop_distance_for_tp = stop_loss_price - entry_price - - # 如果使用ATR策略,优先使用ATR计算的止损距离 - if atr is not None and atr > 0: - atr_percent = atr / entry_price if entry_price > 0 else None - if atr_percent: - atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8) - atr_stop_distance = entry_price * atr_percent * atr_multiplier - # 使用ATR计算的止损距离(更准确) - stop_distance_for_tp = atr_stop_distance - - # 计算止盈(基于保证金,支持ATR动态止盈) - # 优先使用配置的止盈百分比,如果没有配置则使用止损的3倍(盈亏比3:1) - take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.30) - # 如果配置中没有设置止盈,则使用止损的3倍作为默认(盈亏比3:1) - if take_profit_pct_margin is None or take_profit_pct_margin == 0: - take_profit_pct_margin = stop_loss_pct_margin * 3.0 - take_profit_price = self.risk_manager.get_take_profit_price( - entry_price, side, quantity, leverage, - take_profit_pct=take_profit_pct_margin, - atr=atr, # 传递ATR用于动态止盈 - stop_distance=stop_distance_for_tp # 传递止损距离用于盈亏比计算 - ) - - # 下单 - order = await self.client.place_order( - symbol=symbol, - side=side, - quantity=quantity, - order_type='MARKET' - ) - + # ===== 智能入场(方案C:趋势强更少错过,震荡更保守)===== + smart_entry_enabled = bool(config.TRADING_CONFIG.get("SMART_ENTRY_ENABLED", True)) + # LIMIT_ORDER_OFFSET_PCT:兼容“比例/百分比”两种存储方式 + limit_offset_ratio = self._pct_like_to_ratio(float(config.TRADING_CONFIG.get("LIMIT_ORDER_OFFSET_PCT", 0.5) or 0.0)) + + # 规则:趋势(trending) 或 4H共振明显 + 强信号 -> 允许“超时后市价兜底(有追价上限)” + mr = (market_regime or "").strip().lower() if market_regime else "" + t4 = (trend_4h or "").strip().lower() if trend_4h else "" + ss = int(signal_strength) if signal_strength is not None else 0 + + allow_market_fallback = False + if mr == "trending": + allow_market_fallback = True + if ss >= int(config.TRADING_CONFIG.get("SMART_ENTRY_STRONG_SIGNAL", 8) or 8) and t4 in ("up", "down"): + allow_market_fallback = True + + # 追价上限:超过就不追,宁愿错过(避免回到“无脑追价高频打损”) + drift_ratio_trending = self._pct_like_to_ratio(float(config.TRADING_CONFIG.get("ENTRY_MAX_DRIFT_PCT_TRENDING", 0.6) or 0.6)) + drift_ratio_ranging = self._pct_like_to_ratio(float(config.TRADING_CONFIG.get("ENTRY_MAX_DRIFT_PCT_RANGING", 0.3) or 0.3)) + max_drift_ratio = drift_ratio_trending if allow_market_fallback else drift_ratio_ranging + + # 总等待/追价参数 + timeout_sec = int(config.TRADING_CONFIG.get("ENTRY_TIMEOUT_SEC", 180) or 180) + step_wait_sec = int(config.TRADING_CONFIG.get("ENTRY_STEP_WAIT_SEC", 15) or 15) + chase_steps = int(config.TRADING_CONFIG.get("ENTRY_CHASE_MAX_STEPS", 4) or 4) + market_fallback_after_sec = int(config.TRADING_CONFIG.get("ENTRY_MARKET_FALLBACK_AFTER_SEC", 45) or 45) + + # 初始限价(回调入场) + current_px = float(entry_price) + initial_limit = self._calc_limit_entry_price(current_px, side, limit_offset_ratio) + if initial_limit <= 0: + return None + + order = None + entry_order_id = None + order_status = None + actual_entry_price = None + filled_quantity = 0.0 + entry_mode_used = "market" if not smart_entry_enabled else ("limit+fallback" if allow_market_fallback else "limit-chase") + + if not smart_entry_enabled: + order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="MARKET") + else: + # 1) 先挂限价单 + logger.info( + f"{symbol} [智能入场] 模式={entry_mode_used} | side={side} | " + f"marketRegime={market_regime} trend_4h={trend_4h} strength={ss}/10 | " + f"初始限价={initial_limit:.6f} (offset={limit_offset_ratio*100:.2f}%) | " + f"追价上限={max_drift_ratio*100:.2f}%" + ) + + order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="LIMIT", price=initial_limit) + if not order: + return None + entry_order_id = order.get("orderId") + if entry_order_id: + self._pending_entry_orders[symbol] = {"order_id": entry_order_id, "created_at_ms": int(time.time() * 1000)} + + start_ts = time.time() + # 2) 分步等待 + 追价(逐步减少 offset),并在趋势强时允许市价兜底(有追价上限) + for step in range(max(1, chase_steps)): + # 先等待一段时间看是否成交 + wait_res = await self._wait_for_order_filled(symbol, int(entry_order_id), timeout_sec=step_wait_sec, poll_sec=1.0) + order_status = wait_res.get("status") + if wait_res.get("ok"): + actual_entry_price = float(wait_res.get("avg_price") or 0) + filled_quantity = float(wait_res.get("executed_qty") or 0) + break + + # 未成交:如果超时太久且允许市价兜底,检查追价上限后转市价 + elapsed = time.time() - start_ts + ticker2 = await self.client.get_ticker_24h(symbol) + cur2 = float(ticker2.get("price")) if ticker2 else current_px + drift_ratio = 0.0 + try: + base = float(initial_limit) if float(initial_limit) > 0 else cur2 + drift_ratio = abs((cur2 - base) / base) + except Exception: + drift_ratio = 0.0 + + if allow_market_fallback and elapsed >= market_fallback_after_sec: + if drift_ratio <= max_drift_ratio: + try: + await self.client.cancel_order(symbol, int(entry_order_id)) + except Exception: + pass + self._pending_entry_orders.pop(symbol, None) + logger.info(f"{symbol} [智能入场] 限价超时,且偏离{drift_ratio*100:.2f}%≤{max_drift_ratio*100:.2f}%,转市价兜底") + order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="MARKET") + break + else: + logger.info(f"{symbol} [智能入场] 限价超时,但偏离{drift_ratio*100:.2f}%>{max_drift_ratio*100:.2f}%,取消并放弃本次交易") + try: + await self.client.cancel_order(symbol, int(entry_order_id)) + except Exception: + pass + self._pending_entry_orders.pop(symbol, None) + return None + + # 震荡/不允许市价兜底:尝试追价(减小 offset -> 更靠近当前价),但不突破追价上限 + try: + await self.client.cancel_order(symbol, int(entry_order_id)) + except Exception: + pass + + # offset 逐步减少到 0(越追越接近当前价) + step_ratio = (step + 1) / max(1, chase_steps) + cur_offset_ratio = max(0.0, limit_offset_ratio * (1.0 - step_ratio)) + desired = self._calc_limit_entry_price(cur2, side, cur_offset_ratio) + if side == "BUY": + cap = initial_limit * (1 + max_drift_ratio) + desired = min(desired, cap) + else: + cap = initial_limit * (1 - max_drift_ratio) + desired = max(desired, cap) + + if desired <= 0: + self._pending_entry_orders.pop(symbol, None) + return None + + logger.info( + f"{symbol} [智能入场] 追价 step={step+1}/{chase_steps} | 当前价={cur2:.6f} | " + f"offset={cur_offset_ratio*100:.3f}% -> 限价={desired:.6f} | 偏离={drift_ratio*100:.2f}%" + ) + order = await self.client.place_order(symbol=symbol, side=side, quantity=quantity, order_type="LIMIT", price=desired) + if not order: + self._pending_entry_orders.pop(symbol, None) + return None + entry_order_id = order.get("orderId") + if entry_order_id: + self._pending_entry_orders[symbol] = {"order_id": entry_order_id, "created_at_ms": int(time.time() * 1000)} + + # 如果是市价兜底或最终限价成交,这里统一继续后续流程(下面会再查实际成交) + + # ===== 统一处理:确认订单成交并获取实际成交价/数量 ===== if order: - # 获取开仓订单号 - entry_order_id = order.get('orderId') + if not entry_order_id: + entry_order_id = order.get("orderId") if entry_order_id: logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}") - + # 等待订单成交,检查订单状态并获取实际成交价格 # 只有在订单真正成交(FILLED)后才保存到数据库 - actual_entry_price = None - order_status = None - filled_quantity = 0 - max_retries = 5 # 最多重试5次,每次等待1秒 - retry_count = 0 - - while retry_count < max_retries: - try: - # 等待一小段时间让订单成交 - await asyncio.sleep(1) - - # 从币安获取订单详情,检查订单状态 - try: - order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id) - if order_info: - order_status = order_info.get('status') - logger.info(f"{symbol} [开仓] 订单状态: {order_status} (重试 {retry_count + 1}/{max_retries})") - - # 检查订单是否已成交 - if order_status == 'FILLED': - # 订单已完全成交,获取实际成交价格和数量 - actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) - filled_quantity = float(order_info.get('executedQty', 0)) - - if actual_entry_price > 0 and filled_quantity > 0: - logger.info(f"{symbol} [开仓] ✓ 订单已成交,成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}") - break - elif order_info.get('fills'): - # 从成交记录计算加权平均成交价格和总成交数量 - total_qty = 0 - total_value = 0 - for fill in order_info.get('fills', []): - qty = float(fill.get('qty', 0)) - price = float(fill.get('price', 0)) - total_qty += qty - total_value += qty * price - if total_qty > 0: - actual_entry_price = total_value / total_qty - filled_quantity = total_qty - logger.info(f"{symbol} [开仓] ✓ 订单已成交,从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}") - break - elif order_status == 'PARTIALLY_FILLED': - # 部分成交,继续等待 - filled_quantity = float(order_info.get('executedQty', 0)) - logger.info(f"{symbol} [开仓] ⏳ 订单部分成交 ({filled_quantity:.4f}/{quantity:.4f}),继续等待...") - retry_count += 1 - continue - elif order_status in ['NEW', 'PENDING_NEW']: - # 订单已提交但未成交,继续等待 - logger.info(f"{symbol} [开仓] ⏳ 订单已提交但未成交,继续等待...") - retry_count += 1 - continue - elif order_status in ['CANCELED', 'REJECTED', 'EXPIRED']: - # 订单被取消、拒绝或过期 - logger.error(f"{symbol} [开仓] ❌ 订单状态异常: {order_status},订单未成交") - return None - else: - logger.warning(f"{symbol} [开仓] ⚠️ 未知订单状态: {order_status},继续等待...") - retry_count += 1 - continue - except Exception as order_error: - logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},重试中...") - retry_count += 1 - continue - except Exception as price_error: - logger.warning(f"{symbol} [开仓] 检查订单状态时出错: {price_error},重试中...") - retry_count += 1 - continue - - # 检查订单是否最终成交 - if order_status != 'FILLED': - logger.error(f"{symbol} [开仓] ❌ 订单未成交,状态: {order_status},不保存到数据库") - return None - + if entry_order_id: + # 智能入场的限价订单可能需要更长等待,这里给一个总等待兜底(默认 30s) + confirm_timeout = int(config.TRADING_CONFIG.get("ENTRY_CONFIRM_TIMEOUT_SEC", 30) or 30) + res = await self._wait_for_order_filled(symbol, int(entry_order_id), timeout_sec=confirm_timeout, poll_sec=1.0) + order_status = res.get("status") + if res.get("ok"): + actual_entry_price = float(res.get("avg_price") or 0) + filled_quantity = float(res.get("executed_qty") or 0) + else: + logger.error(f"{symbol} [开仓] ❌ 订单未成交,状态: {order_status},不保存到数据库") + self._pending_entry_orders.pop(symbol, None) + return None + if not actual_entry_price or actual_entry_price <= 0: logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库") + self._pending_entry_orders.pop(symbol, None) return None - + if filled_quantity <= 0: logger.error(f"{symbol} [开仓] ❌ 成交数量为0,不保存到数据库") + self._pending_entry_orders.pop(symbol, None) return None # 使用实际成交价格和数量 @@ -317,6 +432,40 @@ class PositionManager: quantity = filled_quantity # 使用实际成交数量 logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}") + # 成交后清理 pending + self._pending_entry_orders.pop(symbol, None) + + # ===== 成交后基于“实际成交价/数量”重新计算止损止盈(修复限价/滑点导致的偏差)===== + stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03) + stop_loss_price = self.risk_manager.get_stop_loss_price( + entry_price, side, quantity, leverage, + stop_loss_pct=stop_loss_pct_margin, + klines=klines, + bollinger=bollinger, + atr=atr + ) + + stop_distance_for_tp = None + if side == 'BUY': + stop_distance_for_tp = entry_price - stop_loss_price + else: + stop_distance_for_tp = stop_loss_price - entry_price + + if atr is not None and atr > 0 and entry_price > 0: + atr_percent = atr / entry_price + atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8) + stop_distance_for_tp = entry_price * atr_percent * atr_multiplier + + take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.30) + if take_profit_pct_margin is None or take_profit_pct_margin == 0: + take_profit_pct_margin = float(stop_loss_pct_margin or 0) * 3.0 + take_profit_price = self.risk_manager.get_take_profit_price( + entry_price, side, quantity, leverage, + take_profit_pct=take_profit_pct_margin, + atr=atr, + stop_distance=stop_distance_for_tp + ) + # 分步止盈(基于“实际成交价 + 已计算的止损/止盈”) if side == 'BUY': take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1 diff --git a/trading_system/strategy.py b/trading_system/strategy.py index 766646e..0117dd2 100644 --- a/trading_system/strategy.py +++ b/trading_system/strategy.py @@ -180,6 +180,9 @@ class TradingStrategy: leverage=dynamic_leverage, trade_direction=trade_direction, entry_reason=entry_reason, + signal_strength=signal_strength, + market_regime=market_regime, + trend_4h=trade_signal.get('trend_4h'), atr=symbol_info.get('atr'), klines=symbol_info.get('klines'), # 传递K线数据用于动态止损 bollinger=symbol_info.get('bollinger') # 传递布林带数据用于动态止损