更新qmt代码

This commit is contained in:
2026-01-04 22:43:13 +08:00
parent 040d65cf9e
commit afc703549f
8 changed files with 10852 additions and 55 deletions

View File

@@ -0,0 +1,527 @@
"""
追涨策略专用因子模块
包含:形态突破、筹码穿透、攻击资金流
"""
import numpy as np
import polars as pl
from main.factor.all_factors import calculate_all_factors
from main.factor.momentum_factors import ReturnFactor, VolatilityFactor
from main.factor.money_flow_factors import AccumAccel, ChipLockin, CostSqueeze, FlowIntensityFactor, HighCostSelling, InstNetAccum, LGFlowFactor
from main.factor.operator_framework import StockWiseFactor
from main.factor.special_factors import VolumeRatioFactor
from main.factor.technical_factors import CrossSectionalRankFactor, SMAFactor
# ==========================================
# 第一类:形态与趋势突破因子 (Price & Trend)
# ==========================================
class LimitUpGene(StockWiseFactor):
"""
涨停基因因子
逻辑:寻找'准涨停''强势封板'的特征。
追涨策略不仅看是否涨停,更看封板的坚决度和实体的饱满度。
"""
factor_id = "factor_limit_up_gene"
required_factor_ids = ["close", "open", "high", "low", "up_limit"]
def __init__(self):
super().__init__(
name=self.factor_id,
parameters={},
required_factor_ids=self.required_factor_ids
)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
open_price = g["open"]
high = g["high"]
low = g["low"]
up_limit = g["up_limit"]
# 1. 封板接近度:收盘价距离涨停价有多近 (1.0 表示封死涨停)
# 注意:这里加 1e-6 是为了防止 up_limit 为 NaN (虽然理论上不该有)
limit_proximity = close / (up_limit + 1e-6)
# 2. 实体饱满度:(收盘-开盘) / (最高-最低)。越接近1说明光头光脚多头越强。
range_len = high - low
body_len = close - open_price
body_strength = body_len / (range_len + 1e-6)
# 3. 极值修正如果是涨停body_strength 可能会失效(一字板),给予最高分
# 逻辑:如果封板 (limit_proximity > 0.99) 且 是一字板 (range_len 极小),给强分
# 综合打分:封板接近度 * 实体强度
score = limit_proximity * body_strength
# 稳态化
return score.alias(self.factor_id)
class TrendBreakout(StockWiseFactor):
"""
历史新高突破因子
逻辑:股价越接近历史新高(his_high),上方的套牢盘越少,拉升阻力越小。
"""
factor_id = "factor_trend_breakout"
required_factor_ids = ["close", "his_high", "his_low"]
def __init__(self):
super().__init__(
name=self.factor_id,
parameters={},
required_factor_ids=self.required_factor_ids
)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
his_high = g["his_high"]
# 距离历史新高的比率 (接近1表示即将突破或已突破)
# 注意:数据清洗时需确保 his_high 有效
dist_to_high = close / (his_high + 1e-6)
# 动量加速当前价格相对于5日前的涨幅
# 注意:这里假设 group_df 是按时间排序的单只股票数据
mom_5 = close / close.shift(5).fill_null(strategy="forward") - 1.0
# 核心逻辑:只有在接近新高时的动量才有效
breakout_score = dist_to_high * (1 + mom_5)
return breakout_score.log1p().alias(self.factor_id)
# ==========================================
# 第二类:筹码穿透与真空因子 (Chip Structure)
# ==========================================
class ChipPenetration(StockWiseFactor):
"""
筹码穿透率因子 (Blue Sky Factor)
逻辑收盘价强力穿透95%筹码成本线,意味着上方进入'真空区'
此时所有持筹者都获利,抛压最小。
"""
factor_id = "factor_chip_penetration"
required_factor_ids = ["close", "cost_50pct", "cost_95pct", "vol"]
def __init__(self):
super().__init__(
name=self.factor_id,
parameters={},
required_factor_ids=self.required_factor_ids
)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
cost95 = g["cost_95pct"]
cost50 = g["cost_50pct"]
vol = g["vol"]
# 1. 穿透度当前价格相对于95%成本线的位置
# > 0 表示突破,数值越大突破越强
penetration = (close - cost95) / (cost95 + 1e-6)
# 2. 放量确认:突破必须伴随放量
vol_5d = vol.rolling_mean(window_size=5, min_periods=1)
vol_ratio = vol / (vol_5d + 1e-6)
# 逻辑:只有放量的突破才是真突破
# 使用 sigmoid 类似的逻辑平滑 volume 影响
valid_breakout = penetration * pl.when(vol_ratio > 1.0).then(vol_ratio.log1p()).otherwise(0.5)
return valid_breakout.alias(self.factor_id)
class WinnerExpansion(StockWiseFactor):
"""
获利盘扩张速率因子
逻辑:追涨最核心的动力来源是'赚钱效应'的快速扩散。
如果我们无法直接获得 winner_rate可以通过 cost 分布估算。
"""
factor_id = "factor_winner_expansion"
required_factor_ids = ["close", "cost_5pct", "cost_95pct"]
def __init__(self):
super().__init__(
name=self.factor_id,
parameters={},
required_factor_ids=self.required_factor_ids
)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
cost5 = g["cost_5pct"]
cost95 = g["cost_95pct"]
# 简易估算获利盘比例 (0~1)
# 假设筹码在 cost5 到 cost95 之间均匀分布
chip_range = cost95 - cost5
winner_proxy = (close - cost5) / (chip_range + 1e-6)
# 截断到 0-1
winner_proxy = winner_proxy.clip(0.0, 1.0)
# 计算获利盘的变化率 (一阶差分)
expansion_rate = winner_proxy.diff()
# 逻辑:我们要找的是获利盘突然急剧增加的时刻 (爆拉脱离成本区)
return expansion_rate.fill_null(0.0).alias(self.factor_id)
# ==========================================
# 第三类:攻击型资金流因子 (Attack Flow)
# ==========================================
class AttackFlow(StockWiseFactor):
"""
主力攻击流因子
逻辑:区别于普通的净流入,我们只关注'上涨过程中的'主力买入。
如果股价下跌而主力流入,可能是左侧抄底(不适合追涨);
如果股价上涨且主力大幅流入,这是右侧点火(适合追涨)。
"""
factor_id = "factor_attack_flow"
required_factor_ids = ["close", "open", "buy_lg_vol", "buy_elg_vol", "sell_lg_vol", "sell_elg_vol", "circ_mv"]
def __init__(self):
super().__init__(
name=self.factor_id,
parameters={},
required_factor_ids=self.required_factor_ids
)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
open_price = g["open"]
buy_elg = g["buy_elg_vol"]
buy_lg = g["buy_lg_vol"]
sell_elg = g["sell_elg_vol"]
sell_lg = g["sell_lg_vol"]
circ_mv = g["circ_mv"]
# 1. 计算大单净额
main_net = (buy_elg + buy_lg) - (sell_elg + sell_lg)
# 2. 归一化 (换手率视角)
circ_shares = (circ_mv * 10000) / (close + 1e-6) # 假设 circ_mv 单位万元
net_rate = main_net / (circ_shares + 1e-6)
# 3. 价格强度权重
# 如果是阳线 (Close > Open),权重为正且放大;阴线权重为 0 或 负
price_strength = (close - open_price) / (open_price + 1e-6)
# 核心逻辑:资金流 * 价格涨幅
# 只有当 资金大幅净流入 AND 价格大涨 时,该因子才会有极高值
attack_score = net_rate * price_strength
# 只保留正向攻击 (负向代表出货或洗盘,暂不计入追涨分)
attack_score = pl.when(attack_score > 0).then(attack_score).otherwise(0.0)
return attack_score.log1p().alias(self.factor_id)
class DivergenceAlert(StockWiseFactor):
"""
量价/资金背离因子 (负面因子)
逻辑:价格在涨,但主力在跑。这是追涨的大忌。
用于过滤掉诱多陷阱。
"""
factor_id = "factor_divergence_alert"
required_factor_ids = ["close", "open", "buy_lg_vol", "buy_elg_vol", "sell_lg_vol", "sell_elg_vol", "vol"]
def __init__(self):
super().__init__(
name=self.factor_id,
parameters={},
required_factor_ids=self.required_factor_ids
)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
open_price = g["open"]
vol = g["vol"]
# 1. 价格涨跌幅
pct_change = (close - open_price) / open_price
# 2. 主力资金净占比
main_net = (g["buy_elg_vol"] + g["buy_lg_vol"]) - (g["sell_elg_vol"] + g["sell_lg_vol"])
flow_ratio = main_net / (vol + 1e-6)
# 3. 背离识别
# 情况A (诱多):价格大涨 (pct > 2%) 但 主力净流出 (flow < -0.1)
trap_signal = (pct_change > 0.02) & (flow_ratio < -0.1)
# 转换为因子值:背离越严重,值越负
# 正常情况给0背离情况给负分
factor = pl.when(trap_signal).then(flow_ratio * 10).otherwise(0.0)
return factor.alias(self.factor_id)
class PriceGammaFactor(StockWiseFactor):
"""
价格加速度因子 (Gamma)
逻辑:识别加速上涨的股票。
比如前天涨1%昨天涨3%今天涨8% -> 加速度极高 -> 适合追涨。
"""
factor_id = "factor_price_gamma"
required_factor_ids = ["close"]
def __init__(self):
super().__init__(name=self.factor_id, parameters={}, required_factor_ids=self.required_factor_ids)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
# 1. 计算每日收益率
ret = close.pct_change()
# 2. 计算收益率的变化率 (即加速度)
# 追涨策略看重短期的爆发比如最近3天
# 使用线性回归斜率或者简单的差分来近似
# 简单版:(Ret_T - Ret_T-2)
accel = ret - ret.shift(2)
# 进阶版:只关注正向加速 (负向加速不重要,那是下跌或回调)
# 如果收益率是负的,直接给低分
score = pl.when(ret > 0).then(accel).otherwise(-1.0)
return score.alias(self.factor_id)
class TrendEfficiencyFactor(StockWiseFactor):
"""
趋势效率因子 (ER - Efficiency Ratio)
逻辑:位移 / 路程。
数值越接近 1.0,说明走势越像一根直线(单边拉升),追涨胜率越高。
"""
factor_id = "factor_trend_efficiency"
required_factor_ids = ["close"]
def __init__(self, window=10):
super().__init__(name=self.factor_id, parameters={"window": window}, required_factor_ids=self.required_factor_ids)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
close = g["close"]
window = self.parameters["window"]
# 1. 总位移 (Change): |Price_T - Price_T-n|
change = (close - close.shift(window)).abs()
# 2. 总路程 (Path): sum(|Price_t - Price_t-1|)
# 也就是每一天波动的绝对值之和
path = (close - close.shift(1)).abs().rolling_sum(window)
# 3. 效率 = 位移 / 路程
efficiency = change / (path + 1e-6)
return efficiency.alias(self.factor_id)
class MoneyUrgencyFactor(StockWiseFactor):
"""
资金饥渴度因子
逻辑:量比 * 大单主动买入占比
"""
factor_id = "factor_money_urgency"
required_factor_ids = ["vol", "buy_lg_vol", "buy_elg_vol", "circ_mv"]
def __init__(self):
super().__init__(name=self.factor_id, parameters={}, required_factor_ids=self.required_factor_ids)
def calc_factor(self, g: pl.DataFrame) -> pl.Series:
vol = g["vol"]
big_buy = g["buy_lg_vol"] + g["buy_elg_vol"]
# 1. 量比 (Volume Ratio): 今日量 / 过去5日均量
vol_ma5 = vol.rolling_mean(5).shift(1) # 注意避开未来函数分母用T-1及之前
vol_ratio = vol / (vol_ma5 + 1e-6)
# 2. 攻击性买入占比 (Aggressive Buy Ratio)
attack_ratio = big_buy / (vol + 1e-6)
# 3. 共振:只有放量且主力大买,才是追涨信号
# 如果缩量大买(可能是控盘),如果放量大卖(出货)
# 这里用乘法放大共振效应
urgency = vol_ratio * attack_ratio
return urgency.alias(self.factor_id)
from typing import List, Dict, Any
import polars as pl
# 假设你之前的因子定义都在这些模块中
# from main.factor.chasing_factors import (
# LimitUpGene, TrendBreakout, # 形态类
# ChipPenetration, WinnerExpansion, # 筹码类
# AttackFlow, DivergenceAlert # 资金类
# )
# from main.factor.fund_flow import LGFlowFactor, CostSqueeze, ... # 原有因子
# from main.factor.common import CrossSectionalRankFactor, SMAFactor ...
# def run_chasing_strategy_pipeline(df: pl.DataFrame) -> pl.DataFrame:
# """
# 执行【追涨/打板策略】的因子计算流程
# 该函数会组合:
# 1. 基础动量与波动率因子 (Base)
# 2. 资金流与筹码原有因子 (Legacy)
# 3. 新增的追涨专用因子 (New Chasing Factors)
# 4. 截面Rank因子 (用于选股排序)
# Returns:
# df_result: 包含所有因子列的 DataFrame
# """
# # =======================================================
# # 1. 配置股票截面因子 (Stock Wise)
# # =======================================================
# stock_configs = [
# # --- A. 新增:追涨核心因子 (Priority High) ---
# {"class": LimitUpGene, "params": {}}, # 涨停基因 (形态)
# {"class": TrendBreakout, "params": {}}, # 趋势突破 (形态)
# {"class": ChipPenetration, "params": {}}, # 筹码穿透 (筹码)
# {"class": WinnerExpansion, "params": {}}, # 获利盘扩张 (筹码)
# {"class": AttackFlow, "params": {}}, # 攻击资金流 (资金)
# {"class": DivergenceAlert, "params": {}}, # 顶背离警示 (风控)
# # --- B. 保留:原有高价值因子 (用于辅助验证) ---
# # 资金流强度
# {"class": FlowIntensityFactor, "params": {}},
# {"class": LGFlowFactor, "params": {}},
# {"class": InstNetAccum, "params": {}}, # 机构净累积
# {"class": AccumAccel, "params": {}}, # 累积加速
# # 筹码结构
# {"class": CostSqueeze, "params": {}}, # 成本挤压 (用于低位启动判断)
# {"class": ChipLockin, "params": {}}, # 筹码锁定
# {"class": HighCostSelling, "params": {}}, # 高位抛压 (用于风控)
# # --- C. 基础:技术指标 (用于过滤) ---
# {"class": SMAFactor, "params": {"window": 5}}, # 5日线防守
# {"class": SMAFactor, "params": {"window": 20}}, # 趋势判断
# {"class": VolatilityFactor, "params": {"period": 10}}, # 波动率(剔除织布机)
# {"class": ReturnFactor, "params": {"period": 5}}, # 5日涨幅
# {"class": ReturnFactor, "params": {"period": 20}}, # 月涨幅
# {"class": VolumeRatioFactor, "params": {}}, # 量比
# ]
# # =======================================================
# # 2. 配置日期截面因子 (Date Wise / Cross Sectional)
# # =======================================================
# # 追涨策略的核心在于:买入全市场最强的票。
# # 因此,我们需要对核心因子进行截面排序 (Rank)。
# date_configs = [
# # --- 基础排序 ---
# {"class": CrossSectionalRankFactor, "params": {"column": "circ_mv", "name": "rank_mv"}}, # 市值排序(剔除微盘)
# {"class": CrossSectionalRankFactor, "params": {"column": "return_5", "name": "rank_ret5"}}, # 短期强度排序
# # --- 策略核心排序 (重要!) ---
# # 1. 攻击力排序:全市场谁的主力攻击性最强?
# {
# "class": CrossSectionalRankFactor,
# "params": {"column": "factor_attack_flow", "name": "rank_attack_flow"}
# },
# # 2. 突破度排序:全市场谁的上方真空度最高?
# {
# "class": CrossSectionalRankFactor,
# "params": {"column": "factor_chip_penetration", "name": "rank_chip_penetration"}
# },
# # 3. 涨停基因排序:全市场谁的板最硬?
# {
# "class": CrossSectionalRankFactor,
# "params": {"column": "factor_limit_up_gene", "name": "rank_limit_gene"}
# },
# # 4. 资金流强度排序
# {
# "class": CrossSectionalRankFactor,
# "params": {"column": "flow_intensity", "name": "rank_flow_intensity"}
# }
# ]
# # =======================================================
# # 3. 调用统一计算接口
# # =======================================================
# print(f"开始计算追涨策略因子... 包含 {len(stock_configs)} 个股票因子配置")
# # 调用你提供的 calculate_all_factors
# # 注意:这里会覆盖函数内部的默认 list只计算我们指定的
# result_df, factor_ids = calculate_all_factors(
# df=df,
# stock_factor_configs=stock_configs,
# date_factor_configs=date_configs
# )
# print(f"计算完成。生成因子列: {factor_ids}")
# return result_df, factor_ids
def run_chasing_strategy_pipeline(df: pl.DataFrame) -> pl.DataFrame:
stock_configs = [
{"class": LimitUpGene, "params": {}}, # 涨停基因 (形态)
{"class": TrendBreakout, "params": {}}, # 趋势突破 (形态)
{"class": ChipPenetration, "params": {}}, # 筹码穿透 (筹码)
{"class": WinnerExpansion, "params": {}}, # 获利盘扩张 (筹码)
{"class": AttackFlow, "params": {}}, # 攻击资金流 (资金)
{"class": DivergenceAlert, "params": {}}, # 顶背离警示 (风控)
# --- B. 保留:原有高价值因子 (用于辅助验证) ---
# 资金流强度
{"class": FlowIntensityFactor, "params": {}},
{"class": LGFlowFactor, "params": {}},
{"class": InstNetAccum, "params": {}}, # 机构净累积
{"class": AccumAccel, "params": {}}, # 累积加速
# 筹码结构
{"class": CostSqueeze, "params": {}}, # 成本挤压 (用于低位启动判断)
{"class": ChipLockin, "params": {}}, # 筹码锁定
{"class": HighCostSelling, "params": {}}, # 高位抛压 (用于风控)
# --- C. 基础:技术指标 (用于过滤) ---
{"class": SMAFactor, "params": {"window": 5}}, # 5日线防守
{"class": SMAFactor, "params": {"window": 20}}, # 趋势判断
{"class": VolatilityFactor, "params": {"period": 10}}, # 波动率(剔除织布机)
{"class": ReturnFactor, "params": {"period": 5}}, # 5日涨幅
{"class": ReturnFactor, "params": {"period": 20}}, # 月涨幅
{"class": VolumeRatioFactor, "params": {}}, # 量比
# 1. 爆发力 (Gamma)
{"class": PriceGammaFactor, "params": {}},
# 2. 纯粹度 (Efficiency)
{"class": TrendEfficiencyFactor, "params": {"window": 10}},
# 3. 饥渴度 (Urgency)
{"class": MoneyUrgencyFactor, "params": {}},
# # 4. 辅助:位置修正 (防止追在山顶)
# # 使用相对位置,剔除已经翻倍的票
# {"class": LowPositionStart, "params": {}},
]
# 日期截面配置 (关键步骤)
date_configs = [
# 对三个核心因子进行排序
{"class": CrossSectionalRankFactor, "params": {"column": "factor_price_gamma", "name": "rank_gamma"}},
{"class": CrossSectionalRankFactor, "params": {"column": "factor_trend_efficiency_10", "name": "rank_eff"}},
{"class": CrossSectionalRankFactor, "params": {"column": "factor_money_urgency", "name": "rank_urgency"}},
]
# ... 执行计算 ...
result_df, calc_feature = calculate_all_factors(df, stock_configs, date_configs)
# ==========================================
# 核心差异点:如何利用因子选股?
# ==========================================
# 不使用简单的加权求和,而是使用“漏斗筛选”或“极值乘积”
# 模拟“规则型策略”的严格性
# result_df = result_df.with_columns([
# (
# # 逻辑:
# # 1. 加速度要在市场前 10% (rank_gamma > 0.9)
# # 2. 走势要非常丝滑 (rank_eff > 0.8)
# # 3. 资金要非常急迫 (rank_urgency > 0.8)
# # 4. 乘法效应:强者恒强
# pl.col("rank_gamma_true_factor_price_gamma") * pl.col("rank_eff_true_factor_trend_efficiency_10") * pl.col("rank_urgency")
# ).alias("aggressive_score")
# ])
return result_df, calc_feature

File diff suppressed because one or more lines are too long

109
qmt/api_server.py Normal file
View File

@@ -0,0 +1,109 @@
# coding:utf-8
import os
import datetime
from typing import Optional, List, Dict, Any
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from qmt_engine import QMTEngine, QMTStatus
# ================= Pydantic模型 =================
class StatusResponse(BaseModel):
"""状态响应模型"""
running: bool
qmt_connected: bool
start_time: str
last_loop_update: str
account_id: str
class PositionsResponse(BaseModel):
"""持仓响应模型"""
real_positions: List[Dict[str, Any]]
virtual_positions: Dict[str, Dict[str, str]]
class LogsResponse(BaseModel):
"""日志响应模型"""
logs: List[str]
# ================= FastAPI应用 =================
class QMTAPIServer:
"""QMT API服务器"""
def __init__(self, qmt_engine: QMTEngine):
self.app = FastAPI(title="QMT Monitor")
self.qmt_engine = qmt_engine
self._setup_middleware()
self._setup_routes()
def _setup_middleware(self):
"""设置中间件"""
self.app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
def _setup_routes(self):
"""设置路由"""
@self.app.get("/", summary="仪表盘页面")
async def read_root():
"""返回仪表盘HTML页面"""
if os.path.exists("dashboard.html"):
return FileResponse("dashboard.html")
return {"error": "Dashboard not found"}
@self.app.get("/api/status", response_model=StatusResponse, summary="获取系统状态")
def get_status():
"""获取QMT连接状态和系统信息"""
status = self.qmt_engine.get_status()
return StatusResponse(
running=status.is_running,
qmt_connected=status.is_connected,
start_time=status.start_time,
last_loop_update=status.last_heartbeat,
account_id=status.account_id
)
@self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息")
def get_positions():
"""获取实盘和虚拟持仓信息"""
positions = self.qmt_engine.get_positions()
return PositionsResponse(
real_positions=positions["real_positions"],
virtual_positions=positions["virtual_positions"]
)
@self.app.get("/api/logs", response_model=LogsResponse, summary="获取日志")
def get_logs(lines: int = Query(50, ge=1, le=1000, description="返回日志行数")):
"""获取最近的交易日志"""
logs = self.qmt_engine.get_logs(lines)
return LogsResponse(logs=logs)
@self.app.get("/api/health", summary="健康检查")
def health_check():
"""健康检查接口"""
status = self.qmt_engine.get_status()
if status.is_running and status.is_connected:
return {"status": "healthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
else:
return {"status": "unhealthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
def get_app(self) -> FastAPI:
"""获取FastAPI应用实例"""
return self.app
# ================= 辅助函数 =================
def create_api_server(qmt_engine: QMTEngine) -> FastAPI:
"""创建API服务器"""
server = QMTAPIServer(qmt_engine)
return server.get_app()

53
qmt/main.py Normal file
View File

@@ -0,0 +1,53 @@
# coding:utf-8
import threading
import sys
import uvicorn
from .qmt_engine import QMTEngine
from .api_server import create_api_server
def main():
"""主函数 - 启动QMT交易引擎和API服务器"""
print(">>> 系统正在启动...")
# 创建QMT引擎实例
engine = QMTEngine()
try:
# 初始化引擎
engine.initialize('config.json')
print("✅ QMT引擎初始化成功")
except Exception as e:
print(f"❌ QMT引擎初始化失败: {e}")
sys.exit(1)
# 启动交易线程
trading_thread = threading.Thread(target=engine.run_trading_loop, daemon=True)
trading_thread.start()
print("✅ 交易线程启动成功")
# 创建API服务器
app = create_api_server(engine)
print("✅ API服务器创建成功")
# 启动Web服务
print(">>> Web服务启动: http://localhost:8001")
try:
uvicorn.run(
app,
host="0.0.0.0",
port=8001,
log_level="warning",
access_log=False
)
except KeyboardInterrupt:
print("\n>>> 正在关闭系统...")
engine.stop()
print(">>> 系统已关闭")
if __name__ == '__main__':
# 使用 -u 参数运行是最佳实践: python -u main.py
# 但这里也在代码里强制 flush 了
main()

426
qmt/qmt_engine.py Normal file
View File

@@ -0,0 +1,426 @@
# coding:utf-8
import time
import datetime
import traceback
import sys
import json
import os
import threading
import logging
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
import redis
from xtquant import xtdata
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant
# ================= 0. Windows 控制台防卡死补丁 =================
try:
import ctypes
kernel32 = ctypes.windll.kernel32
# 禁用快速编辑模式 (0x0040),防止鼠标点击终端导致程序挂起
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 128)
except:
pass
@dataclass
class QMTStatus:
"""系统状态封装类"""
is_connected: bool
start_time: str
last_heartbeat: str
account_id: str
is_running: bool
# ================= 1. 虚拟持仓与对账辅助类 =================
class PositionManager:
"""Redis 持仓管理器:负责维护每个子策略的虚拟仓位"""
def __init__(self, r_client):
self.r = r_client
def _get_key(self, strategy_name):
return f"POS:{strategy_name}"
def mark_holding(self, strategy_name, code):
"""下单时先在 Redis 占位0股占用一个槽位"""
self.r.hsetnx(self._get_key(strategy_name), code, 0)
def rollback_holding(self, strategy_name, code):
"""报单失败时回滚,释放 Redis 占位"""
key = self._get_key(strategy_name)
val = self.r.hget(key, code)
if val is not None and int(val) == 0:
self.r.hdel(key, code)
def update_actual_volume(self, strategy_name, code, delta_vol):
"""成交回调时更新 Redis 实际股数"""
key = self._get_key(strategy_name)
new_vol = self.r.hincrby(key, code, int(delta_vol))
if new_vol <= 0:
self.r.hdel(key, code)
new_vol = 0
return new_vol
def get_position(self, strategy_name, code):
"""获取某个策略下某只股票的虚拟持仓"""
vol = self.r.hget(self._get_key(strategy_name), code)
return int(vol) if vol else 0
def get_holding_count(self, strategy_name):
"""获取当前策略已占用的槽位总数"""
return self.r.hlen(self._get_key(strategy_name))
def get_all_virtual_positions(self, strategy_name):
return self.r.hgetall(self._get_key(strategy_name))
def force_delete(self, strategy_name, code):
self.r.hdel(self._get_key(strategy_name), code)
def clean_stale_placeholders(self, strategy_name, xt_trader, acc):
"""清理长时间未成交且实盘无持仓的占位符"""
try:
key = self._get_key(strategy_name)
all_pos = self.r.hgetall(key)
if not all_pos: return
active_orders = xt_trader.query_stock_orders(acc, cancelable_only=True)
active_codes = [o.stock_code for o in active_orders] if active_orders else []
real_positions = xt_trader.query_stock_positions(acc)
real_holdings = [p.stock_code for p in real_positions if p.volume > 0] if real_positions else []
for code, vol_str in all_pos.items():
if int(vol_str) == 0:
if (code not in real_holdings) and (code not in active_codes):
self.r.hdel(key, code)
except Exception as e:
logging.getLogger("QMT_Engine").error(f"清理占位异常: {e}")
class DailySettlement:
"""收盘对账逻辑"""
def __init__(self, xt_trader, acc, pos_mgr, strategies_config):
self.trader = xt_trader
self.acc = acc
self.pos_mgr = pos_mgr
self.strategies_config = strategies_config
self.has_settled = False
def run_settlement(self):
"""收盘后强制同步 Redis 和实盘持仓"""
real_positions = self.trader.query_stock_positions(self.acc)
real_pos_map = {p.stock_code: p.volume for p in real_positions if p.volume > 0} if real_positions else {}
for strategy in self.strategies_config.keys():
virtual = self.pos_mgr.get_all_virtual_positions(strategy)
for code, v_str in virtual.items():
v = int(v_str)
if code not in real_pos_map:
self.pos_mgr.force_delete(strategy, code)
elif v == 0 and code in real_pos_map:
self.pos_mgr.update_actual_volume(strategy, code, real_pos_map[code])
self.has_settled = True
# ================= 2. QMT 核心引擎 =================
class MyXtQuantTraderCallback(XtQuantTraderCallback):
"""交易回调事件监听"""
def __init__(self, pos_mgr):
self.pos_mgr = pos_mgr
self.is_connected = False
self.logger = logging.getLogger("QMT_Engine")
def on_disconnected(self):
self.logger.warning(">> 回调通知: 交易端连接断开")
self.is_connected = False
def on_stock_trade(self, trade):
try:
# QMTEngine 是单例,可直接通过类访问
cache_info = QMTEngine().order_cache.get(trade.order_id)
if not cache_info: return
strategy, _, action = cache_info
self.logger.info(f">>> [成交] {strategy} | {trade.stock_code} | {trade.traded_volume}")
if action == 'BUY':
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, trade.traded_volume)
elif action == 'SELL':
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, -trade.traded_volume)
except:
traceback.print_exc()
def on_order_error(self, err):
try:
self.logger.error(f"下单失败回调: {err.error_msg} ID:{err.order_id}")
cache = QMTEngine().order_cache.get(err.order_id)
if cache and cache[2] == 'BUY':
self.pos_mgr.rollback_holding(cache[0], cache[1])
if err.order_id in QMTEngine().order_cache:
del QMTEngine().order_cache[err.order_id]
except:
pass
class QMTEngine:
"""QMT 交易引擎单例"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, '_initialized'): return
self.logger = None
self.config = {}
self.xt_trader = None
self.acc = None
self.pos_manager = None
self.callback = None
self.is_running = True
self.start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.last_heartbeat = "Initializing..."
self.order_cache = {} # OrderID -> (Strategy, Code, Action)
self.settler = None
self._initialized = True
def initialize(self, config_file='config.json'):
self._setup_logger()
self.config = self._load_config(config_file)
# 初始化 Redis
try:
self.redis_client = redis.Redis(**self.config['redis'], decode_responses=True)
self.redis_client.ping()
self.pos_manager = PositionManager(self.redis_client)
self.logger.info("Redis 建立连接成功")
except Exception as e:
self.logger.critical(f"Redis 连接失败: {e}")
raise
self._reconnect_qmt()
def _setup_logger(self):
log_dir = "logs"
if not os.path.exists(log_dir): os.makedirs(log_dir)
log_file = os.path.join(log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
self.logger = logging.getLogger("QMT_Engine")
self.logger.setLevel(logging.INFO)
if self.logger.handlers:
for h in self.logger.handlers[:]: h.close(); self.logger.removeHandler(h)
fmt = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(threadName)s] %(message)s', '%Y-%m-%d %H:%M:%S')
fh = logging.FileHandler(log_file, mode='a', encoding='utf-8')
fh.setFormatter(fmt)
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(fmt)
self.logger.addHandler(fh);
self.logger.addHandler(sh)
def _load_config(self, config_file):
base = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(
os.path.abspath(__file__))
path = os.path.join(base, config_file)
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def _get_global_total_slots(self):
"""本地引擎计算:所有策略总共分配了多少个槽位"""
return sum(info.get('total_slots', 0) for info in self.config.get('strategies', {}).values())
def _get_execution_setting(self, strategy_name, key, default=None):
"""扩展性配置读取:读取 execution 字典中的参数"""
strat_cfg = self.config.get('strategies', {}).get(strategy_name, {})
exec_cfg = strat_cfg.get('execution', {})
return exec_cfg.get(key, default)
def _reconnect_qmt(self):
q = self.config['qmt']
if self.xt_trader:
try:
self.xt_trader.stop()
except:
pass
self.xt_trader = XtQuantTrader(q['path'], int(time.time()))
self.acc = StockAccount(q['account_id'], q['account_type'])
self.callback = MyXtQuantTraderCallback(self.pos_manager)
self.xt_trader.register_callback(self.callback)
self.xt_trader.start()
if self.xt_trader.connect() == 0:
self.xt_trader.subscribe(self.acc)
self.callback.is_connected = True
self.settler = DailySettlement(self.xt_trader, self.acc, self.pos_manager, self.config['strategies'])
for s in self.config['strategies'].keys():
self.pos_manager.clean_stale_placeholders(s, self.xt_trader, self.acc)
self.logger.info("✅ QMT 终端连接成功")
return True
return False
def process_strategy_queue(self, strategy_name):
"""处理 Redis 中的策略信号"""
queue_key = f"{strategy_name}_real"
msg_json = self.redis_client.lpop(queue_key)
if not msg_json: return
try:
self.redis_client.rpush(f"{queue_key}:history", msg_json)
data = json.loads(msg_json)
if data.get('is_backtest'): return
today = datetime.date.today().strftime('%Y-%m-%d')
if data.get('timestamp', '').split(' ')[0] != today: return
action = data.get('action')
stock = data.get('stock_code')
price = float(data.get('price', 0))
msg_slots = int(data.get('total_slots', 0))
if action == 'BUY':
self._process_buy(strategy_name, stock, price, msg_slots)
elif action == 'SELL':
self._process_sell(strategy_name, stock, price)
except Exception as e:
self.logger.error(f"消息解析异常: {e}")
def _process_buy(self, strategy_name, stock_code, price, msg_slots):
"""核心开仓逻辑"""
# 1. 验证配置
strat_cfg = self.config.get('strategies', {}).get(strategy_name)
if not strat_cfg: return
local_slots = strat_cfg.get('total_slots', 0)
# 2. 安全校验:信号槽位与本地实盘配置必须严格一致
if msg_slots != local_slots:
self.logger.error(f"⚠️ [{strategy_name}] 槽位不匹配!拒绝下单。信号预期:{msg_slots} | 本地配置:{local_slots}")
return
# 3. 检查子策略占用
if self.pos_manager.get_holding_count(strategy_name) >= local_slots:
self.logger.warning(f"[{strategy_name}] 槽位已满,拦截买入 {stock_code}")
return
# 4. 资金计算(由本地引擎统筹全局)
try:
asset = self.xt_trader.query_stock_asset(self.acc)
global_total = self._get_global_total_slots()
if not asset or global_total <= 0: return
# 单笔预算 = (总资产现金 + 持仓市值) / 全局总槽位
total_equity = asset.cash + asset.market_value
target_amt = total_equity / global_total
# 实际可用金额不超过现金的 98%(预留滑点/手续费)
actual_amt = min(target_amt, asset.cash * 0.98)
if actual_amt < 2000:
self.logger.warning(f"[{strategy_name}] 可用金额不足2000取消买入 {stock_code}")
return
# --- 价格偏移处理 ---
offset = self._get_execution_setting(strategy_name, 'buy_price_offset', 0.0)
final_price = round(price + offset, 3)
vol = int(actual_amt / (final_price if final_price > 0 else 1.0) / 100) * 100
if vol < 100: return
oid = self.xt_trader.order_stock(self.acc, stock_code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE,
final_price, strategy_name, 'PyBuy')
if oid != -1:
self.logger.info(
f"√√√ [{strategy_name}] 开仓下单: {stock_code} | 价格:{final_price}(加价:{offset}) | 数量:{vol}")
self.order_cache[oid] = (strategy_name, stock_code, 'BUY')
self.pos_manager.mark_holding(strategy_name, stock_code)
else:
self.logger.error(f"XXX [{strategy_name}] 开仓发单拒绝")
except Exception as e:
self.logger.error(f"买入异常: {e}", exc_info=True)
def _process_sell(self, strategy_name, stock_code, price):
"""核心平仓逻辑"""
v_vol = self.pos_manager.get_position(strategy_name, stock_code)
if v_vol <= 0: return
real_pos = self.xt_trader.query_stock_positions(self.acc)
rp = next((p for p in real_pos if p.stock_code == stock_code), None) if real_pos else None
can_use = rp.can_use_volume if rp else 0
final_vol = min(v_vol, can_use)
if final_vol <= 0:
self.logger.warning(f"[{strategy_name}] {stock_code} 无可用平仓额度 (Redis:{v_vol}, 实盘:{can_use})")
return
# --- 价格偏移处理 ---
offset = self._get_execution_setting(strategy_name, 'sell_price_offset', 0.0)
final_price = round(price + offset, 3)
oid = self.xt_trader.order_stock(self.acc, stock_code, xtconstant.STOCK_SELL, final_vol, xtconstant.FIX_PRICE,
final_price, strategy_name, 'PySell')
if oid != -1:
self.logger.info(
f"√√√ [{strategy_name}] 平仓下单: {stock_code} | 价格:{final_price}(偏移:{offset}) | 数量:{final_vol}")
self.order_cache[oid] = (strategy_name, stock_code, 'SELL')
def run_trading_loop(self):
"""交易主线程循环"""
self.logger.info(">>> 交易主循环子线程已启动 <<<")
last_check = 0
while self.is_running:
try:
self.last_heartbeat = datetime.datetime.now().strftime('%H:%M:%S')
# 健康检查
if time.time() - last_check > 15:
last_check = time.time()
try:
if not (self.xt_trader and self.acc and self.xt_trader.query_stock_asset(self.acc)):
self._reconnect_qmt()
except:
self._reconnect_qmt()
# 交易时间判断
curr = datetime.datetime.now().strftime('%H%M%S')
is_trading = ('091500' <= curr <= '113000') or ('130000' <= curr <= '150000')
if is_trading and self.callback and self.callback.is_connected:
if self.settler: self.settler.reset_flag()
for s in self.config.get('strategies', {}).keys():
self.process_strategy_queue(s)
elif '150500' <= curr <= '151500' and self.settler and not self.settler.has_settled:
self.settler.run_settlement()
time.sleep(1 if is_trading else 5)
except Exception as e:
self.logger.error(f"主循环异常: {e}")
time.sleep(10)
# ================= 外部接口 =================
def get_status(self) -> QMTStatus:
conn = self.callback.is_connected if self.callback else False
return QMTStatus(conn, self.start_time, self.last_heartbeat,
self.acc.account_id if self.acc else "Unknown", self.is_running)
def get_positions(self) -> Dict[str, Any]:
real = []
if self.callback and self.callback.is_connected:
pos = self.xt_trader.query_stock_positions(self.acc)
if pos:
real = [{"code": p.stock_code, "volume": p.volume, "can_use": p.can_use_volume, "value": p.market_value}
for p in pos if p.volume > 0]
virtual = {s: self.pos_manager.get_all_virtual_positions(s) for s in self.config.get('strategies', {}).keys()}
return {"real_positions": real, "virtual_positions": virtual}
def get_logs(self, lines=50):
log_path = os.path.join("logs", f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
if not os.path.exists(log_path): return ["今日暂无日志"]
with open(log_path, 'r', encoding='utf-8') as f:
return [l.strip() for l in f.readlines()[-lines:]]
def stop(self):
self.is_running = False
self.logger.info("收到引擎停止指令")

View File

@@ -1,4 +1,5 @@
from xtquant import xttrader
from xtquant.xtdata import download_history_data, get_market_data
from xtquant.xttype import StockAccount
import random
@@ -31,5 +32,5 @@ else:
print('订阅失败')
asset = xt_trader.query_stock_asset(account)
print(asset.cash)
download_history_data('000001.SZ', '1m', start_time='20251201', end_time='')
print(get_market_data(stock_list=['000001.SZ'], period='1m', start_time='20251201', end_time=''))

64
qmt/run.py Normal file
View File

@@ -0,0 +1,64 @@
# coding:utf-8
"""
QMT交易系统启动器
用于直接运行,避免包导入问题
"""
import sys
import os
# 将当前目录添加到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
# 导入模块
from qmt_engine import QMTEngine
from api_server import create_api_server
import threading
import uvicorn
def main():
"""主函数 - 启动QMT交易引擎和API服务器"""
print(">>> 系统正在启动...")
# 创建QMT引擎实例
engine = QMTEngine()
try:
# 初始化引擎
engine.initialize('config.json')
print("✅ QMT引擎初始化成功")
except Exception as e:
print(f"❌ QMT引擎初始化失败: {e}")
sys.exit(1)
# 启动交易线程
trading_thread = threading.Thread(target=engine.run_trading_loop, daemon=True)
trading_thread.start()
print("✅ 交易线程启动成功")
# 创建API服务器
app = create_api_server(engine)
print("✅ API服务器创建成功")
# 启动Web服务
print(">>> Web服务启动: http://localhost:8001")
try:
uvicorn.run(
app,
host="0.0.0.0",
port=8001,
log_level="warning",
access_log=False
)
except KeyboardInterrupt:
print("\n>>> 正在关闭系统...")
engine.stop()
print(">>> 系统已关闭")
if __name__ == '__main__':
# 使用 -u 参数运行是最佳实践: python -u run.py
# 但这里也在代码里强制 flush 了
main()

View File

@@ -1,75 +1,57 @@
@echo off
setlocal enabledelayedexpansion
:: ================= <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> =================
:: <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ¼ (<28><>ȷ<EFBFBD><C8B7>·<EFBFBD><C2B7><EFBFBD><EFBFBD>ȷ)
set "WORK_DIR=C:\Data\Project\NewStock\qmt"
:: Python<EFBFBD>ű<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
set "SCRIPT_NAME=qmt_trader.py"
:: <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><EFBFBD><EFBFBD>
set MAX_RETRIES=5
:: <20><><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD><EFBFBD>
:: ================= 配置选项 =================
:: 1. 自动获取当前脚本所在目录作为工作目录
set "WORK_DIR=%~dp0"
:: 2. 设置启动文件(对应你拆分后的入口文件)
set "SCRIPT_NAME=run.py"
:: 3. 失败重启配置
set MAX_RETRIES=10
set RETRY_COUNT=0
:: <20><><EFBFBD>Եȴ<D4B5>ʱ<EFBFBD><CAB1>(<28><>)
set RETRY_WAIT=10
:: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־Ŀ¼
set "LOG_DIR=%WORK_DIR%\logs\launcher"
set RETRY_WAIT=15
:: 4. 日志配置
set "LOG_DIR=%WORK_DIR%logs\launcher"
:: ===========================================
:: 1. <20>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD>Ŀ¼
:: 切换到工作目录
cd /d "%WORK_DIR%"
title QMT ʵ<EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD>ϵͳ [Port:8001]
title QMT 自动化交易系统 [监控中]
:: 2. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־Ŀ¼
:: 创建日志目录
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
:: <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>־<EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD> (<28>򵥵<EFBFBD><F2B5A5B5><EFBFBD><EFBFBD>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E4B3A3>Windows<77><73>ʽ)
:: 获取当前日期用于日志命名
set "TODAY=%date:~0,4%-%date:~5,2%-%date:~8,2%"
set "LOG_FILE=%LOG_DIR%\%TODAY%.log"
set "LOG_FILE=%LOG_DIR%\launcher_%TODAY%.log"
cls
echo ==================================================
echo QMT ʵ<EFBFBD>̽<EFBFBD><EFBFBD><EFBFBD>ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
echo ʱ<EFBFBD><EFBFBD>: %time%
echo <EFBFBD><EFBFBD>־: %LOG_FILE%
echo <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: http://localhost:8001
echo QMT 交易系统守护进程启动
echo 工作目录: %WORK_DIR%
echo 启动文件: %SCRIPT_NAME%
echo 监控地址: http://localhost:8001
echo 日志文件: %LOG_FILE%
echo ==================================================
:LOOP
echo.
echo [%time%] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӽ<EFBFBD><D3BD><EFBFBD>...
echo [%time%] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӽ<EFBFBD><D3BD><EFBFBD>... >> "%LOG_FILE%"
:: 3. <20><><EFBFBD><EFBFBD> Python <20>ű<EFBFBD>
:: ʹ<><CAB9> uv run <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2>&1 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҳд<D2B2><D0B4><EFBFBD><EFBFBD>־
uv run %SCRIPT_NAME% >> "%LOG_FILE%" 2>&1
:: 4. <20><><EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD>
set EXIT_CODE=%errorlevel%
echo [%time%] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD><CBB3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: %EXIT_CODE% >> "%LOG_FILE%"
echo <20><><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD> (Code: %EXIT_CODE%)
:: 5. <20><><EFBFBD><EFBFBD><EFBFBD>߼<EFBFBD>
:: 检查重试次数
if %RETRY_COUNT% GEQ %MAX_RETRIES% (
echo [%time%] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵͳֹͣ<EFBFBD><EFBFBD> >> "%LOG_FILE%"
:: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ
msg * "QMT <20><><EFBFBD><EFBFBD>ϵͳ<CFB5>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>޷<EFBFBD><DEB7>Զ<EFBFBD><D4B6>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><D6BE>"
echo [%time%] !!! 达到最大重试次数,系统停止重启 !!! >> "%LOG_FILE%"
color 0C
echo 错误: 系统多次崩溃,请检查日志后手动重启。
msg * "QMT 系统异常崩溃且无法自动恢复,请检查日志!"
goto FAIL
)
set /a RETRY_COUNT+=1
echo [%time%] <EFBFBD>ȴ<EFBFBD> %RETRY_WAIT% <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD> %RETRY_COUNT% <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>... >> "%LOG_FILE%"
echo <20><><EFBFBD>ڵȴ<DAB5><C8B4><EFBFBD><EFBFBD><EFBFBD> (%RETRY_COUNT%/%MAX_RETRIES%)...
timeout /t %RETRY_WAIT% >nul
echo [%time%] 正在启动交易引擎 (第 %RETRY_COUNT% 次重启)...
echo [%time%] >>> 启动子进程: uv run %SCRIPT_NAME% >> "%LOG_FILE%"
goto LOOP
:: 启动程序 (使用 -u 参数确保输出实时刷新到日志)
uv run python -u %SCRIPT_NAME% >> "%LOG_FILE%" 2>&1
:FAIL
title QMT ʵ<><CAB5><EFBFBD>ػ<EFBFBD>ϵͳ [<5B>ѱ<EFBFBD><D1B1><EFBFBD>]
color 4F
echo.
echo ==========================================
echo ϵͳ<CFB5><CDB3>ֹͣ<CDA3><D6B9><EFBFBD><EFBFBD>
echo <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD>ļ<EFBFBD>: %LOG_FILE%
echo ==========================================
pause
exit /b 1
:: 程序退出逻辑
set EXIT_CODE=%errorlevel%
if %EXIT_CODE% EQU 0 (
echo [%time%] 程序正常关闭。 >> "%LOG_FILE%"
echo 程序运行结束。