refactor QMT client and optimize API

This commit is contained in:
2026-08-28 22:46:04 +08:00
parent d09f271569
commit 29fee85b3d
18 changed files with 1239 additions and 519 deletions

View File

@@ -1,167 +1,187 @@
"""趋势策略持仓管理逻辑,对应 Go 版本的 ``logic/positions.go``"""
"""趋势策略持仓止盈与分级补仓"""
from __future__ import annotations
import logging
from math import floor
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 libs.calc import calc_buy_volume,calculate_min_profit_rate
from libs.grid_take_profit import GridState, GridTrailingTracker
from sdk import OP_BUY, OP_SELL
import config
from .order import PlaceOrderRequest
from .state import STATUS_ING, STATUS_NONE, STATUS_OK
from .runtime import Runtime
from .state import STATUS_ING
LEG_BASE = "base"
LEG_ADDED = "add"
LOSS_TIERS = (-30.0, -50.0)
# 止盈网格跟踪器延迟初始化,避免导入模块时账户配置尚未加载。
profit_tracker = None
# 分级补仓档位(百分比)
LOSS_TIERS = [-30, -50]
# 补仓反弹确认阈值(百分比)
LOSS_REBOUND_THRESHOLD = 0.5
@dataclass(frozen=True, slots=True)
class TradeDecision:
"""一次止盈或补仓判断的统一结果。"""
def manage_positions(run:Runtime, ticks, positions, market_ok: bool,available:float) -> None:
"""执行持仓计算。"""
logging.info(f"持仓:{len(positions)} 支股票,开始处理")
global profit_tracker
profit_tracker = GridTrailingTracker(step=run.account_cfg.grid_step_pct)
for idx,pos in positions:
code = pos['stock_code']
avg_price = pos.get('avg_price', 0)
volume = pos.get('volume', 0)
can_use_volume = pos.get('can_use_volume', 0)
current_price = ticks.get(code, {}).get('lastPrice', 0)
strategy_name = pos.get('strategy_name', '')
market_value = pos.get('market_value',0)
profit = pos.get('profit_rate', 0)
submitted: bool
message: str = ""
reserved_cash: float = 0.0
# 排除指定股票
if code in config.account_config.excluded_codes:
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
# 过滤无效仓位
if avg_price == 0 or can_use_volume == 0 or current_price == 0 or volume == 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)
# 计算盈亏率(百分比)
pnl_ratio = (current_price - avg_price) / avg_price * 100 if avg_price != 0 else 0
pnl_ratio = round(pnl_ratio, 2)
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)
# 计算最小利润率1倍
min_profit_rate_val = calculate_min_profit_rate(avg_price, 1)
# 盈利处理
is_closed, message = handle_profit(run,code,avg_price, pnl_ratio, min_profit_rate_val, can_use_volume, strategy_name)
if is_closed:
logging.info("profit", code, f"止盈执行 | {message}")
if message != "":
logging.info("profit", code, message)
# 补仓处理
if config.account_config.enable_loss_add_position and market_ok:
is_replenished, message = handle_loss(run,code,current_price,pnl_ratio,market_value,market_ok,available)
if is_replenished:
logging.info("loss", code, f"补仓执行 | {message}")
if message != "":
logging.info("loss", code, message)
# 盈利处理
def handle_profit(run:Runtime, code: str, pnl_rate: float,
min_profit_rate: float, vol: int) -> tuple[bool, str]:
"""
盈利处理 - 基于网格的止盈策略
Args:
code: 股票代码
open_price: 开仓价格
pnl_rate: 当前盈亏率(百分比)
min_profit_rate: 最小利润率阈值
vol: 可用股数
strategy_name: str
Returns:
tuple[bool, str]: (是否执行平仓, 操作说明)
"""
# 预检查:未达到最小利润率
if pnl_rate < min_profit_rate:
return False, ""
position_key = f"{run.account_cfg.account_id}:{code}"
observation = profit_tracker.observe(position_key, pnl_rate)
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:
msg = f"首次达到{pnl_rate}%,设置峰值网格{observation.current_grid}"
return False, msg
return TradeDecision(
False,
f"首次达到 {pnl_rate:.2f}%,峰值网格={observation.current_grid}",
)
if observation.state == GridState.RAISED:
return False, f"上涨至{pnl_rate}%,更新峰值网格{observation.current_grid}"
# 执行平仓
if observation.state == GridState.RETREAT:
order_id = run.orders.new_order_id(LEG_BASE)
request = PlaceOrderRequest(run.client, OP_SELL, code, vol, order_id)
result = run.orders.place(request)
if result :
success_msg = f"✓ 委托成功 | {vol}股 订单号:{result} 等待成交"
logging.info("profit", code, success_msg)
return True, success_msg
else:
fail_msg = f"止盈委托失败: {code}"
logging.error("profit", code, "✗ 止盈委托失败")
return False, fail_msg
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(run:Runtime, code: str, current_price,pnl_rate,market_value: float,market_ok: bool, available: float) -> tuple[bool, str]:
"""满足条件时提交补仓委托,并返回扣减后的剩余预算。"""
state = run.state.get(code)
added_num = state.get('added_num',0)
# 预检查:未达到最低补仓阈值
if pnl_rate > LOSS_TIERS[added_num]:
return False, ""
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 current_price>200 or market_value>=60000:
return False, f"成本价{current_price}>200,仓位价值{market_value}>=60000, 不补仓"
# 1. 大盘必须允许开仓,且价格已从观察低点达到反弹阈值。
if not market_ok or not run.add_watch.triggered("补仓", code, current_price):
return 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, "买入委托处理中")
# 2. 计算补仓数量和预计占用金额。
volume = calc_buy_volume(current_price, run.account_cfg.buy_value)
amount = current_price * volume
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, "本轮可用资金不足")
# 3. 检查预算。
if amount > available:
return False, f"f{code} f{amount} 仓位资金不够补仓"
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, "补仓委托失败")
# 是否已有未完成的买入委托
if run.orders.busy(run, code, "BUY"):
return False, f"{code}订单锁定中"
# 4. 生成补仓订单号并提交买入委托。
order_id = run.orders.new_order_id(LEG_ADDED)
request = PlaceOrderRequest(run.client, OP_BUY, code, volume, order_id)
result = run.orders.place(request)
if result :
state.added_num = +1
state.added_status = run.state.STATUS_ING
state.added_order_id = order_id
run.state.set(state)
run.state.save()
run.add_watch.forget(code)
return True,f"补仓委托成功: {code} {volume}手, 等待成交确认"
else:
return False,f"补仓失败: {code}"
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 forget(run, code: str) -> None:
"""持仓退出后清理开仓、补仓观察记录和止盈峰值。"""
run.peak_grids.pop(f"{code}|{LEG_ADDED}", None)
def _position_key(runtime: Runtime, code: str) -> str:
return f"{runtime.account_cfg.account_id}:{code}"