This commit is contained in:
2026-09-06 11:53:28 +08:00
parent ac9e9193ad
commit bcc6f02398
7 changed files with 612 additions and 218 deletions

View File

@@ -1,112 +1,114 @@
"""日内做 T 策略启动器。"""
"""做 T 策略启动器。
该模块负责组合 SDK、配置、状态存储和做 T 策略组件,供 main.py 调用。
"""
from __future__ import annotations
import logging
import logging as log
import time
from concurrent.futures import Future, ThreadPoolExecutor
from datetime import datetime, time as clock_time
import config
from libs.calc import trading_time
from libs.grid_take_profit import GridTrailingTracker
from libs.market import market_allow_open
from libs.signal import init_signals
from libs.collector import collector_push
from libs.grid_take_profit import GridTrailingTracker
from sdk import Client
from strategy.trend.order import OrderBook
from strategy.trend.watch import DipWatch
from .order import OrderBook
from .watch import DipWatch
from .runtime import Runtime
from .state import TState, SOLD
from .open import open_signal
from .positions import manage_positions
from .runtime import Runtime
from .state import TState
def StartZT() -> None:
client = Client(config.global_config.qmt_base_url, config.global_config.qmt_token, config.HTTP_TIMEOUT)
orders = OrderBook()
orders.refresh(client)
_, positions = client.positions()
state = TState.for_strategy(config.global_config.qmt_data_dir, config.account_config.strategy, config.account_config.account_id)
state.reconcile(positions, orders.data, datetime.now().date().isoformat())
run = Runtime(client, config.global_config, config.account_config, state, orders, DipWatch(), GridTrailingTracker(config.account_config.grid_step_pct))
while True:
started = time.monotonic()
try:
RunOnce(run)
except Exception:
logging.exception("ZT 策略本轮失败")
time.sleep(max(0.0, 30.0 - (time.monotonic() - started)))
"""初始化做 T 策略,并以 30 秒间隔持续执行。"""
with Client(config.global_config.qmt_base_url, 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)
run = Runtime(client, config.global_config, config.account_config, state,
OrderBook(), DipWatch(), DipWatch(),
GridTrailingTracker(config.account_config.grid_step_pct))
log.info("[ZT 启动] 账户=%s,底仓信号=dcm状态文件=%s", run.account_cfg.account_id, state.path)
while True:
now = datetime.now()
if now.time() >= clock_time(15):
# 收盘前最后一次只读对账,不发新单;未完成买回继续持久保存。
try:
portfolio = client.portfolio()
state.reconcile(list(portfolio.positions.values()), portfolio.orders, 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)
return
# 单轮失败不能杀死唯一的交易定时线程。
try:
RunOnce(run)
except Exception:
log.exception("[ZT] 本 tick 执行失败,下一个 tick 继续")
# 计算距离下一个目标时间点0秒或30秒的等待时间。
time.sleep(30 - datetime.now().second % 30)
def RunOnce(run: Runtime) -> None:
if not trading_time(datetime.now()):
"""账户快照 → 成交对账 → 做 T 管理 → dcm 建仓,共用一份资金预算。"""
now = datetime.now()
if not trading_time(now) or now.time() >= clock_time(15):
return
today = now.date().isoformat()
started_at = time.monotonic()
# 1. 一次获取资产、持仓和订单,并清理过期订单。
portfolio = run.client.portfolio()
positions = list(portfolio.positions.values())
run.orders.refresh(run.client, portfolio.orders)
# 对账使用完整原始订单列表,不能丢弃撤单和废单的部分成交。
run.state.reconcile(positions, portfolio.orders, today)
# 2. 获取本策略的信号开仓数据;信号失败不阻断已有做 T 买回。
try:
run.orders.refresh(run.client)
assets = run.client.assets()
position_codes, positions = run.client.positions()
signals = init_signals(run.global_cfg, ["dcm"])
except Exception:
logging.exception("[ZT] 刷新账户或订单失败")
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(run.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):
return
today = datetime.now().date().isoformat()
try:
signals = init_signals(run.global_cfg, run.account_cfg.signal_allow)
except Exception:
logging.exception("[ZT] 获取 dcm 信号失败")
return
candidate_codes = [item.code for item in signals if item.code not in position_codes]
codes = list(dict.fromkeys(position_codes + candidate_codes))
try:
ticks = run.client.full_tick(codes)
except Exception:
logging.exception("[ZT] 获取行情失败")
return
market_ok = market_allow_open(run.global_cfg.api_host)
can_open = market_ok and assets.available >= assets.total * run.account_cfg.min_cash_ratio
force_buy_back = datetime.now().time() >= clock_time(14, 50)
# 状态对账与开仓判断并行。持仓线程在自己的线程中等待对账完成,
# 以保证它读取到最新的底仓和做 T 轮次状态,避免并发写 State。
with ThreadPoolExecutor(max_workers=3, thread_name_prefix="zt") as executor:
state_future = executor.submit(run.state.reconcile, positions, run.orders.data, today)
open_future = executor.submit(_run_open_signal, state_future, run, ticks, signals, can_open)
positions_future = executor.submit(
_run_manage_positions,
state_future,
run,
ticks,
positions,
assets.available,
today,
force_buy_back,
)
_wait_worker("状态对账", state_future)
_wait_worker("开仓", open_future)
_wait_worker("持仓管理", positions_future)
# 4. 先完成买回,避免开底仓抢占资金;交易逻辑串行,状态无需多线程写入。
available = max(0.0, portfolio.assets.available)
# 未确认买单可能尚未反映在资金快照中,保守预留,宁可少买也不重复使用。
for pending in run.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, ticks, positions, available, today, force)
# 5. 验证可用资金;低于资金安全线时禁止开新仓,尾盘只完成做 T 买回。
reserve = max(0.0, portfolio.assets.total * run.account_cfg.min_cash_ratio)
# 未完成的卖出/买回可能继续占用资金,不再额外开底仓。
outstanding = bool(run.state.pending) or any(item.phase == SOLD for item in run.state.items.values())
if not force and not outstanding and market_allow_open() and available > reserve:
open_signal(run, ticks, candidates, available - reserve)
def _run_open_signal(state_future: Future, run: Runtime, ticks, signals, can_open: bool) -> None:
state_future.result()
if can_open:
open_signal(run, ticks, signals)
def _run_manage_positions(
state_future: Future,
run: Runtime,
ticks,
positions,
available: float,
today: str,
force_buy_back: bool,
) -> None:
state_future.result()
manage_positions(run, ticks, positions, available, today, force_buy_back)
def _wait_worker(name: str, future: Future) -> None:
try:
future.result()
except Exception:
logging.exception("[ZT] %s线程失败", name)
# 6. 数据采集不与交易逻辑争用状态;采集函数自身隔离传输异常。
collector_push(run.account_cfg.account_id, portfolio.assets, positions)
log.info("[ZT] 本轮完成,底仓=%d,待确认=%d,耗时=%d毫秒",
len(run.state.items), len(run.state.pending), int((time.monotonic() - started_at) * 1000))