"""趋势策略启动器。 该模块负责组合 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" Ticker {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("[Order] 刷新订单失败") 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("[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total) # 3. 获取大盘状态,只有大盘信号允许时才执行开仓。 market_ok = market_allow_open() # 4. 获取当前持仓及持仓证券代码。 try: position_codes, positions = run.client.positions() except Exception: log.exception("[Position] 获取持仓失败") 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)