Files
big-qmt/py-client/strategy/trend/positions.py

188 lines
6.1 KiB
Python

"""趋势策略持仓止盈与分级补仓。"""
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}"