"""趋势策略持仓止盈与分级补仓。""" 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.state import State from libs.order import PlaceOrderRequest from libs.runtime import Runtime import logging as log LOSS_TIERS = -30.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, state:State, ) -> 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 posState = state.get_by_code(position.stock_code) if not posState: continue volume = position.can_use_volume cost_price = position.open_price if posState.get('added_qty',0) >=100: volume = posState.get('added_qty',0) cost_price = posState.get('added_price',0) pnl_rate = round( (tick.last_price - cost_price) / cost_price * 100, 2, ) minimum_profit = calculate_min_profit_rate(cost_price, 1) profit_decision = handle_profit( runtime=runtime, stock_code=position.stock_code, volume=volume, 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, stock_code: str, volume:int, tick: Tick, pnl_rate: float, minimum_profit: float, ) -> TradeDecision: """基于跨轮保存的最高盈利网格判断是否提交止盈。""" if pnl_rate < minimum_profit: return TradeDecision(False) key = _position_key(runtime, 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(stock_code, "SELL"): return TradeDecision(False, "卖出委托处理中") volume = volume % 100 if volume <= 0: return TradeDecision(False, "无可用整手持仓") order_id = runtime.orders.new_order_id("zt","SELL") request = PlaceOrderRequest( op=OP_SELL, code=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, stock_code: str, volume:int, tick: Tick, pnl_rate: float, available: float, ) -> TradeDecision: """按亏损档位、反弹确认和本轮剩余预算提交补仓。""" if pnl_rate > LOSS_TIERS: return TradeDecision(False) if not runtime.add_watch.triggered("补仓", stock_code, tick.last_price): return TradeDecision(False, "等待价格反弹确认") if runtime.orders.busy(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("zt","added") request = PlaceOrderRequest( op=OP_BUY, code=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