"""趋势策略持仓止盈与分级补仓。""" from __future__ import annotations import logging 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, Position, Tick from .order import PlaceOrderRequest from .runtime import Runtime from .state import STATUS_ING LEG_BASE = "base" LEG_ADDED = "add" LOSS_TIERS = (-30.0, -50.0) @dataclass(frozen=True, slots=True) class TradeDecision: """一次止盈或补仓判断的统一结果。""" submitted: bool message: str = "" reserved_cash: float = 0.0 def manage_positions( runtime: Runtime, ticks: dict[str, Tick], positions: list[Position], market_ok: bool, available: float, ) -> None: """处理所有真实持仓,并在本轮内统一控制补仓预算。""" active_keys = { _position_key(runtime, position.stock_code) for position in positions if position.volume > 0 and position.stock_code } runtime.profit_tracker.retain(active_keys) remaining_cash = max(0.0, available) logging.info("[持仓] 共 %d 只,开始处理", len(positions)) for position in positions: code = position.stock_code tick = ticks.get(code) if code in runtime.account_cfg.excluded_codes: continue if ( not code or position.open_price <= 0 or position.volume <= 0 or tick is None or tick.last_price <= 0 ): 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, ) if profit_decision.message: logging.info("[止盈] %s %s", code, profit_decision.message) 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=remaining_cash, ) remaining_cash -= loss_decision.reserved_cash if loss_decision.message: logging.info("[补仓] %s %s", code, loss_decision.message) def handle_profit( runtime: Runtime, position: Position, 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_rate:.2f}%,峰值网格={observation.current_grid}", ) if observation.state == GridState.RAISED: return TradeDecision( False, f"上涨至 {pnl_rate:.2f}%,峰值网格={observation.current_grid}", ) if observation.state in {GridState.STEADY}: return TradeDecision(False) 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(LEG_BASE) request = PlaceOrderRequest( client=runtime.client, op=OP_SELL, code=position.stock_code, volume=volume, order_id=order_id, strategy_name=runtime.account_cfg.strategy, ) if not runtime.orders.place(request): return TradeDecision(False, "止盈委托失败") return TradeDecision(True, f"卖出 {volume} 股,订单={order_id}") def handle_loss( runtime: Runtime, position: Position, tick: Tick, pnl_rate: float, available: float, ) -> TradeDecision: """按亏损档位、反弹确认和本轮剩余预算提交补仓。""" try: state = runtime.state.get(position.stock_code) except KeyError: return TradeDecision(False, "缺少持仓状态,跳过补仓") if state.added_num >= len(LOSS_TIERS): return TradeDecision(False, "已达到最大补仓次数") if pnl_rate > LOSS_TIERS[state.added_num]: return TradeDecision(False) if tick.last_price > 200 or position.market_value >= 60_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(LEG_ADDED) request = PlaceOrderRequest( client=runtime.client, op=OP_BUY, code=position.stock_code, volume=volume, order_id=order_id, strategy_name=runtime.account_cfg.strategy, ) if not runtime.orders.place(request): return TradeDecision(False, "补仓委托失败") state.added_num += 1 state.added_status = STATUS_ING state.added_order_id = order_id state.added_qty = volume state.added_cost = tick.last_price runtime.state.set(state) runtime.state.save() 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}"