Files
big-qmt/py-client/strategy/trend/boot.py
2026-09-01 14:28:49 +08:00

206 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""趋势策略启动器。
该模块负责组合 SDK、配置、状态存储和趋势策略组件供 main.py 调用。
"""
from __future__ import annotations
import time
import logging as log
from concurrent.futures import Future, ThreadPoolExecutor
from datetime import datetime
import config
from libs.calc import trading_time
from libs.market import market_allow_open
from libs.signal import init_signals, SignalItem
from sdk import Client
from libs.grid_take_profit import GridTrailingTracker
from .state import State
from .order import OrderBook
from .watch import DipWatch
from .runtime import Runtime
from .open import open_signal
from .positions import manage_positions
def Overview(assets, positions, account_cfg=None) -> None:
"""记录策略启动时的账户、资金和持仓概览。"""
account_cfg = account_cfg or config.account_config
if account_cfg is not None:
log.info("[启动] 账户=%s,主机=%s,单笔金额=%.2f", account_cfg.account_id, account_cfg.host_key, account_cfg.buy_value)
if assets is not None:
log.info("[启动] 总资产=%.2f,可用资金=%.2f", assets.total, assets.available)
else:
log.warning("[启动] 获取资金概览失败")
log.info("[启动] 持仓数量=%d", len(positions))
for position in positions:
if position.volume <= 0:
continue
log.info("[启动] %s %s,持仓=%d,可用=%d,成本=%.2f,现价=%.2f,盈亏=%.2f%%", position.stock_code, position.stock_name, position.volume, position.can_use_volume, position.open_price, position.last_price, position.profit_rate * 100)
def StartTrend() -> None:
"""初始化趋势策略,并以 30 秒间隔持续执行。"""
client = Client(
config.global_config.qmt_base_url,
config.global_config.qmt_token,
config.HTTP_TIMEOUT,
)
assets = client.assets()
_, positions = client.positions()
order_book = OrderBook()
order_book.refresh(client)
storeState = State.for_strategy(
config.global_config.qmt_data_dir,
config.account_config.strategy,
config.account_config.account_id,
)
storeState.reconcile(positions, order_book.data)
# 获取本策略的信号开仓数据
signals = init_signals(
config.global_config,
config.account_config.signal_allow,
)
log.info("[启动] 趋势策略已启动,账户=%s,信号=%d,持仓=%d", config.account_config.account_id, len(signals), len(positions))
run = Runtime(
client=client,
global_cfg=config.global_config,
account_cfg=config.account_config,
state=storeState,
orders=order_book,
open_watch=DipWatch(),
add_watch=DipWatch(),
profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct),
executor=ThreadPoolExecutor(max_workers=2, thread_name_prefix="trend"),
)
Overview(assets, positions, config.account_config)
DEFAULT_TICK_INTERVAL = 30
while True:
current_sec = time.localtime().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, signals)
except Exception as e:
log.error(f"[Trend] 本 tick 执行失败,下一 tick 继续: {e}", exc_info=True)
def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
"""按固定步骤执行一轮趋势策略, ``RunOnce``。"""
if not trading_time(datetime.now()):
log.info("[运行] 非交易时间,跳过本轮")
return
print("=" * 40 + f" RunOnce {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} " +"=" * 40)
started_at = time.monotonic()
# 1. 刷新订单数据,清理过期订单。
try:
run.orders.refresh(run.client)
except Exception:
log.exception("[订单] 刷新订单失败")
return
# 2. 验证可用资金;低于资金安全线时禁止开新仓。
try:
assets = run.client.assets()
except Exception:
log.exception("[资金] 获取资产失败")
return
allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio
if not allow_open_by_cash:
log.info("[开仓] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total)
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
market_ok = market_allow_open()
# 4. 获取当前持仓及持仓证券代码。
try:
position_codes, positions = run.client.positions()
except Exception:
log.exception("[持仓] 获取持仓失败")
return
# 5. 验证有效开仓信号:排除已有持仓和未决订单。
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))
# 6. 获取持仓和待开仓证券的实时行情 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
# 7. 更新状态机
try:
run.state.reconcile(positions, run.orders.data)
except Exception:
log.exception("[状态] 订单状态对账失败")
return
log.info("[RunOnce] 本轮就绪,持仓=%d,候选=%d,大盘允许=%s,资金允许=%s", len(positions), len(allow_open), market_ok, allow_open_by_cash)
# 启动线程,开始计算
# 9. 持仓计算。
futures: list[tuple[str, Future]] = [
(
"持仓计算",
run.executor.submit(
manage_positions,
run,
ticks,
positions,
market_ok,
assets.available,
),
)
]
# 10. 开仓计算:必须同时存在有效信号且大盘允许开仓。
if allow_open and market_ok and allow_open_by_cash:
futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open)))
# 11. 开始执行
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)