209 lines
7.0 KiB
Python
209 lines
7.0 KiB
Python
"""趋势策略持仓止盈与分级补仓。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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, 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 = "↓"
|
|
|
|
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_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("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
|