refactor QMT client and optimize API
This commit is contained in:
@@ -12,6 +12,7 @@ from datetime import datetime
|
||||
import config
|
||||
from libs import init_signals, market_allow_open, trading_time
|
||||
from sdk import Client
|
||||
from libs.grid_take_profit import GridTrailingTracker
|
||||
from .state import State
|
||||
from .order import OrderBook
|
||||
from .watch import DipWatch
|
||||
@@ -78,7 +79,9 @@ def StartTrend() -> None:
|
||||
config.account_config.strategy,
|
||||
config.account_config.account_id,
|
||||
)
|
||||
storeState.sync_positions(positions)
|
||||
orders = client.trade_detail_data("order")
|
||||
deals = client.deals()
|
||||
storeState.reconcile(positions, orders, deals)
|
||||
|
||||
# 获取本策略的信号开仓数据
|
||||
signals = init_signals(config.global_config,["morning","tail","arbitrage"])
|
||||
@@ -90,6 +93,7 @@ def StartTrend() -> None:
|
||||
orders=OrderBook(),
|
||||
open_watch=DipWatch(),
|
||||
add_watch=DipWatch(),
|
||||
profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct),
|
||||
)
|
||||
|
||||
logging.info(
|
||||
@@ -129,9 +133,9 @@ def RunOnce(run: Runtime, signals) -> None:
|
||||
except Exception:
|
||||
logging.exception("获取资产失败")
|
||||
return
|
||||
if assets.available < assets.total * run.account_cfg.min_cash_ratio:
|
||||
allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio
|
||||
if not allow_open_by_cash:
|
||||
logging.info("资金总闸:可用金额太少,禁止开新仓")
|
||||
return
|
||||
|
||||
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
|
||||
market_ok = market_allow_open(run.global_cfg.api_host)
|
||||
@@ -143,11 +147,23 @@ def RunOnce(run: Runtime, signals) -> None:
|
||||
logging.exception("获取持仓失败")
|
||||
return
|
||||
|
||||
active_codes = set(position_codes)
|
||||
removed_codes = set(run.state.codes) - active_codes
|
||||
for code in removed_codes:
|
||||
run.state.delete(code)
|
||||
run.open_watch.forget(code)
|
||||
run.add_watch.forget(code)
|
||||
if removed_codes:
|
||||
run.state.save()
|
||||
|
||||
# 5. 验证有效开仓信号:排除已有持仓,并按 signal_allow 过滤。
|
||||
position_code_set = set(position_codes)
|
||||
allow_open = [
|
||||
signal for signal in signals if signal.code not in position_code_set
|
||||
]
|
||||
allow_open = []
|
||||
seen_codes = set(position_code_set)
|
||||
for signal in signals:
|
||||
if signal.code not in seen_codes:
|
||||
allow_open.append(signal)
|
||||
seen_codes.add(signal.code)
|
||||
|
||||
# 6. 获取持仓和待开仓证券的实时行情 tick。
|
||||
all_codes = list(position_codes)
|
||||
@@ -161,7 +177,7 @@ def RunOnce(run: Runtime, signals) -> None:
|
||||
return
|
||||
|
||||
# 7. 执行开仓:必须同时存在有效信号且大盘允许开仓。
|
||||
if allow_open and market_ok:
|
||||
if allow_open and market_ok and allow_open_by_cash:
|
||||
open_signal(run, ticks, allow_open)
|
||||
|
||||
# 8. 持仓计算。当前 Go 版本的 managePositions 为空,保留扩展入口。
|
||||
|
||||
@@ -41,7 +41,14 @@ def open_signal(run, ticks, open_signals) -> None:
|
||||
|
||||
# 6. 生成本地订单号并按最新价提交开仓委托。
|
||||
order_id = run.orders.new_order_id("base")
|
||||
request = PlaceOrderRequest(run.client, OP_BUY, item.code, volume, order_id)
|
||||
request = PlaceOrderRequest(
|
||||
run.client,
|
||||
OP_BUY,
|
||||
item.code,
|
||||
volume,
|
||||
order_id,
|
||||
item.signal_key,
|
||||
)
|
||||
if not run.orders.place(request):
|
||||
continue
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ OFFSET_FLAG = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
|
||||
BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PlaceOrderRequest:
|
||||
"""``OrderBook.place`` 提交委托所需的全部参数。"""
|
||||
|
||||
@@ -24,9 +24,10 @@ class PlaceOrderRequest:
|
||||
code: str
|
||||
volume: int
|
||||
order_id: str
|
||||
strategy_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class OrderItem:
|
||||
"""从 QMT 委托明细转换得到的本地订单记录。"""
|
||||
|
||||
@@ -37,6 +38,7 @@ class OrderItem:
|
||||
status: str
|
||||
created_at: datetime | None
|
||||
volume: int
|
||||
local_order_id: str = ""
|
||||
|
||||
|
||||
class OrderBook:
|
||||
@@ -50,8 +52,8 @@ class OrderBook:
|
||||
|
||||
@staticmethod
|
||||
def new_order_id(leg: str) -> str:
|
||||
"""生成不超过 24 个字符的策略订单号。"""
|
||||
return f"zt-{leg}-{secrets.token_hex(6)}"[:24]
|
||||
"""生成短订单号,为 QMT 备注中的信号键预留空间。"""
|
||||
return f"zt-{leg[:1]}-{secrets.token_hex(4)}"
|
||||
|
||||
def is_lock(self, side: str, code: str) -> bool:
|
||||
"""判断证券在指定买卖方向上是否已经被委托锁定。"""
|
||||
@@ -61,8 +63,9 @@ class OrderBook:
|
||||
def busy(self, code: str, side: str) -> bool:
|
||||
"""判断证券是否存在仍在处理中的同方向委托。"""
|
||||
with self.lock:
|
||||
order = self.data.get(f"{side}-{code}")
|
||||
return bool(order and order.status in BUSY_STATUSES)
|
||||
key = f"{side}-{code}"
|
||||
order = self.data.get(key)
|
||||
return key in self.index or bool(order and order.status in BUSY_STATUSES)
|
||||
|
||||
def refresh(self, client: Any) -> None:
|
||||
"""从 QMT 刷新当前委托明细和方向索引。"""
|
||||
@@ -71,7 +74,9 @@ class OrderBook:
|
||||
]
|
||||
with self.lock:
|
||||
self.data = {key: item for key, item in parsed_orders}
|
||||
self.index = [key for key, _ in parsed_orders]
|
||||
self.index = [
|
||||
key for key, item in parsed_orders if item.status in BUSY_STATUSES
|
||||
]
|
||||
|
||||
def cancel_expired(self, client: Any, now: datetime | None = None) -> None:
|
||||
"""尝试撤销超过有效期且具有委托编号的订单。"""
|
||||
@@ -85,20 +90,39 @@ class OrderBook:
|
||||
and current - order.created_at > self.timeout
|
||||
and order.id
|
||||
):
|
||||
client.can_cancel_order(order.id)
|
||||
client.cancel_by_id(order.id)
|
||||
|
||||
def place(self, request: PlaceOrderRequest) -> bool:
|
||||
"""按最新价提交委托,并立即写入本地方向锁。"""
|
||||
request.client.passorder_latest_tagged(
|
||||
result = request.client.passorder_latest_tagged(
|
||||
request.op,
|
||||
request.code,
|
||||
request.volume,
|
||||
request.strategy_name,
|
||||
request.order_id,
|
||||
)
|
||||
if not isinstance(result, dict):
|
||||
return False
|
||||
order_ref = str(result.get("order_ref") or "").strip().lower()
|
||||
if result.get("status") != "success" or order_ref in {"", "unknown", "none"}:
|
||||
return False
|
||||
|
||||
side = OFFSET_FLAG.get(str(request.op), "")
|
||||
pending = OrderItem(
|
||||
id=order_ref,
|
||||
code=request.code,
|
||||
side=side,
|
||||
remark=request.order_id,
|
||||
status="48",
|
||||
created_at=datetime.now(),
|
||||
volume=request.volume,
|
||||
local_order_id=request.order_id,
|
||||
)
|
||||
with self.lock:
|
||||
self.index.append(f"{side}-{request.code}")
|
||||
key = f"{side}-{request.code}"
|
||||
self.data[key] = pending
|
||||
if key not in self.index:
|
||||
self.index.append(key)
|
||||
return True
|
||||
|
||||
|
||||
@@ -126,6 +150,7 @@ def parse_order(row: dict[str, Any]) -> tuple[str, OrderItem]:
|
||||
status=str(row.get("m_nOrderStatus") or ""),
|
||||
created_at=created_at,
|
||||
volume=volume,
|
||||
local_order_id=_local_order_id(str(row.get("m_strRemark") or "")),
|
||||
)
|
||||
return f"{item.side}-{item.code}", item
|
||||
|
||||
@@ -146,3 +171,8 @@ def _parse_insert_datetime(row: dict[str, Any]) -> datetime | None:
|
||||
return datetime.strptime(date + clock, "%Y%m%d%H%M%S")
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _local_order_id(remark: str) -> str:
|
||||
"""兼容 ``local_order_id|signal_key`` 形式的 QMT 备注。"""
|
||||
return remark.split("|", 1)[0] if remark else ""
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
||||
|
||||
from config import AccountConfig, GlobalConfig
|
||||
from sdk import Client
|
||||
from libs.grid_take_profit import GridTrailingTracker
|
||||
|
||||
from .order import OrderBook
|
||||
from .state import State
|
||||
@@ -27,7 +28,7 @@ class Runtime:
|
||||
orders: 当前活动委托和证券方向锁。
|
||||
open_watch: 新开仓使用的价格反弹观察器。
|
||||
add_watch: 亏损补仓使用的价格反弹观察器。
|
||||
peak_grids: ``证券代码|仓位类型`` 到最高盈利网格的映射。
|
||||
profit_tracker: 跨轮保存的账户持仓最高盈利网格跟踪器。
|
||||
"""
|
||||
|
||||
# 外部服务与账户配置。
|
||||
@@ -40,4 +41,4 @@ class Runtime:
|
||||
orders: OrderBook
|
||||
open_watch: DipWatch
|
||||
add_watch: DipWatch
|
||||
|
||||
profit_tracker: GridTrailingTracker
|
||||
|
||||
@@ -15,6 +15,9 @@ from sdk import Position
|
||||
STATUS_NONE = ""
|
||||
STATUS_ING = "ING"
|
||||
STATUS_OK = "OK"
|
||||
STATUS_FAILED = "FAILED"
|
||||
STATUS_CANCELED = "CANCELED"
|
||||
STATUS_UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -110,6 +113,33 @@ class State:
|
||||
|
||||
self.save()
|
||||
|
||||
def reconcile(
|
||||
self,
|
||||
positions: Iterable[Position],
|
||||
orders: list[dict[str, str]],
|
||||
deals: list[dict[str, str]],
|
||||
) -> None:
|
||||
"""用真实持仓、委托和成交恢复本地状态,不增加持久化字段。"""
|
||||
position_list = list(positions)
|
||||
self.sync_positions(position_list)
|
||||
active_codes = {
|
||||
item.stock_code for item in position_list if item.volume > 0
|
||||
}
|
||||
for code in list(self.codes):
|
||||
if code not in active_codes:
|
||||
self.delete(code)
|
||||
|
||||
for code in list(self.codes):
|
||||
item = self.get(code)
|
||||
item.base_status = _reconcile_leg(
|
||||
item.base_order_id, item.base_status, orders, deals
|
||||
)
|
||||
item.added_status = _reconcile_leg(
|
||||
item.added_order_id, item.added_status, orders, deals
|
||||
)
|
||||
self.set(item)
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
"""将内存状态格式化写入 JSON,并原子替换正式文件。"""
|
||||
with self.lock:
|
||||
@@ -144,3 +174,40 @@ class State:
|
||||
}
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"[状态] 状态字段无效: {exc}") from exc
|
||||
|
||||
|
||||
def _reconcile_leg(
|
||||
local_order_id: str,
|
||||
current_status: str,
|
||||
orders: list[dict[str, str]],
|
||||
deals: list[dict[str, str]],
|
||||
) -> str:
|
||||
if current_status != STATUS_ING or not local_order_id:
|
||||
return current_status
|
||||
if any(local_order_id in row.get("m_strRemark", "") for row in deals):
|
||||
return STATUS_OK
|
||||
order = next(
|
||||
(
|
||||
row for row in orders
|
||||
if local_order_id in row.get("m_strRemark", "")
|
||||
),
|
||||
None,
|
||||
)
|
||||
if order is None:
|
||||
return STATUS_UNKNOWN
|
||||
traded = _as_int(order.get("m_nVolumeTraded"))
|
||||
status = str(order.get("m_nOrderStatus", ""))
|
||||
if traded > 0 and status not in {"48", "49", "50", "51", "52", "55"}:
|
||||
return STATUS_OK
|
||||
if status in {"54", "56"}:
|
||||
return STATUS_CANCELED
|
||||
if status in {"57", "58"}:
|
||||
return STATUS_FAILED
|
||||
return STATUS_ING
|
||||
|
||||
|
||||
def _as_int(value: object) -> int:
|
||||
try:
|
||||
return int(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
@@ -4,7 +4,7 @@ from threading import Lock
|
||||
import logging
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class _Entry:
|
||||
last_close: float
|
||||
expires_at: datetime
|
||||
|
||||
Reference in New Issue
Block a user