"""趋势策略持仓止盈与分级补仓。""" from dataclasses import dataclass from libs.calc import calc_buy_volume, calculate_min_profit_rate from libs.grid_take_profit import GridState from sdk import OP_BUY, OP_SELL, PositionItem, Tick from libs.order import PlaceOrderRequest from libs.runtime import Runtime import logging as log LOSS_TIERS = [-50.0] @dataclass(slots=True) class TradeDecision: """一次止盈或补仓判断的统一结果。""" submitted: bool message: str = "" reserved_cash: float = 0.0 def manage_positions( runtime: Runtime, ticks: dict[str, Tick], positions: list[PositionItem], market_ok: bool, available: float, ) -> None: # 遍历处理每个持仓 for position in positions: try: available = max(0, available) code = position.stock_code tick = ticks.get(code) if code in runtime.account_cfg.excluded_codes: log.info( "[Position - ] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=已配置为排除股票", code, position.stock_name, ) continue if ( not code or position.open_price <= 0 or position.volume <= 0 or tick is None or tick.last_price <= 0 ): log.warning( "[Position - ] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=持仓或行情数据无效", code or "未知", position.stock_name, ) continue pnl_rate = round( (tick.last_price - position.open_price) / position.open_price * 100, 2, ) minimum_profit = calculate_min_profit_rate(position.open_price, 1) profit_decision = handle_profit( runtime=runtime, position=position, tick=tick, pnl_rate=pnl_rate, minimum_profit=minimum_profit, ) profit_action = profit_decision.message or "未触发" loss_add_action = "未启用" if runtime.account_cfg.enable_loss_add_position and market_ok: loss_decision = handle_loss( runtime=runtime, position=position, tick=tick, pnl_rate=pnl_rate, available=available, ) available = available - loss_decision.reserved_cash loss_add_action = loss_decision.message or "未触发" elif runtime.account_cfg.enable_loss_add_position: loss_add_action = "大盘信号不允许" strTag = "-" if pnl_rate >= minimum_profit: strTag = "↑" elif pnl_rate< LOSS_TIERS[0]: strTag = "↓" if strTag != "-": log.info( "[Position %s ] %s %s,盈亏=%.2f%%,止盈=%s,补仓=%s", strTag, code, position.stock_name, pnl_rate, profit_action, loss_add_action, ) except Exception: log.exception( "[Position] 持仓处理异常,代码=%s,继续处理后续持仓", position.stock_code, ) def handle_profit( runtime: Runtime, position: PositionItem, tick: Tick, pnl_rate: float, minimum_profit: float, ) -> TradeDecision: """基于跨轮保存的最高盈利网格判断是否提交止盈。""" if pnl_rate < minimum_profit: return TradeDecision(False) key = _position_key(runtime, position.stock_code) observation = runtime.profit_tracker.observe(key, pnl_rate) if observation.state == GridState.ARMED: return TradeDecision( False, f"首次, PNL:{pnl_rate:.2f}%,网格={observation.current_grid}", ) if observation.state == GridState.RAISED: return TradeDecision( False, f"突破, PNL:{pnl_rate:.2f}%,网格={observation.current_grid}", ) if observation.state == GridState.STEADY: return TradeDecision(False,f"持平, PNL:{pnl_rate:.2f}%,网格={observation.current_grid}",) if runtime.orders.busy(position.stock_code, "SELL"): return TradeDecision(False, "卖出委托处理中") volume = position.can_use_volume - position.can_use_volume % 100 if volume <= 0: return TradeDecision(False, "无可用整手持仓") order_id = runtime.orders.new_order_id("SELL") request = PlaceOrderRequest( op=OP_SELL, code=position.stock_code, volume=volume, order_id=order_id, strategy_name=runtime.account_cfg.strategy, ) if not runtime.orders.place(runtime.client, request): return TradeDecision(False, "止盈委托失败") return TradeDecision(True, f"[止盈卖出] {volume} 股,订单={order_id}") def handle_loss( runtime: Runtime, position: PositionItem, tick: Tick, pnl_rate: float, available: float, ) -> TradeDecision: """按亏损档位、反弹确认和本轮剩余预算提交补仓。""" add_num = get_add_num( hands=int(position.volume / 100), market_value=position.market_value ) if add_num >= len(LOSS_TIERS) or add_num < 0: return TradeDecision(False, f"补仓次数无效:{add_num}") if pnl_rate > LOSS_TIERS[add_num]: return TradeDecision(False) if tick.last_price > 200 or position.market_value >= 20_000: return TradeDecision(False, "价格或仓位市值超过补仓限制") if not runtime.add_watch.triggered("补仓", position.stock_code, tick.last_price): return TradeDecision(False, "等待价格反弹确认") if runtime.orders.busy(position.stock_code, "BUY"): return TradeDecision(False, "买入委托处理中") volume = calc_buy_volume(tick.last_price, runtime.account_cfg.buy_value) amount = tick.last_price * volume if volume <= 0 or amount > available: return TradeDecision(False, "本轮可用资金不足") order_id = runtime.orders.new_order_id("BUY") request = PlaceOrderRequest( op=OP_BUY, code=position.stock_code, volume=volume, order_id=order_id, strategy_name=runtime.account_cfg.strategy, kind="add", ) if not runtime.orders.place(runtime.client, request): return TradeDecision(False, "补仓订单委托失败") runtime.add_watch.forget(position.stock_code) return TradeDecision(True, f"[补仓买入] {volume} 股,订单={order_id}", amount) def _position_key(runtime: Runtime, code: str) -> str: return f"{runtime.account_cfg.account_id}:{code}" def get_add_num(hands: int, market_value: float) -> int: if market_value > 10000: return -1 if hands < 2: return 0 return -1