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

216 lines
7.0 KiB
Python
Raw Normal View History

2026-09-11 17:40:51 +08:00
"""趋势策略持仓止盈与分级补仓。"""
2026-08-31 13:00:22 +08:00
2026-09-11 17:40:51 +08:00
from dataclasses import dataclass
2026-08-31 13:00:22 +08:00
2026-09-11 17:40:51 +08:00
from libs.calc import calc_buy_volume, calculate_min_profit_rate
2026-08-31 13:00:22 +08:00
from libs.grid_take_profit import GridState
2026-09-11 17:40:51 +08:00
from sdk import OP_BUY, OP_SELL, PositionItem, Tick
from libs.state import State
2026-09-06 13:12:48 +08:00
from libs.order import PlaceOrderRequest
from libs.runtime import Runtime
2026-09-11 17:40:51 +08:00
import logging as log
LOSS_TIERS = -30.0
@dataclass(slots=True)
class TradeDecision:
"""一次止盈或补仓判断的统一结果。"""
submitted: bool
message: str = ""
reserved_cash: float = 0.0
2026-08-31 13:00:22 +08:00
2026-09-06 13:12:48 +08:00
def manage_positions(
2026-09-11 17:40:51 +08:00
runtime: Runtime,
ticks: dict[str, Tick],
positions: list[PositionItem],
market_ok: bool,
available: float,
state:State,
) -> None:
# 遍历处理每个持仓
for position in positions:
2026-08-31 13:00:22 +08:00
try:
2026-09-11 17:40:51 +08:00
available = max(0, available)
code = position.stock_code
2026-09-06 11:53:28 +08:00
tick = ticks.get(code)
2026-09-11 17:40:51 +08:00
if code in runtime.account_cfg.excluded_codes:
log.info(
"[Position - ] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=已配置为排除股票",
code,
position.stock_name,
)
2026-09-10 12:50:40 +08:00
continue
2026-09-11 17:40:51 +08:00
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,
)
2026-09-06 11:53:28 +08:00
continue
2026-09-11 17:40:51 +08:00
posState = state.get_by_code(position.stock_code)
if not posState:
2026-09-10 12:50:40 +08:00
continue
2026-09-11 17:40:51 +08:00
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,
)
2026-09-06 11:53:28 +08:00
except Exception:
2026-09-11 17:40:51 +08:00
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