From 8dda6106af72101460522407d35f0a1271660d77 Mon Sep 17 00:00:00 2001 From: silencesdg Date: Tue, 12 May 2026 08:51:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=93=E4=B8=9A=E9=94=81=E4=BB=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20-=20Lock=20Walk=20+=20Trend=20Confirmation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 锁仓决策优先级: 组合盈利大于趋势确认大于对冲止盈大于继续锁仓 不再单纯靠信号解锁, 不会两头挨打 --- config.py | 3 + core/risk/hedge.py | 225 ++++++++++++++++++++++++------------------ core/risk/position.py | 8 +- 3 files changed, 136 insertions(+), 100 deletions(-) diff --git a/config.py b/config.py index baf5318..a00aa33 100644 --- a/config.py +++ b/config.py @@ -87,6 +87,9 @@ HEDGE_CONFIG = { # ── 对冲单止盈 ── "hedge_take_profit_pct": 0.005, # 对冲单自身盈利0.5%止盈 + # ── 锁仓管理 ── + "lock_net_profit_pct": 0.0, # 锁仓组合净盈利>0→双平离场 + # ── 风控限制 ── "max_hedges_per_day": 5, # 每日最多对冲5次 } diff --git a/core/risk/hedge.py b/core/risk/hedge.py index 2665803..711371b 100644 --- a/core/risk/hedge.py +++ b/core/risk/hedge.py @@ -1,14 +1,15 @@ """ -对冲管理器 — 信号对冲 + 回撤锁仓 +对冲管理器 — 信号对冲 + 专业锁仓管理 两种对冲模式: 1. 信号对冲:持仓方向与当前加权信号相反 → 开反向单保护 2. 回撤锁仓:持仓亏损超过阈值 → 锁仓防进一步亏损 -解锁条件: - - 信号回到中性/同向 → 平对冲单 - - 对冲单自身止盈 → 平对冲单 - - 原始仓位平仓 → 同时平对冲单 +锁仓解锁策略 (Lock & Walk + Trend Confirmation): + - 锁仓组合净盈利 > 0 → 双平离场(保本) + - 趋势确认向上(信号>1.0) → 平空留多 + - 趋势确认向下(信号<-1.0) → 平多留空(顺势反手) + - 趋势不明 → 继续锁仓(避免两头挨打) """ from logger import logger @@ -23,26 +24,25 @@ class HedgeManager: # ── 信号对冲 ── self.signal_hedge_enabled = cfg.get("signal_hedge_enabled", True) - # 加权信号绝对值超过此阈值才触发对冲(避免噪音) self.signal_hedge_threshold = cfg.get("signal_hedge_threshold", 2.0) - # 对冲比例:0.5=开一半手数,1.0=等量对冲 self.signal_hedge_ratio = cfg.get("signal_hedge_ratio", 0.5) - # 信号回到中性以下(绝对值<此值)时解锁 self.signal_unhedge_threshold = cfg.get("signal_unhedge_threshold", 1.0) # ── 回撤锁仓 ── self.drawdown_hedge_enabled = cfg.get("drawdown_hedge_enabled", True) - # 浮亏超过此比例触发锁仓 - self.drawdown_hedge_pct = cfg.get("drawdown_hedge_pct", -0.003) # -0.3% - # 锁仓比例 - self.drawdown_hedge_ratio = cfg.get("drawdown_hedge_ratio", 1.0) # 100% + self.drawdown_hedge_pct = cfg.get("drawdown_hedge_pct", -0.003) + self.drawdown_hedge_ratio = cfg.get("drawdown_hedge_ratio", 1.0) + + # ── 锁仓管理(新) ── + # 锁仓组合盈亏阈值:净盈超过此比例→双平离场 + self.lock_net_profit_pct = cfg.get("lock_net_profit_pct", 0.0) # 打平即可 + # 趋势确认信号阈值(方向确认用,复用 signal_unhedge_threshold) # ── 对冲单元管理 ── - # 每个对冲单元: {original_ticket, hedge_ticket, hedge_type, hedge_volume} self._hedge_units = [] # ── 对冲单止盈 ── - self.hedge_take_profit_pct = cfg.get("hedge_take_profit_pct", 0.005) # 0.5% + self.hedge_take_profit_pct = cfg.get("hedge_take_profit_pct", 0.005) # ── 每日最大对冲次数 ── self.max_hedges_per_day = cfg.get("max_hedges_per_day", 5) @@ -54,37 +54,37 @@ class HedgeManager: def evaluate(self, current_signal: float, current_price: dict) -> list: """ - 每轮主循环调用:评估所有持仓是否需要对冲/解锁。 - 返回需要执行的操作列表: [("hedge", position, reason), ("unhedge", unit, reason), ...] + 每轮主循环调用。 + 返回操作列表: action 类型见下 + ("hedge", position, reason) — 开新对冲 + ("unhedge_only", unit, reason) — 只平对冲单,保留原始 + ("close_original", unit, reason) — 平原始单,保留对冲(顺势反手) + ("close_both", unit, reason) — 双平离场 + ("hold", unit, reason) — 继续锁仓(仅日志用途,不执行) """ actions = [] - # ① 先检查是否需要解锁已有对冲 + # ① 锁仓决策 for unit in list(self._hedge_units): original = self._find_position(unit["original_ticket"]) if original is None: - # 原始仓位已平 → 解锁对冲 - actions.append(("unhedge", unit, "原始仓位已平仓")) + actions.append(("close_both", unit, "原始仓位已消失")) continue - if self._should_unhedge(unit, original, current_signal, current_price): - actions.append(("unhedge", unit, unit.get("unhedge_reason", "解锁条件满足"))) + action, reason = self._resolve_lock(unit, original, current_signal, current_price) + if action != "hold": + actions.append((action, unit, reason)) - # ② 再检查是否需要开新对冲 + # ② 开新对冲 for pos in self._pm.positions: ticket = pos["ticket"] - # 已有对冲的跳过 if any(u["original_ticket"] == ticket for u in self._hedge_units): continue hedge_reason = None - - # 回撤锁仓优先(更严重的情况) if self.drawdown_hedge_enabled: hedge_reason = self._check_drawdown_hedge(pos, current_price) - - # 信号对冲作为补充 if not hedge_reason and self.signal_hedge_enabled: hedge_reason = self._check_signal_hedge(pos, current_signal) @@ -94,19 +94,75 @@ class HedgeManager: return actions # ═══════════════════════════════════════════════════════════ - # 对冲执行 + # 锁仓决策核心(Lock & Walk + Trend Confirmation) + # ═══════════════════════════════════════════════════════════ + + def _resolve_lock(self, unit, original_pos, current_signal: float, current_price: dict): + """ + 专业锁仓决策: + 1. 锁仓组合整体盈利 → 双平离场 + 2. 趋势确认方向 → 跟趋势走,平亏损方留盈利方 + 3. 趋势不明 → 继续锁仓 + """ + price = current_price.get("last", 0) + if price <= 0: + return ("hold", "价格无效") + + # 计算双方盈亏 + original_pnl = self._pm._calculate_pnl_pct(original_pos, price) + hedge_price = unit.get("entry_price", 0) + if hedge_price <= 0: + return ("close_both", "对冲单价格异常") + + if unit["hedge_direction"] == "sell": + hedge_pnl = (hedge_price - price) / hedge_price + else: + hedge_pnl = (price - hedge_price) / hedge_price + + net_pnl = original_pnl + hedge_pnl + + # ① 锁仓组合净盈利 → 双平(绝不亏钱离场) + if net_pnl >= self.lock_net_profit_pct: + return ("close_both", + f"组合盈利(原={original_pnl:+.2%}+对={hedge_pnl:+.2%}={net_pnl:+.2%})") + + # ② 趋势确认 → 跟趋势 + is_long = original_pos["position_type"] == "long" + thresh = self.signal_unhedge_threshold + + if current_signal > thresh: + # 信号看多 + if is_long: + return ("unhedge_only", f"趋势看多(信号={current_signal:.2f})→平空留多") + else: + return ("close_original", f"趋势看多(信号={current_signal:.2f})→平空留多顺势") + + if current_signal < -thresh: + # 信号看空 + if is_long: + return ("close_original", f"趋势看空(信号={current_signal:.2f})→平多留空顺势") + else: + return ("unhedge_only", f"趋势看空(信号={current_signal:.2f})→平多留空") + + # ③ 对冲单止盈(即使趋势不明,对冲单赚够了也解锁) + if hedge_pnl >= self.hedge_take_profit_pct: + return ("unhedge_only", f"对冲单止盈({hedge_pnl:.2%}≥{self.hedge_take_profit_pct:.1%})") + + # ④ 趋势不明 → 继续锁 + return ("hold", f"趋势不明(信号={current_signal:.2f})→继续锁仓") + + # ═══════════════════════════════════════════════════════════ + # 执行方法 # ═══════════════════════════════════════════════════════════ def execute_hedge(self, position, reason: str, current_price: dict) -> bool: - """对指定持仓开反向对冲单""" + """开反向对冲单""" opposite = "sell" if position["position_type"] == "long" else "buy" - - # 确定对冲手数 hedge_type = "signal" if "信号" in reason else "drawdown" ratio = self.signal_hedge_ratio if hedge_type == "signal" else self.drawdown_hedge_ratio hedge_volume = position["quantity"] * ratio - # ★ 手数校验:不能低于最小手数 + # 手数校验 symbol_info = self._pm.data_provider.get_symbol_info(self._pm.symbol) if symbol_info: vol_min = symbol_info.get("volume_min", 0.01) if isinstance(symbol_info, dict) else getattr(symbol_info, "volume_min", 0.01) @@ -117,10 +173,7 @@ class HedgeManager: logger.info(f"🔒 准备对冲: {opposite} {hedge_volume:.2f}手 | 原因: {reason}") - # 调取开仓 - result = self._pm.data_provider.send_order( - self._pm.symbol, opposite, hedge_volume - ) + result = self._pm.data_provider.send_order(self._pm.symbol, opposite, hedge_volume) if result is None: logger.error(f"对冲开仓失败: {opposite} @ {hedge_volume}手") return False @@ -143,27 +196,53 @@ class HedgeManager: } self._hedge_units.append(unit) self._daily_hedge_count += 1 - logger.info( - f"🔒 对冲开仓: Ticket {position['ticket']} → 反向 {opposite} " - f"@{hedge_volume}手 (Ticket {order_id}) | 原因: {reason}" - ) + logger.info(f"🔒 对冲开仓: Ticket {position['ticket']} → {opposite} @{hedge_volume}手 (Ticket {order_id})") return True logger.error(f"对冲开仓失败: order_id={order_id}") return False - def execute_unhedge(self, unit: dict, reason: str) -> bool: - """平掉对冲单,解锁""" + def execute_unhedge_only(self, unit: dict, reason: str) -> bool: + """只平对冲单,保留原始仓位""" + return self._close_hedge_position(unit, reason) + + def execute_close_original(self, unit: dict, reason: str) -> bool: + """平原始仓位,保留对冲单(顺势反手)""" + success = self._pm.data_provider.close_position( + unit["original_ticket"], self._pm.symbol, unit["hedge_volume"] + ) + if success: + self._hedge_units.remove(unit) + logger.info(f"🔄 顺势反手: 平原始 Ticket {unit['original_ticket']},留对冲 Ticket {unit['hedge_ticket']} | {reason}") + return True + logger.error(f"反手平仓失败: Ticket {unit['original_ticket']}") + return False + + def execute_close_both(self, unit: dict, reason: str) -> bool: + """双平离场""" + ok1 = self._pm.data_provider.close_position( + unit["original_ticket"], self._pm.symbol, unit["hedge_volume"] + ) + ok2 = self._pm.data_provider.close_position( + unit["hedge_ticket"], self._pm.symbol, unit["hedge_volume"] + ) + if ok1 and ok2: + self._hedge_units.remove(unit) + logger.info(f"✅ 双平离场: Ticket {unit['original_ticket']}+{unit['hedge_ticket']} 已平 | {reason}") + return True + logger.error(f"双平失败: 原={ok1} 对={ok2}") + return False + + def _close_hedge_position(self, unit: dict, reason: str) -> bool: + """平对冲单(保留原始)""" success = self._pm.data_provider.close_position( unit["hedge_ticket"], self._pm.symbol, unit["hedge_volume"] ) if success: self._hedge_units.remove(unit) - logger.info( - f"🔓 对冲解锁: Ticket {unit['hedge_ticket']} 已平仓 | 原因: {reason}" - ) + logger.info(f"🔓 解锁(留原始): Ticket {unit['hedge_ticket']} 已平 | {reason}") return True - logger.error(f"解锁平仓失败: Ticket {unit['hedge_ticket']}") + logger.error(f"解锁失败: Ticket {unit['hedge_ticket']}") return False # ═══════════════════════════════════════════════════════════ @@ -171,18 +250,14 @@ class HedgeManager: # ═══════════════════════════════════════════════════════════ def _check_signal_hedge(self, position, current_signal: float) -> str | None: - """检查信号是否触发对冲""" is_long = position["position_type"] == "long" - # 持多 + 信号强烈看空 → 对冲 if is_long and current_signal < -self.signal_hedge_threshold: return f"信号对冲(持多, 加权={current_signal:.2f} < -{self.signal_hedge_threshold})" - # 持空 + 信号强烈看多 → 对冲 if not is_long and current_signal > self.signal_hedge_threshold: return f"信号对冲(持空, 加权={current_signal:.2f} > {self.signal_hedge_threshold})" return None def _check_drawdown_hedge(self, position, current_price: dict) -> str | None: - """检查浮亏是否触发锁仓""" price = current_price.get("last", 0) if price <= 0: return None @@ -191,54 +266,12 @@ class HedgeManager: return f"回撤锁仓(浮亏={pnl_pct:.2%} <= {self.drawdown_hedge_pct:.2%})" return None - def _should_unhedge(self, unit, original_pos, current_signal: float, current_price: dict) -> bool: - """判断是否应该解锁对冲""" - is_drawdown = unit.get("hedge_type") == "drawdown" - - # 条件1:回撤锁仓 → 检查浮亏是否恢复 - if is_drawdown: - price = current_price.get("last", 0) - if price > 0: - pnl_pct = self._pm._calculate_pnl_pct(original_pos, price) - if pnl_pct > self.drawdown_hedge_pct: # 浮亏回到阈值以上 - unit["unhedge_reason"] = f"回撤恢复(浮亏={pnl_pct:.2%} > {self.drawdown_hedge_pct:.2%})" - return True - - # 条件2:信号回到中性 (|signal| < unhedge_threshold) - if abs(current_signal) < self.signal_unhedge_threshold: - unit["unhedge_reason"] = f"信号中性({current_signal:.2f})" - return True - - # 条件3:信号与原始仓位同向且有足够力度 - is_long = original_pos["position_type"] == "long" - if is_long and current_signal > self.signal_unhedge_threshold: - unit["unhedge_reason"] = f"信号同向做多({current_signal:.2f} > {self.signal_unhedge_threshold})" - return True - if not is_long and current_signal < -self.signal_unhedge_threshold: - unit["unhedge_reason"] = f"信号同向做空({current_signal:.2f} < -{self.signal_unhedge_threshold})" - return True - - # 条件4:对冲单自身止盈 - price = current_price.get("last", 0) - if price > 0 and unit.get("entry_price", 0) > 0: - if unit["hedge_direction"] == "sell": - hedge_pnl = (unit["entry_price"] - price) / unit["entry_price"] - else: - hedge_pnl = (price - unit["entry_price"]) / unit["entry_price"] - if hedge_pnl >= self.hedge_take_profit_pct: - unit["unhedge_reason"] = f"对冲单止盈({hedge_pnl:.2%})" - return True - - return False - def _can_hedge(self) -> bool: - """检查是否允许开新对冲(次数限制)""" if self.max_hedges_per_day <= 0: return True return self._daily_hedge_count < self.max_hedges_per_day def _find_position(self, ticket): - """在持仓列表中查找指定 ticket 的仓位""" return next((p for p in self._pm.positions if p["ticket"] == ticket), None) # ═══════════════════════════════════════════════════════════ @@ -254,12 +287,8 @@ class HedgeManager: "active_hedges": len(self._hedge_units), "daily_hedge_count": self._daily_hedge_count, "units": [ - { - "original": u["original_ticket"], - "hedge": u["hedge_ticket"], - "type": u["hedge_type"], - "volume": u["hedge_volume"], - } + {"original": u["original_ticket"], "hedge": u["hedge_ticket"], + "type": u["hedge_type"], "volume": u["hedge_volume"]} for u in self._hedge_units ], } diff --git a/core/risk/position.py b/core/risk/position.py index d03b8c4..87b12ae 100644 --- a/core/risk/position.py +++ b/core/risk/position.py @@ -242,8 +242,12 @@ class PositionManager: for action, target, reason in hedge_actions: if action == "hedge": self.hedge_manager.execute_hedge(target, reason, current_price) - elif action == "unhedge": - self.hedge_manager.execute_unhedge(target, reason) + elif action == "unhedge_only": + self.hedge_manager.execute_unhedge_only(target, reason) + elif action == "close_original": + self.hedge_manager.execute_close_original(target, reason) + elif action == "close_both": + self.hedge_manager.execute_close_both(target, reason) # ── 盈亏计算 ──