Files
big-qmt/py-client/strategy/trend/boot.py
2026-08-31 15:33:39 +08:00

207 lines
6.5 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 logging
import time
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
print("\n" + "=" * 80)
print(f"【时间】{datetime.now():%Y-%m-%d %H:%M:%S}")
if account_cfg is not None:
print(
"【配置】"
f"account_id: {account_cfg.account_id} "
f"host_key: {account_cfg.host_key} "
f"buy_value: {account_cfg.buy_value:.0f}"
)
if assets is not None:
print(
f"【资金】总资产:{assets.total:.2f}元,"
f"可用资金:{assets.available:.2f}"
)
else:
print("【资金】查询失败")
print(f"【持仓】{len(positions)}")
print("=" * 80)
for position in positions:
if position.volume <= 0:
continue
print(
f"【持仓】{position.stock_code} {position.stock_name} "
f"持仓={position.volume} 可用={position.can_use_volume} "
f"冻结={position.frozen_volume} 在途={position.on_road_volume} "
f"昨仓={position.yesterday_volume} 成本={position.open_price:.3f} "
f"现价={position.last_price:.3f} 市值={position.market_value:.2f} "
f"浮盈={position.float_profit:.2f} "
f"盈亏比例={position.profit_rate * 100:.2f}%"
)
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,
)
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)
try:
while True:
started_at = time.monotonic()
try:
RunOnce(run, signals)
except Exception:
# 单轮错误只记录日志,下一轮仍继续运行。
logging.exception("趋势策略本轮执行失败")
elapsed = time.monotonic() - started_at
time.sleep(max(0.0, 30.0 - elapsed))
finally:
run.executor.shutdown(wait=True, cancel_futures=True)
def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
"""按固定步骤执行一轮趋势策略, ``RunOnce``。"""
if not trading_time(datetime.now()):
return
# 1. 刷新订单数据,清理过期订单。
try:
run.orders.refresh(run.client)
except Exception:
logging.exception("取消过期订单失败")
return
# 2. 验证可用资金;低于资金安全线时禁止开新仓。
try:
assets = run.client.assets()
except Exception:
logging.exception("获取资产失败")
return
allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio
if not allow_open_by_cash:
logging.info("资金总闸:可用金额太少,禁止开新仓")
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
market_ok = market_allow_open(run.global_cfg.api_host)
# 4. 获取当前持仓及持仓证券代码。
try:
position_codes, positions = run.client.positions()
except Exception:
logging.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)
# 6. 获取持仓和待开仓证券的实时行情 tick。
all_codes = list(dict.fromkeys(position_codes + allow_codes))
try:
ticks = run.client.full_tick(all_codes)
except Exception:
logging.exception("获取行情失败")
return
# 7. 更新状态机
try:
run.state.reconcile(positions, run.orders.data)
except Exception:
logging.exception("订单状态对账失败,本轮禁止自动交易")
return
# 启动线程,开始计算
# 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)
def _wait_worker(name: str, future: Future) -> None:
"""保留单轮继续运行的语义,分别记录工作线程异常。"""
try:
future.result()
except Exception:
logging.exception("趋势策略%s线程失败", name)