This commit is contained in:
2026-09-07 00:27:33 +08:00
parent fdcbdc7869
commit d37f9edefc
11 changed files with 839 additions and 384 deletions

View File

@@ -5,18 +5,21 @@
from __future__ import annotations
from concurrent.futures import Future, ThreadPoolExecutor
import logging as log
import time
from datetime import datetime, time as clock_time
from pathlib import Path
import config
from libs.calc import trading_time
from libs.market import market_allow_open
from libs.signal import init_signals
from libs.signal import SignalItem, init_signals
from libs.collector import collector_push
from libs.grid_take_profit import GridTrailingTracker
from sdk import Client
from libs.order import OrderBook
from libs.overview import Overview
from libs.order import BUSY_STATUSES, OrderBook
from libs.watch import DipWatch
from libs.runtime import Runtime
from .state import TState, SOLD
@@ -31,9 +34,11 @@ def StartZT() -> None:
config.global_config.qmt_token,
config.HTTP_TIMEOUT,
) as client:
state = TState.for_strategy(
config.global_config.qmt_data_dir, "zt", config.account_config.account_id
state = TState(
Path(config.global_config.qmt_data_dir)
/ f"zt_{config.account_config.account_id}_state.db"
)
executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="zt")
run = Runtime(
client=client,
global_cfg=config.global_config,
@@ -42,110 +47,153 @@ def StartZT() -> None:
open_watch=DipWatch(),
add_watch=DipWatch(),
profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct),
executor=executor
)
log.info(
"[ZT 启动] 账户=%s,底仓信号=dcm状态文件=%s",
run.account_cfg.account_id,
state.path,
portfolio = client.portfolio()
assets = portfolio.assets
positions = list(portfolio.positions.values())
run.orders.refresh(client, portfolio.orders)
# 获取本策略的信号开仓数据
signals = init_signals(config.global_config,["dcm"])
log.info("[启动] ZT 策略已启动,账户=%s,信号=%d,持仓=%d",
config.account_config.account_id,
len(signals),
len(positions),
)
Overview(assets, positions, config.account_config)
DEFAULT_TICK_INTERVAL = 30
while True:
now = datetime.now()
if now.time() >= clock_time(15):
# 收盘前最后一次只读对账,不发新单;未完成买回继续持久保存。
try:
portfolio = client.portfolio()
deals = client.deals()
state.reconcile(
list(portfolio.positions.values()),
deals,
now.date().isoformat(),
)
except Exception:
log.exception("[ZT] 收盘对账失败,保留本地待确认记录")
for item in state.items.values():
if item.phase == SOLD or state.busy(item.code):
log.warning("[ZT] 收盘仍有待完成轮次:%s", item.code)
lt = time.localtime()
if (lt.tm_hour, lt.tm_min, lt.tm_sec) >= (15, 0, 0):
log.info("[Trend] 已到 15:00结束趋势策略")
return
current_sec = lt.tm_sec
# 计算距离下一个目标时间点0秒或30秒的等待时间
if current_sec < DEFAULT_TICK_INTERVAL:
wait_seconds = DEFAULT_TICK_INTERVAL - current_sec
elif current_sec < 60:
wait_seconds = 60 - current_sec
else:
wait_seconds = DEFAULT_TICK_INTERVAL
# 等待到目标时间点
time.sleep(wait_seconds)
# 单轮失败不能杀死唯一的交易定时线程。
try:
RunOnce(run, state)
except Exception:
log.exception("[ZT] 本 tick 执行失败,下一个 tick 继续")
# 计算距离下一个目标时间点0秒或30秒的等待时间。
time.sleep(30 - datetime.now().second % 30)
RunOnce(run, state, signals)
except Exception as e:
log.error(
f"[Trend] 本 tick 执行失败,下一 tick 继续: {e}", exc_info=True
)
def RunOnce(run: Runtime, state: TState) -> None:
def RunOnce(run: Runtime, state: TState, signals: list[SignalItem]) -> None:
"""账户快照 → 成交对账 → 做 T 管理 → dcm 建仓,共用一份资金预算。"""
now = datetime.now()
if not trading_time(now) or now.time() >= clock_time(15):
if not trading_time(now):
return
today = now.date().isoformat()
started_at = time.monotonic()
# 1. 一次获取资产、持仓和订单,并清理过期订单。
portfolio = run.client.portfolio()
deals = run.client.deals()
positions = list(portfolio.positions.values())
run.orders.refresh(run.client, portfolio.orders)
# 状态只按真实成交记账,不使用委托状态推算数量和成本。
state.reconcile(positions, deals, today)
# 2. 获取本策略的信号开仓数据;信号失败不阻断已有做 T 买回。
try:
signals = init_signals(run.global_cfg, ["dcm"])
portfolio = run.client.portfolio()
assets = portfolio.assets
deals = run.client.deals()
position_codes = list(portfolio.positions)
positions = list(portfolio.positions.values())
run.orders.refresh(run.client, portfolio.orders)
state.reconcile(positions,deals)
except Exception:
log.exception("[ZT] 获取 dcm 信号失败,本轮只管理已有底仓")
signals = []
position_codes = {
position.stock_code for position in positions if position.volume > 0
}
candidates = [
signal
for signal in signals
if signal.signal_key == "dcm" and signal.code not in position_codes
]
# 3. 获取持仓和待开仓证券的实时行情 tick零持仓的待买回证券也包含在内。
codes = list(
dict.fromkeys(
list(position_codes)
+ list(state.items)
+ [signal.code for signal in candidates]
)
)
ticks = run.client.full_tick(codes) if codes else {}
now = datetime.now() # 网络请求可能跨过尾盘边界,提交前重新判断。
if not trading_time(now) or now.time() >= clock_time(15):
log.exception("[Portfolio] 刷新账户快照失败")
return
# 4. 先完成买回,避免开底仓抢占资金;交易逻辑串行,状态无需多线程写入。
available = max(0.0, portfolio.assets.available)
# 未确认买单可能尚未反映在资金快照中,保守预留,宁可少买也不重复使用。
for pending in state.pending.values():
if pending.kind != "sell":
tick = ticks.get(pending.code)
if tick is None or tick.last_price <= 0:
available = 0.0
break
available = max(0.0, available - pending.qty * tick.last_price * 1.01)
force = now.time() >= clock_time(14, 50)
available = manage_positions(run, state, ticks, positions, available, today, force)
futures: list[tuple[str, Future]] = [
(
"数据提交",
run.executor.submit(
collector_push,
run.account_cfg.account_id,
assets,
positions,
),
)
]
# 5. 验证可用资金;低于资金安全线时禁止开新仓,尾盘只完成做 T 买回
reserve = max(0.0, portfolio.assets.total * run.account_cfg.min_cash_ratio)
# 未完成的卖出/买回可能继续占用资金,不再额外开底仓。
outstanding = bool(state.pending) or any(
item.phase == SOLD for item in state.items.values()
# 2. 验证可用资金;低于资金安全线时禁止开新仓。
allow_open_by_cash = (
assets.available >= assets.total * run.account_cfg.min_cash_ratio
)
if not force and not outstanding and market_allow_open() and available > reserve:
open_signal(run, state, ticks, candidates, available - reserve)
if not allow_open_by_cash:
log.info(
"[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f",
assets.available,
assets.total,
)
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
market_ok = market_allow_open()
# 4. 验证有效开仓信号:排除已有持仓和未决订单。
allow_open: list[SignalItem] = []
allow_codes: list[str] = []
for signal in signals:
if signal.code not in position_codes:
allow_open.append(signal)
allow_codes.append(signal.code)
if allow_open and not market_ok:
log.info("[开仓] 禁止开仓:大盘信号不允许,候选=%d", len(allow_open))
# 5. 获取持仓和待开仓证券的实时行情 tick。
all_codes = list(dict.fromkeys(position_codes + allow_codes))
try:
ticks = run.client.full_tick(all_codes)
except Exception:
log.exception("[行情] 获取行情失败,代码数量=%d", len(all_codes))
return
# 6. 数据采集不与交易逻辑争用状态;采集函数自身隔离传输异常。
collector_push(run.account_cfg.account_id, portfolio.assets, positions)
log.info(
"[ZT] 本轮完成,底仓=%d待确认=%d耗时=%d毫秒",
len(state.items),
len(state.pending),
int((time.monotonic() - started_at) * 1000),
"[RunOnce] 本轮就绪,持仓=%d候选=%d大盘允许=%s,资金允许=%s",
len(positions),
len(allow_open),
market_ok,
allow_open_by_cash,
)
# 启动线程,开始计算
# 7. 持仓计算。
futures.append(
(
"持仓计算",
run.executor.submit(
manage_positions, run, ticks, positions, market_ok, assets.available
),
)
)
# 8. 开仓计算:必须同时存在有效信号且大盘允许开仓。
if allow_open and market_ok and allow_open_by_cash:
futures.append(
("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open))
)
# 9. 开始执行
for name, future in futures:
_wait_worker(name, future)
log.info(
"[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000)
)
def _wait_worker(name: str, future: Future) -> None:
"""保留单轮继续运行的语义,分别记录工作线程异常。"""
try:
future.result()
except Exception:
log.exception("[运行] %s线程失败", name)

View File

@@ -10,7 +10,7 @@ from libs.calc import calc_buy_volume
from sdk import OP_BUY
from libs.runtime import Runtime
from libs.order import PlaceOrderRequest
from .state import PendingOrder, TState
from .state import TState
def open_signal(run: Runtime, state: TState, ticks, signals, available: float) -> float:
@@ -20,23 +20,18 @@ def open_signal(run: Runtime, state: TState, ticks, signals, available: float) -
now = datetime.now()
if (now.hour, now.minute) >= (14, 50):
break
if item.signal_key != "dcm" or item.code in run.account_cfg.excluded_codes:
if item.code in run.account_cfg.excluded_codes:
continue
item_state = state.items.get(item.code)
if item_state is not None and item_state.base_qty > 0:
continue
# 1. 验证信号配置允许开仓的时间区间
signal_config = run.global_cfg.signals.get("dcm")
if signal_config is None or not check_timezone(signal_config.timezone):
continue
# 2. 检查该证券是否已有买入委托锁,防止重复下单。
# 由委托簿检查活动委托,防止重复下单
if (
state.busy(item.code)
or run.orders.busy(item.code, "BUY")
run.orders.busy(item.code, "BUY")
or run.orders.busy(item.code, "SELL")
):
continue
# 3. 验证行情和最新价格是否有效
# 行情无效或超过策略价格上限时跳过
tick = ticks.get(item.code)
price = tick.last_price if tick else 0.0
if (
@@ -45,29 +40,20 @@ def open_signal(run: Runtime, state: TState, ticks, signals, available: float) -
or price > run.account_cfg.zt_max_price
):
continue
# 4. 根据单笔买入金额计算整手开仓数量,预留少量价差和费用。
# 根据单笔买入金额计算整手数量,预留少量价差和费用。
budget = min(run.account_cfg.buy_value, available)
volume = calc_buy_volume(price, budget)
amount = price * volume * 1.01
if volume <= 0 or price * volume > budget or amount > available:
continue
# 5. 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。
# 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。
if not run.open_watch.triggered("ZT 建仓", item.code, price):
continue
order_id = run.orders.new_order_id("base")
request = PlaceOrderRequest(
OP_BUY, item.code, volume, order_id, "zt", kind="base"
)
state.new_order(
PendingOrder(
order_id,
item.code,
"base",
volume,
datetime.now().date().isoformat(),
)
)
# 即使响应丢失,也保留资金预算和 pending不能继续使用这笔钱。
# 即使响应丢失,本轮也预留资金;状态簿只在取得实际成交后入账。
available -= amount
if run.orders.place(run.client, request):
run.open_watch.forget(item.code)
@@ -75,45 +61,3 @@ def open_signal(run: Runtime, state: TState, ticks, signals, available: float) -
except Exception:
log.exception("[ZT 建仓] %s 处理异常,继续后续信号", item.code)
return available
def check_timezone(timezone: str, now: datetime | None = None) -> bool:
"""验证当前时间是否处于配置区间。
``*`` 表示全天允许;多个区间用逗号分隔,例如
``9:30-10:30,13:30-14:30``。同时支持跨午夜区间。
"""
timezone = str(timezone or "").strip()
if timezone == "*":
return True
current = now or datetime.now()
current_minutes = current.hour * 60 + current.minute
for section in timezone.split(","):
bounds = section.strip().split("-")
if len(bounds) != 2:
continue
start = _parse_minutes(bounds[0])
end = _parse_minutes(bounds[1])
if start is None or end is None:
continue
if start <= end and start <= current_minutes <= end:
return True
if start > end and (current_minutes >= start or current_minutes <= end):
return True
return False
def _parse_minutes(value: str) -> int | None:
"""把 ``时:分`` 转换为当天分钟数,无效值返回 None。"""
try:
hour_text, minute_text = value.strip().split(":")
hour, minute = int(hour_text), int(minute_text)
except (TypeError, ValueError):
return None
if not 0 <= hour <= 23 or not 0 <= minute <= 59:
return None
return hour * 60 + minute

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from datetime import datetime
import logging as log
import math
@@ -10,7 +9,7 @@ from libs.grid_take_profit import GridState
from sdk import OP_BUY, OP_SELL, PositionItem
from libs.order import PlaceOrderRequest
from libs.runtime import Runtime
from .state import PendingOrder, READY, SOLD, TState
from .state import READY, SOLD, TState
def manage_positions(
@@ -26,11 +25,7 @@ def manage_positions(
by_code = {position.stock_code: position for position in positions}
for code, state in list(state_store.items.items()):
try:
now = datetime.now()
if now.hour >= 15:
break
force_buy_back = force_buy_back or (now.hour, now.minute) >= (14, 50)
if code in run.account_cfg.excluded_codes or state_store.busy(code):
if code in run.account_cfg.excluded_codes:
continue
if run.orders.busy(code, "BUY") or run.orders.busy(code, "SELL"):
continue
@@ -52,11 +47,11 @@ def manage_positions(
continue
if state.phase == SOLD:
available = _try_buy_back(
run, state_store, state, price, available, today, force_buy_back
run, state, price, available, force_buy_back
)
elif state.phase == READY and position and not force_buy_back:
if price <= run.account_cfg.zt_max_price:
_try_sell(run, state_store, state, position, price, today)
_try_sell(run, state, position, price, today)
except Exception:
log.exception("[ZT 持仓] %s 处理异常,继续后续证券", code)
return available
@@ -64,7 +59,6 @@ def manage_positions(
def _try_sell(
run: Runtime,
state_store: TState,
state,
position: PositionItem,
price: float,
@@ -88,18 +82,15 @@ def _try_sell(
request = PlaceOrderRequest(
OP_SELL, state.code, volume, order_id, "zt", kind="sell"
)
state_store.new_order(PendingOrder(order_id, state.code, "sell", volume, today))
if run.orders.place(run.client, request):
log.info("[ZT 卖出] %s %d 股,等待成交后确定买回数量和价格", state.code, volume)
def _try_buy_back(
run: Runtime,
state_store: TState,
state,
price: float,
available: float,
today: str,
force: bool,
) -> float:
"""按实际卖出均价下跌后反弹买回;尾盘不再受下跌幅度、反弹及价格上限限制。"""
@@ -115,18 +106,14 @@ def _try_buy_back(
return available
order_id = run.orders.new_order_id("t-buy")
request = PlaceOrderRequest(OP_BUY, state.code, volume, order_id, "zt", kind="buy")
state_store.new_order(PendingOrder(order_id, state.code, "buy", volume, today))
# pending 已落盘,任何请求结果都预留资金;下一轮再从柜台快照确认。
# 本轮预留资金;状态簿只在取得实际成交后入账。
available -= amount
try:
if run.orders.place(run.client, request):
run.add_watch.forget(state.code)
log.info(
"[ZT 买回] %s %d 股,%s",
state.code,
volume,
"尾盘强制买回" if force else "下跌后反弹",
)
except Exception:
log.exception("[ZT 买回] %s 请求结果未知,保留 pending 和预算", state.code)
if run.orders.place(run.client, request):
run.add_watch.forget(state.code)
log.info(
"[ZT 买回] %s %d 股,%s",
state.code,
volume,
"尾盘强制买回" if force else "下跌后反弹",
)
return available

View File

@@ -1,15 +1,14 @@
"""做 T 策略的底仓、待确认委托和实际成交记录。"""
"""做 T 策略的持仓状态和逐笔实际成交记录。"""
from __future__ import annotations
import json
import logging as log
import math
from dataclasses import asdict, dataclass, field
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from time import time
from sdk import DealItem, PositionItem
from libs.orderbook import OrderBook
READY, SOLD, DONE = "READY", "SOLD", "DONE"
@@ -21,180 +20,162 @@ class TStateItem:
base_cost: float = 0.0
trade_date: str = ""
phase: str = READY
sell_order_id: str = ""
sell_qty: int = 0
sell_price: float = 0.0
buy_order_id: str = ""
base_order_id: str = ""
buy_qty: int = 0
buy_cost: float = 0.0
@dataclass(slots=True)
class PendingOrder:
order_id: str
code: str
kind: str # base底仓sell做 T 卖出buy做 T 买回
qty: int
trade_date: str
submit_at: float = field(default_factory=time)
id: int = 0
base_order_id: str = ''
added_order_id: str = ''
added_num: int = 0
added_qty: int = 0
added_cost: float = 0.0
class TState:
"""交易逻辑串行更新JSON 保存底仓、待确认委托及成交历史。"""
"""Apply actual executions immediately, atomically with their position changes."""
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self.items: dict[str, TStateItem] = {}
self.pending: dict[str, PendingOrder] = {}
self.records: list[dict] = []
self._store = OrderBook(path)
self.path = self._store.path
self._load()
@staticmethod
def _is_zt_deal(deal: DealItem) -> bool:
return (
deal.side == 'BUY' and deal.local_order_id.startswith(('zt-base-', 'zt-t-buy-'))
) or (
deal.side == 'SELL' and deal.local_order_id.startswith('zt-t-sell-')
)
@staticmethod
def _reset(item: TStateItem, date: str) -> bool:
if item.phase == DONE and item.trade_date != date:
item.phase, item.trade_date = READY, ''
item.sell_qty = item.buy_qty = 0
item.sell_price = item.buy_cost = 0.0
return True
return False
@classmethod
def for_strategy(
cls, data_dir: str | Path, strategy: str, account_id: str
) -> TState:
return cls(Path(data_dir) / f"{strategy}_{account_id}_state.json")
def busy(self, code: str) -> bool:
return any(order.code == code for order in self.pending.values())
def new_order(self, order: PendingOrder) -> None:
"""下单前落盘;请求超时不能当作失败删除,等待后续委托确认。"""
if self.busy(order.code):
raise ValueError(f"{order.code} 已有待确认委托")
self.pending[order.order_id] = order
try:
self.save()
except Exception:
del self.pending[order.order_id]
raise
def _apply_t_deal(cls, item: TStateItem, deal: dict) -> None:
"""实时入账与重启恢复共用同一套做 T 轮次计算。"""
cls._reset(item, deal['insert_date'])
qty, amount = deal['traded_volume'], deal['trade_amount']
if deal['side'] == 'SELL':
total = item.sell_qty + qty
item.sell_price = (item.sell_qty * item.sell_price + amount) / total
item.sell_qty = total
item.phase = SOLD
else:
total = item.buy_qty + qty
item.buy_cost = (item.buy_qty * item.buy_cost + amount) / total
item.buy_qty = total
item.phase = DONE if total >= item.sell_qty else SOLD
item.trade_date = deal['insert_date']
def reconcile(
self, positions: list[PositionItem], deals: list[DealItem], today: str
self, positions: list[PositionItem], deals: list[DealItem]
) -> None:
"""先按实际成交记账,再接管未知持仓;不覆盖已记录的底仓成本。"""
# 同一本地委托可能有多笔成交;按成交编号去重后合并数量和金额。
by_id: dict[str, dict[str, DealItem]] = {}
"""Deduplicate each fill; partial fills do not wait for order completion."""
today = datetime.now().date().isoformat()
seen = {row['sys_order_id'] for row in self.deals}
rows = []
for deal in deals:
if deal.local_order_id and deal.id:
by_id.setdefault(deal.local_order_id, {})[deal.id] = deal
for order_id, pending in list(self.pending.items()):
side = "SELL" if pending.kind == "sell" else "BUY"
rows = [
row
for row in by_id.get(order_id, {}).values()
if row.code == pending.code and row.side == side
]
if not rows:
log.warning("[ZT 状态] 成交暂未查到,保留待确认:%s", order_id)
if not self._is_zt_deal(deal) or deal.sys_order_id in seen:
continue
qty = sum(row.volume for row in rows)
# 成交未达到计划数量时继续等待,防止后续成交到达后重复记账。
if qty != pending.qty:
try:
row = self._store.deal_record(deal)
except ValueError:
continue
amounts = [
row.amount if row.amount > 0 else row.price * row.volume
for row in rows
if row.volume > 0
]
if any(not math.isfinite(amount) or amount <= 0 for amount in amounts):
continue
amount = sum(amounts)
cost = amount / qty if qty else 0.0
item = self.items.setdefault(pending.code, TStateItem(pending.code))
if pending.kind == "base":
item.base_order_id = order_id
item.base_qty, item.base_cost = qty, cost
elif pending.kind == "sell":
item.trade_date = today # 跨日成交也占用确认当天的一轮。
item.sell_order_id = order_id
item.sell_qty, item.sell_price = qty, cost
item.buy_qty, item.buy_cost = 0, 0.0
item.phase = SOLD if qty else READY
else:
total = item.buy_qty + qty
item.buy_cost = (
(item.buy_qty * item.buy_cost + amount) / total if total else 0.0
rows.append(row)
seen.add(deal.sys_order_id)
rows.sort(key=lambda r: (r['insert_date'], r['insert_time']))
modified = False
try:
# Snapshot includes these fills: subtract their net quantity before replay.
net = {}
for row in rows:
net[row['code']] = net.get(row['code'], 0) + (
row['traded_volume'] if row['side'] == 'BUY' else -row['traded_volume']
)
item.buy_qty = total
item.buy_order_id = order_id
item.phase = DONE if total >= item.sell_qty else SOLD
if item.phase == DONE:
item.trade_date = today
# 记录真实成交编号,重启后仍可核对本次状态变更的来源。
self.records.append(
{
**asdict(pending),
"confirmed_date": today,
"filled_qty": qty,
"filled_cost": cost,
"amount": amount,
"deal_ids": [row.id for row in rows],
}
)
del self.pending[order_id]
for position in positions:
code = position.stock_code
if code in self.items or position.volume <= 0:
continue
if not math.isfinite(position.open_price) or position.open_price <= 0:
continue
qty = max(0, position.volume - net.get(code, 0))
self.items[code] = TStateItem(code, qty, position.open_price if qty else 0.0)
modified = True
for position in positions:
code = position.stock_code
if position.volume <= 0 or self.busy(code):
continue
if (
code not in self.items
and math.isfinite(position.open_price)
and position.open_price > 0
):
self.items[code] = TStateItem(
code, position.volume, position.open_price
)
self.records.append(
{
"kind": "import",
"code": code,
"date": today,
"filled_qty": position.volume,
"filled_cost": position.open_price,
}
)
log.warning(
"[ZT 底仓] 首次接管 %s,使用当前均价,无法还原历史成本", code
)
# 全部卖出时快照可能已无该证券,按净卖出数量恢复待买回的底仓数量。
for code, delta in net.items():
if code not in self.items and delta < 0:
self.items[code] = TStateItem(code, -delta)
for item in self.items.values():
# 未买回的轮次跨日继续,不删除零持仓的做 T 债务。
if (
item.trade_date != today
and item.phase == DONE
and not self.busy(item.code)
):
item.phase, item.trade_date = READY, ""
item.sell_qty = item.buy_qty = 0
item.sell_price = item.buy_cost = 0.0
item.sell_order_id = item.buy_order_id = ""
self.save()
for row in rows:
item = self.items.setdefault(row['code'], TStateItem(row['code']))
self._reset(item, row['insert_date'])
qty, amount = row['traded_volume'], row['trade_amount']
if row['local_order_id'].startswith('zt-base-'):
total = item.base_qty + qty
item.base_cost = (item.base_qty * item.base_cost + amount) / total
item.base_qty = total
item.base_order_id = row['local_order_id']
else:
self._apply_t_deal(item, row)
self.deals.append(row)
modified = True
for item in self.items.values():
modified = self._reset(item, today) or modified
if modified:
self.save()
except Exception:
self._load()
raise
def save(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
temporary = self.path.with_suffix(self.path.suffix + ".tmp")
payload = {
"items": {key: asdict(item) for key, item in self.items.items()},
"pending": {key: asdict(item) for key, item in self.pending.items()},
"records": self.records,
}
temporary.write_text(
json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
encoding="utf-8",
)
temporary.replace(self.path)
try:
self._store.save(
{
code: {
'code': item.code,
'base_order_id': item.base_order_id,
'base_qty': item.base_qty,
'base_cost': item.base_cost,
'added_order_id': item.added_order_id,
'added_num': item.added_num,
'added_qty': item.added_qty,
'added_cost': item.added_cost,
'status': item.phase,
}
for code, item in self.items.items()
},
self.deals,
)
except Exception:
self._load()
raise
def _load(self) -> None:
if not self.path.is_file():
return
raw = json.loads(self.path.read_text(encoding="utf-8"))
self.items = {
code: TStateItem(**item) for code, item in raw["items"].items()
}
self.pending = {
key: PendingOrder(**item) for key, item in raw["pending"].items()
}
self.records = raw["records"]
self._store.load()
self.items = {}
for code, position in self._store.positions.items():
position = dict(position)
position['phase'] = position.pop('status')
self.items[code] = TStateItem(**position)
self.deals = [
{key: value for key, value in deal.items() if key != 'id'}
for deal in self._store.deals.values()
]
# 轮次明细不占用持仓表字段,从已保存的逐笔成交重建。
for deal in self.deals:
if not deal['local_order_id'].startswith('zt-base-') and deal['code'] in self.items:
self._apply_t_deal(self.items[deal['code']], deal)
for code, item in self.items.items():
if self._store.positions[code]['status'] == READY:
item.phase, item.trade_date = READY, ''
item.sell_qty = item.buy_qty = 0
item.sell_price = item.buy_cost = 0.0