更新qmt代码,支持多端qmt登录

This commit is contained in:
2026-01-10 04:06:35 +08:00
parent dd60589280
commit 50ee1a5a0a
6 changed files with 487 additions and 495 deletions

View File

@@ -8,37 +8,46 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from qmt_engine import QMTEngine, QMTStatus
# 导入新的管理器类
from qmt_engine import MultiEngineManager, TerminalStatus
# ================= Pydantic模型 =================
class StatusResponse(BaseModel):
"""状态响应模型"""
running: bool
qmt_connected: bool
start_time: str
last_loop_update: str
account_id: str
class TerminalStatusModel(BaseModel):
"""单个终端状态模型"""
qmt_id: str
alias: str
account_id: str
is_connected: bool
last_heartbeat: str
class StatusResponse(BaseModel):
"""全局状态响应模型"""
running: bool
start_time: str
terminals: List[TerminalStatusModel]
class PositionsResponse(BaseModel):
"""持仓响应模型"""
real_positions: List[Dict[str, Any]]
# 按 qmt_id 分组的实盘持仓
real_positions: Dict[str, 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
class QMTAPIServer:
"""多终端 QMT API服务器"""
def __init__(self, manager: MultiEngineManager):
self.app = FastAPI(title="QMT Multi-Terminal Monitor")
self.manager = manager
self._setup_middleware()
self._setup_routes()
@@ -61,41 +70,78 @@ class QMTAPIServer:
return FileResponse("dashboard.html")
return {"error": "Dashboard not found"}
@self.app.get("/api/status", response_model=StatusResponse, summary="获取系统状态")
@self.app.get("/api/status", response_model=StatusResponse, summary="获取所有终端状态")
def get_status():
"""获取QMT连接状态和系统信息"""
status = self.qmt_engine.get_status()
"""获取所有 QMT 终端的连接状态"""
terminal_data = self.manager.get_all_status()
terminals = [
TerminalStatusModel(
qmt_id=t.qmt_id,
alias=t.alias,
account_id=t.account_id,
is_connected=t.is_connected,
last_heartbeat=t.last_heartbeat
) for t in terminal_data
]
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
running=self.manager.is_running,
start_time=self.manager.start_time,
terminals=terminals
)
@self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息")
def get_positions():
"""获取实盘和虚拟持仓信息"""
positions = self.qmt_engine.get_positions()
"""汇总所有终端的实盘持仓和所有策略的虚拟持仓"""
real_pos_data = {}
virtual_pos_data = {}
# 1. 遍历所有终端单元获取实盘持仓
for qmt_id, unit in self.manager.units.items():
positions = []
if unit.callback and unit.callback.is_connected:
try:
xt_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
if xt_pos:
positions = [
{
"code": p.stock_code,
"volume": p.volume,
"can_use": p.can_use_volume,
"market_value": round(p.market_value, 2)
} for p in xt_pos if p.volume > 0
]
except:
pass
real_pos_data[qmt_id] = positions
# 2. 遍历所有策略获取虚拟持仓
for s_name in self.manager.config.get('strategies', {}).keys():
v_data = self.manager.pos_manager.get_all_virtual_positions(s_name)
virtual_pos_data[s_name] = v_data
return PositionsResponse(
real_positions=positions["real_positions"],
virtual_positions=positions["virtual_positions"]
real_positions=real_pos_data,
virtual_positions=virtual_pos_data
)
@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)
@self.app.get("/api/logs", response_model=LogsResponse, summary="获取系统日志")
def get_logs(lines: int = Query(50, ge=1, le=1000)):
"""获取最近的系统运行日志"""
logs = self.manager.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:
"""健康检查:只要有一个终端在线即视为正常"""
terminal_data = self.manager.get_all_status()
any_connected = any(t.is_connected for t in terminal_data)
if self.manager.is_running and any_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')}
return {"status": "unhealthy", "reason": "No terminals connected", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
def get_app(self) -> FastAPI:
"""获取FastAPI应用实例"""
@@ -103,7 +149,8 @@ class QMTAPIServer:
# ================= 辅助函数 =================
def create_api_server(qmt_engine: QMTEngine) -> FastAPI:
"""创建API服务器"""
server = QMTAPIServer(qmt_engine)
def create_api_server(manager: MultiEngineManager) -> FastAPI:
"""创建API服务器入口"""
server = QMTAPIServer(manager)
return server.get_app()

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QMT 实盘监控看板</title>
<title>QMT 多终端监控看板</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/element-plus"></script>
@@ -13,23 +13,18 @@
.card-header { display: flex; justify-content: space-between; align-items: center; }
.box-card { margin-bottom: 20px; }
.log-box {
background: #1e1e1e;
color: #d4d4d4;
padding: 10px;
border-radius: 4px;
height: 400px;
overflow-y: scroll;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px;
height: 350px; overflow-y: scroll; font-family: 'Consolas', monospace;
font-size: 12px; line-height: 1.5;
}
.log-line { margin: 0; border-bottom: 1px solid #333; white-space: pre-wrap; word-break: break-all; }
.log-line:hover { background-color: #2a2a2a; }
.virtual-item { margin-bottom: 20px; border-left: 4px solid #409EFF; padding-left: 10px; }
.virtual-title { font-weight: bold; font-size: 14px; margin-bottom: 8px; color: #606266; }
.terminal-group { margin-bottom: 15px; border: 1px solid #ebeef5; border-radius: 8px; padding: 10px; background: #fff; }
.terminal-title { font-weight: bold; font-size: 14px; margin-bottom: 8px; color: #409EFF; display: flex; align-items: center; }
.status-badge { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }
.bg-green { background-color: #67C23A; }
.bg-red { background-color: #F56C6C; }
.bg-gray { background-color: #909399; }
.virtual-item { margin-bottom: 15px; border-left: 4px solid #E6A23C; padding-left: 10px; }
</style>
</head>
<body>
@@ -41,86 +36,100 @@
<div class="card-header">
<div style="display:flex; align-items:center;">
<el-icon size="24" style="margin-right: 10px;"><Monitor /></el-icon>
<span style="font-weight: bold; font-size: 20px;">QMT 实盘守护系统</span>
<span style="font-weight: bold; font-size: 20px;">QMT 多账号实盘守护系统</span>
</div>
<div>
<el-tag :type="status.running ? 'success' : 'info'" effect="dark" style="margin-right: 10px;">
API: {{ status.running ? 'Running' : 'Offline' }}
</el-tag>
<el-tag :type="status.qmt_connected ? 'success' : 'danger'" effect="dark">
QMT: {{ status.qmt_connected ? 'Connected' : 'Disconnected' }}
系统: {{ status.running ? '运行中' : '离线' }}
</el-tag>
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
</div>
</div>
</template>
<el-descriptions border :column="4" size="large">
<el-descriptions-item label="资金账号">{{ status.account_id || '---' }}</el-descriptions-item>
<el-descriptions-item label="启动时间">{{ status.start_time || '---' }}</el-descriptions-item>
<el-descriptions-item label="心跳时间">
<span :style="{color: isHeartbeatStalled ? 'red' : 'green', fontWeight: 'bold'}">
{{ status.last_loop_update || '---' }}
</span>
</el-descriptions-item>
<el-descriptions-item label="控制">
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
<div style="margin-left: 15px; display: inline-flex; align-items: center;">
<el-checkbox v-model="autoRefresh" label="自动刷新(1min)" border></el-checkbox>
<span style="font-size: 12px; margin-left: 8px; color: #909399;">
{{ tradingStatusText }}
</span>
<!-- 终端状态概览 -->
<el-row :gutter="20">
<el-col :span="6" v-for="t in status.terminals" :key="t.qmt_id">
<el-card shadow="never" style="background: #fcfcfc;">
<div style="font-size: 14px; font-weight: bold; margin-bottom: 5px;">{{ t.alias }}</div>
<div style="font-size: 12px; color: #909399;">ID: {{ t.account_id }}</div>
<div style="margin-top: 8px; display: flex; align-items: center; justify-content: space-between;">
<el-tag :type="t.is_connected ? 'success' : 'danger'" size="small">
{{ t.is_connected ? '已连接' : '已断开' }}
</el-tag>
<span style="font-size: 11px; color: #c0c4cc;">{{ t.last_heartbeat }}</span>
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="6">
<div style="padding: 10px;">
<div style="font-size: 12px; color: #909399;">启动时间: {{ status.start_time }}</div>
<div style="margin-top: 10px;">
<el-checkbox v-model="autoRefresh" label="自动刷新 (1min)" border size="small"></el-checkbox>
<span style="font-size: 12px; margin-left: 8px; color: #E6A23C;">{{ tradingStatusText }}</span>
</div>
</div>
</el-col>
</el-row>
</el-card>
</el-header>
<el-main style="padding: 0;">
<el-row :gutter="20">
<el-col :span="12">
<!-- 左侧:多账号实盘持仓 -->
<el-col :span="13">
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><span class="status-badge bg-green"></span>实盘真实持仓 (QMT)</span>
<span><el-icon><Suitcase /></el-icon> 实盘真实持仓 (按终端分组)</span>
</div>
</template>
<el-table :data="positions.real_positions" style="width: 100%" border stripe size="small" empty-text="当前空仓">
<el-table-column prop="code" label="代码" width="100" sortable></el-table-column>
<el-table-column prop="volume" label="总持仓" width="100"></el-table-column>
<el-table-column prop="can_use" label="可用" width="100"></el-table-column>
<div v-if="Object.keys(positions.real_positions).length === 0" style="text-align:center; padding:20px; color:#909399;">暂无持仓数据</div>
<div v-for="(posList, qmtId) in positions.real_positions" :key="qmtId" class="terminal-group">
<div class="terminal-title">
<span class="status-badge" :class="getTerminalStatusClass(qmtId)"></span>
{{ getTerminalAlias(qmtId) }}
</div>
<el-table :data="posList" style="width: 100%" border size="small" empty-text="当前空仓">
<el-table-column prop="code" label="代码" width="100"></el-table-column>
<el-table-column prop="volume" label="持仓" width="90"></el-table-column>
<el-table-column prop="can_use" label="可用" width="90"></el-table-column>
<el-table-column prop="market_value" label="市值"></el-table-column>
</el-table>
</div>
</el-card>
</el-col>
<el-col :span="12">
<!-- 右侧Redis 虚拟账本 -->
<el-col :span="11">
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><span class="status-badge bg-gray"></span>Redis 虚拟账本 (策略隔离)</span>
<span><el-icon><Memo /></el-icon> Redis 虚拟账本 (策略隔离)</span>
</div>
</template>
<div v-if="Object.keys(positions.virtual_positions).length === 0" style="color:#909399; text-align:center; padding: 20px;">
暂无策略数据 / Redis未连接
</div>
<div v-for="(posObj, strategyName) in positions.virtual_positions" :key="strategyName" class="virtual-item">
<div class="virtual-title">{{ strategyName }}</div>
<el-table :data="formatVirtual(posObj)" style="width: 100%;" border size="small" empty-text="该策略当前空仓">
<el-table-column prop="code" label="代码"></el-table-column>
<el-table-column prop="vol" label="记账数量">
<template #default="scope"><span style="font-weight: bold;">{{ scope.row.vol }}</span></template>
</el-table-column>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:5px;">
<span style="font-weight:bold; font-size:13px; color:#606266;">{{ strategyName }}</span>
<el-tag size="small" type="warning">虚拟占位: {{ Object.keys(posObj).length }}</el-tag>
</div>
<el-table :data="formatVirtual(posObj)" style="width: 100%;" border size="small">
<el-table-column prop="code" label="股票代码"></el-table-column>
<el-table-column prop="vol" label="记账数量"></el-table-column>
</el-table>
</div>
</el-card>
</el-col>
</el-row>
<!-- 底部:日志 -->
<el-row>
<el-col :span="24">
<el-card class="box-card" shadow="never">
<el-card class="box-card">
<template #header>
<div class="card-header"><span>系统实时日志 (Last 50 lines)</span></div>
<div class="card-header"><span>系统实时日志 (最新 50 条)</span></div>
</template>
<div class="log-box" ref="logBox">
<div v-for="(line, index) in logs" :key="index" class="log-line">{{ line }}</div>
@@ -134,122 +143,92 @@
<script>
const { createApp, ref, onMounted, onUnmounted, computed } = Vue;
const { Monitor, Refresh } = ElementPlusIconsVue;
const { Monitor, Refresh, Suitcase, Memo } = ElementPlusIconsVue;
const app = createApp({
setup() {
const status = ref({});
const positions = ref({ real_positions: [], virtual_positions: {} });
const status = ref({ running: false, terminals: [], start_time: "" });
const positions = ref({ real_positions: {}, virtual_positions: {} });
const logs = ref([]);
const autoRefresh = ref(true); // 默认开启自动刷新
const autoRefresh = ref(true);
const loading = ref(false);
const logBox = ref(null);
let timer = null;
const API_BASE = "";
// === 核心逻辑修改:判断是否为交易时间 ===
const isTradingTime = () => {
const now = new Date();
const day = now.getDay();
const hour = now.getHours();
const minute = now.getMinutes();
const currentTimeVal = hour * 100 + minute;
// 1. 判断是否为周末 (0是周日, 6是周六)
if (day === 0 || day === 6) return false;
// 2. 判断时间段 (09:00 - 15:10)
// 包含集合竞价和收盘清算时间
if (currentTimeVal >= 900 && currentTimeVal <= 1510) {
return true;
}
return false;
const val = now.getHours() * 100 + now.getMinutes();
return (val >= 900 && val <= 1515);
};
// 界面显示的提示文本
const tradingStatusText = computed(() => {
if (!autoRefresh.value) return "已关闭";
return isTradingTime() ? "监控中..." : "休市暂停";
});
const isHeartbeatStalled = computed(() => {
if (!status.value.last_loop_update) return true;
return false;
return isTradingTime() ? "市场开放中" : "非交易时段";
});
const fetchData = async () => {
loading.value = true;
try {
const resStatus = await fetch(`${API_BASE}/api/status`);
if(resStatus.ok) status.value = await resStatus.json();
else status.value = { running: false };
const sRes = await fetch(`${API_BASE}/api/status`);
if(sRes.ok) status.value = await sRes.json();
const resPos = await fetch(`${API_BASE}/api/positions`);
if(resPos.ok) positions.value = await resPos.json();
const pRes = await fetch(`${API_BASE}/api/positions`);
if(pRes.ok) positions.value = await pRes.json();
const resLogs = await fetch(`${API_BASE}/api/logs`);
if(resLogs.ok) {
const logData = await resLogs.json();
const needScroll = (logs.value.length !== logData.logs.length);
logs.value = logData.logs;
// 只有在自动刷新且有新日志时才自动滚动
if (needScroll && autoRefresh.value) {
const lRes = await fetch(`${API_BASE}/api/logs`);
if(lRes.ok) {
const data = await lRes.json();
logs.value = data.logs;
setTimeout(() => {
if(logBox.value) logBox.value.scrollTop = logBox.value.scrollHeight;
}, 100);
}
}
} catch (e) {
console.error("API Error:", e);
status.value.running = false;
console.error("Fetch Error:", e);
} finally {
loading.value = false;
}
};
// 手动刷新按钮:不受时间限制
const manualRefresh = () => {
fetchData();
const getTerminalAlias = (qmtId) => {
const t = status.value.terminals.find(x => x.qmt_id === qmtId);
return t ? t.alias : qmtId;
};
const getTerminalStatusClass = (qmtId) => {
const t = status.value.terminals.find(x => x.qmt_id === qmtId);
return t && t.is_connected ? 'bg-green' : 'bg-red';
};
const formatVirtual = (obj) => {
if (!obj) return [];
return Object.keys(obj).map(key => ({ code: key, vol: obj[key] }));
return Object.keys(obj).map(k => ({ code: k, vol: obj[k] }));
};
const manualRefresh = () => fetchData();
onMounted(() => {
// 页面加载时先拉取一次
fetchData();
// === 修改定时器:每 60 秒触发一次 ===
timer = setInterval(() => {
// 只有在 "开关开启" 且 "处于交易时间" 时才请求
if (autoRefresh.value && isTradingTime()) {
fetchData();
}
}, 60000); // 60000ms = 1分钟
if (autoRefresh.value && isTradingTime()) fetchData();
}, 60000);
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
onUnmounted(() => { if (timer) clearInterval(timer); });
return {
status, positions, logs, autoRefresh, loading, logBox,
manualRefresh, // 绑定到按钮
fetchData,
formatVirtual,
isHeartbeatStalled,
tradingStatusText, // 绑定到提示文本
Monitor, Refresh
manualRefresh, formatVirtual, tradingStatusText,
getTerminalAlias, getTerminalStatusClass,
Monitor, Refresh, Suitcase, Memo
};
}
});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
app.component(key, component);
}
app.use(ElementPlus);
app.mount('#app');

View File

@@ -16,32 +16,27 @@ from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant
# ================= 0. Windows 控制台防卡死补丁 =================
# ================= 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
class TerminalStatus:
"""终端实例状态封装"""
qmt_id: str
alias: str
account_id: str
is_running: bool
is_connected: bool
last_heartbeat: str
# ================= 1. 虚拟持仓与对账辅助类 =================
# ================= 1. 业务逻辑辅助类 =================
class PositionManager:
"""Redis 持仓管理器:负责维护每个子策略的虚拟仓位"""
"""Redis 虚拟持仓管理(全局单例)"""
def __init__(self, r_client):
self.r = r_client
@@ -49,18 +44,15 @@ class PositionManager:
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:
@@ -69,12 +61,10 @@ class PositionManager:
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):
@@ -83,98 +73,120 @@ class PositionManager:
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
"""终端级别的日终对账"""
def __init__(self, unit):
self.unit = unit
self.has_settled = False
def run_settlement(self):
"""收盘后强制同步 Redis 和实盘持仓"""
real_positions = self.trader.query_stock_positions(self.acc)
trader = self.unit.xt_trader
acc = self.unit.acc_obj
if not trader: return
real_positions = trader.query_stock_positions(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)
manager = MultiEngineManager()
strategies = manager.get_strategies_by_terminal(self.unit.qmt_id)
for s_name in strategies:
virtual = manager.pos_manager.get_all_virtual_positions(s_name)
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])
manager.pos_manager.force_delete(s_name, code)
elif int(v_str) == 0 and code in real_pos_map:
manager.pos_manager.update_actual_volume(s_name, code, real_pos_map[code])
self.has_settled = True
def reset_flag(self):
"""重置结算标志,以便第二天重新执行"""
self.has_settled = False
# ================= 2. 执行单元 (TradingUnit) =================
# ================= 2. QMT 核心引擎 =================
class MyXtQuantTraderCallback(XtQuantTraderCallback):
"""交易回调事件监听"""
def __init__(self, pos_mgr):
self.pos_mgr = pos_mgr
class UnitCallback(XtQuantTraderCallback):
def __init__(self, unit):
self.unit = unit
self.is_connected = False
self.logger = logging.getLogger("QMT_Engine")
def on_disconnected(self):
self.logger.warning(">> 回调通知: 交易端连接断开")
logging.getLogger("QMT_Engine").warning(f"终端 {self.unit.alias}({self.unit.qmt_id}) 物理连接断开")
self.is_connected = False
def on_stock_trade(self, trade):
try:
# QMTEngine 是单例,可直接通过类访问
cache_info = QMTEngine().order_cache.get(trade.order_id)
cache_info = self.unit.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}")
s_name, _, action = cache_info
manager = MultiEngineManager()
if action == 'BUY':
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, trade.traded_volume)
manager.pos_manager.update_actual_volume(s_name, trade.stock_code, trade.traded_volume)
elif action == 'SELL':
self.pos_mgr.update_actual_volume(strategy, trade.stock_code, -trade.traded_volume)
manager.pos_manager.update_actual_volume(s_name, trade.stock_code, -trade.traded_volume)
except:
traceback.print_exc()
logging.getLogger("QMT_Engine").error(traceback.format_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)
cache = self.unit.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]
MultiEngineManager().pos_manager.rollback_holding(cache[0], cache[1])
self.unit.order_cache.pop(err.order_id, None)
class TradingUnit:
"""终端实例执行单元,负责管理单个 QMT 进程"""
def __init__(self, t_cfg):
self.qmt_id = t_cfg['qmt_id']
self.alias = t_cfg.get('alias', self.qmt_id)
self.path = t_cfg['path']
self.account_id = t_cfg['account_id']
self.account_type = t_cfg['account_type']
self.xt_trader = None
self.acc_obj = None
self.callback = None
self.settler = None
self.order_cache = {}
self.last_heartbeat = "N/A"
def cleanup(self):
"""强制销毁资源,确保文件句柄释放"""
if self.xt_trader:
try:
logging.getLogger("QMT_Engine").info(f"正在销毁终端 {self.alias} 的旧资源...")
self.xt_trader.stop()
self.xt_trader = None # 显式置空
self.callback = None
time.sleep(1.5) # 给 C++ 引擎留出释放 down_queue 锁的时间
except:
pass
def connect(self):
"""连接 QMT 终端"""
self.cleanup() # 启动前先执行清理
try:
# 采用动态 Session ID 避免冲突
session_id = int(time.time()) + hash(self.qmt_id) % 1000
self.xt_trader = XtQuantTrader(self.path, session_id)
self.acc_obj = StockAccount(self.account_id, self.account_type)
self.callback = UnitCallback(self)
class QMTEngine:
"""QMT 交易引擎单例"""
self.xt_trader.register_callback(self.callback)
self.xt_trader.start()
res = self.xt_trader.connect()
if res == 0:
self.xt_trader.subscribe(self.acc_obj)
self.callback.is_connected = True
self.settler = DailySettlement(self)
logging.getLogger("QMT_Engine").info(f"终端 {self.alias} 连接成功 (SID: {session_id})")
return True
return False
except Exception as e:
logging.getLogger("QMT_Engine").error(f"终端 {self.alias} 连接异常: {repr(e)}")
return False
# ================= 3. 总控中心 (MultiEngineManager) =================
class MultiEngineManager:
_instance = None
_lock = threading.Lock()
@@ -187,244 +199,196 @@ class QMTEngine:
def __init__(self):
if hasattr(self, '_initialized'): return
self.logger = None
self.units: Dict[str, TradingUnit] = {}
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()
with open(config_file, 'r', encoding='utf-8') as f:
self.config = json.load(f)
self.r = redis.Redis(**self.config['redis'], decode_responses=True)
self.pos_manager = PositionManager(self.r)
for t_cfg in self.config.get('qmt_terminals', []):
unit = TradingUnit(t_cfg)
unit.connect()
self.units[unit.qmt_id] = unit
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')
logger = logging.getLogger("QMT_Engine")
logger.setLevel(logging.INFO)
# 确保日志流为 UTF-8
fmt = logging.Formatter('[%(asctime)s] [%(threadName)s] %(message)s', '%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)
logger.addHandler(fh)
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 get_strategies_by_terminal(self, qmt_id):
return [s for s, cfg in self.config['strategies'].items() if cfg.get('qmt_id') == qmt_id]
def run_trading_loop(self):
"""交易主线程循环"""
self.logger.info(">>> 交易主循环线程已启动 <<<")
self.logger = logging.getLogger("QMT_Engine")
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()
now_t = time.time()
curr_hms = datetime.datetime.now().strftime('%H%M%S')
# --- 健康检查与自动修复 ---
if now_t - last_check > 25:
last_check = now_t
for unit in self.units.values():
is_unit_alive = False
if unit.xt_trader and unit.acc_obj:
try:
if not (self.xt_trader and self.acc and self.xt_trader.query_stock_asset(self.acc)):
self._reconnect_qmt()
# 物理探测:通过查资产确认连接有效性
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
if asset:
is_unit_alive = True
unit.last_heartbeat = datetime.datetime.now().strftime('%H:%M:%S')
# 状态修正物理通但逻辑False时自动拉回
if unit.callback and not unit.callback.is_connected:
unit.callback.is_connected = True
self.logger.info(f"✅ 修正终端 {unit.alias} 状态为在线")
except:
self._reconnect_qmt()
is_unit_alive = False
# 交易时间判断
curr = datetime.datetime.now().strftime('%H%M%S')
is_trading = ('091500' <= curr <= '113000') or ('130000' <= curr <= '150000')
# 断线重连策略
if not is_unit_alive:
# 避让 QMT 夜间重启高峰 (21:32 - 21:50)
if not ('213200' <= curr_hms <= '215000'):
self.logger.warning(f"🚫 终端 {unit.alias} 物理连接丢失,执行重连...")
unit.connect()
else:
self.logger.info(f"⏳ 处于 QMT 重启时段 ({curr_hms}),跳过重连操作...")
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()
# --- 交易逻辑处理 ---
is_trading = ('091500' <= curr_hms <= '113030') or ('130000' <= curr_hms <= '150030')
if is_trading:
for s_name in self.config['strategies'].keys():
self.process_route(s_name)
# --- 收盘结算与标志位重置 ---
elif '150500' <= curr_hms <= '151500':
for unit in self.units.values():
if unit.settler and not unit.settler.has_settled:
unit.settler.run_settlement()
elif '153000' <= curr_hms <= '160000':
for unit in self.units.values():
if unit.settler: unit.settler.reset_flag()
time.sleep(1 if is_trading else 5)
except Exception as e:
self.logger.error(f"主循环异常: {e}")
except:
self.logger.error("主循环异常")
self.logger.error(traceback.format_exc())
time.sleep(10)
# ================= 外部接口 =================
def process_route(self, strategy_name):
strat_cfg = self.config['strategies'].get(strategy_name)
unit = self.units.get(strat_cfg.get('qmt_id'))
if not unit or not unit.callback or not unit.callback.is_connected: return
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)
msg_json = self.r.lpop(f"{strategy_name}_real")
if not msg_json: return
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}
try:
data = json.loads(msg_json)
# 严格校验消息日期
if data.get('timestamp', '').split(' ')[0] != datetime.date.today().strftime('%Y-%m-%d'):
return
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:]]
if data['action'] == 'BUY':
self._execute_buy(unit, strategy_name, data)
elif data['action'] == 'SELL':
self._execute_sell(unit, strategy_name, data)
except:
pass
def _execute_buy(self, unit, strategy_name, data):
strat_cfg = self.config['strategies'][strategy_name]
# 1. 槽位校验
if data['total_slots'] != strat_cfg['total_slots']:
self.logger.error(f"[{strategy_name}] 信号槽位({data['total_slots']})与配置({strat_cfg['total_slots']})不符")
return
# 2. 持仓数检查
if self.pos_manager.get_holding_count(strategy_name) >= strat_cfg['total_slots']:
return
try:
asset = unit.xt_trader.query_stock_asset(unit.acc_obj)
# 计算该终端的总槽位之和
terminal_strategies = self.get_strategies_by_terminal(unit.qmt_id)
total_slots = sum(self.config['strategies'][s]['total_slots'] for s in terminal_strategies)
if not asset or total_slots <= 0: return
# 3. 资金等权分配 (基于该终端总资产)
total_equity = asset.cash + asset.market_value
target_amt = total_equity / total_slots
actual_amt = min(target_amt, asset.cash * 0.98) # 预留手续费滑点
if actual_amt < 2000:
self.logger.warning(f"[{strategy_name}] 单笔预算 {actual_amt:.2f} 不足 2000 元,取消买入")
return
# 4. 价格与股数
offset = strat_cfg.get('execution', {}).get('buy_price_offset', 0.0)
price = round(float(data['price']) + offset, 3)
vol = int(actual_amt / (price if price > 0 else 1.0) / 100) * 100
if vol < 100: return
oid = unit.xt_trader.order_stock(unit.acc_obj, data['stock_code'], xtconstant.STOCK_BUY,
vol, xtconstant.FIX_PRICE, price, strategy_name, 'PyBuy')
if oid != -1:
unit.order_cache[oid] = (strategy_name, data['stock_code'], 'BUY')
self.pos_manager.mark_holding(strategy_name, data['stock_code'])
self.logger.info(f"√√√ [{unit.alias}] {strategy_name} 下单买入: {data['stock_code']} {vol}股 @ {price}")
except:
self.logger.error(traceback.format_exc())
def _execute_sell(self, unit, strategy_name, data):
v_vol = self.pos_manager.get_position(strategy_name, data['stock_code'])
if v_vol <= 0: return
real_pos = unit.xt_trader.query_stock_positions(unit.acc_obj)
rp = next((p for p in real_pos if p.stock_code == data['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}] 卖出拦截: {data['stock_code']} 实盘无可用持仓")
return
try:
offset = self.config['strategies'][strategy_name].get('execution', {}).get('sell_price_offset', 0.0)
price = round(float(data['price']) + offset, 3)
oid = unit.xt_trader.order_stock(unit.acc_obj, data['stock_code'], xtconstant.STOCK_SELL,
final_vol, xtconstant.FIX_PRICE, price, strategy_name, 'PySell')
if oid != -1:
unit.order_cache[oid] = (strategy_name, data['stock_code'], 'SELL')
self.logger.info(f"√√√ [{unit.alias}] {strategy_name} 下单卖出: {data['stock_code']} {final_vol}股 @ {price}")
except:
self.logger.error(traceback.format_exc())
def get_all_status(self) -> List[TerminalStatus]:
return [TerminalStatus(u.qmt_id, u.alias, u.account_id, (u.callback.is_connected if u.callback else False), u.last_heartbeat) for u in self.units.values()]
def stop(self):
self.is_running = False
self.logger.info("收到引擎停止指令")
for u in self.units.values():
u.cleanup()

View File

@@ -5,7 +5,7 @@ import random
##订阅账户
# 设置 QMT 交易端的数据路径和会话ID
min_path = r"D:\QMT\国金证券QMT交易端\userdata_mini"
min_path = r"C:\\QMT\\中金财富QMT个人版交易端\\userdata_mini"
session_id = int(random.randint(100000, 999999))
# 创建 XtQuantTrader 实例并启动
@@ -22,7 +22,7 @@ else:
exit()
# 设置账户信息
account = StockAccount('8886100517')
account = StockAccount('8176081580')
# 订阅账户
res = xt_trader.subscribe(account)
@@ -30,7 +30,3 @@ if res == 0:
print('订阅成功')
else:
print('订阅失败')
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=''))

View File

@@ -1,50 +1,58 @@
# coding:utf-8
"""
QMT交易系统启动器
用于直接运行,避免包导入问题
QMT多终端交易系统启动器
版本V2.0 (Multi-Terminal Edition)
"""
import sys
import os
import threading
import uvicorn
# 将当前目录添加到Python路径
# 将当前目录添加到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 qmt_engine import MultiEngineManager
from api_server import create_api_server
import threading
import uvicorn
def main():
"""主函数 - 启动QMT交易引擎和API服务器"""
print(">>> 系统正在启动...")
"""主函数 - 启动多终端QMT交易引擎管理中心和API服务器"""
# 强制设置环境变量确保Python在Windows控制台输出不因编码崩溃
os.environ["PYTHONUTF8"] = "1"
# 创建QMT引擎实例
engine = QMTEngine()
print("==================================================")
print(" QMT Multi-Terminal System Starting... ")
print("==================================================")
# 1. 获取多终端管理器单例
manager = MultiEngineManager()
try:
# 初始化引擎
engine.initialize('config.json')
print("QMT引擎初始化成功")
# 2. 初始化引擎加载配置、连接Redis、初始化各终端执行单元
manager.initialize('config.json')
print("Done: Multi-Manager initialized successfully.")
except Exception as e:
print(f"QMT引擎初始化失败: {e}")
print(f"Error: System initialization failed: {repr(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
# 启动交易线程
trading_thread = threading.Thread(target=engine.run_trading_loop, daemon=True)
# 3. 启动全局监控与交易路由主循环线程
# 该线程负责:终端健康检查、断线重连、消息路由、收盘结算
trading_thread = threading.Thread(target=manager.run_trading_loop, name="MainTradeLoop", daemon=True)
trading_thread.start()
print("交易线程启动成功")
print("Done: Global trading loop thread started.")
# 创建API服务器
app = create_api_server(engine)
print("API服务器创建成功")
# 4. 创建适配多终端的API服务器
app = create_api_server(manager)
print("Done: API server created with multi-terminal support.")
# 启动Web服务
print(">>> Web服务启动: http://localhost:8001")
# 5. 启动Web服务
print(">>> Web Dashboard: http://localhost:8001")
try:
# 建议关闭 access_log 以减少控制台刷屏
uvicorn.run(
app,
host="0.0.0.0",
@@ -53,12 +61,10 @@ def main():
access_log=False
)
except KeyboardInterrupt:
print("\n>>> 正在关闭系统...")
engine.stop()
print(">>> 系统已关闭")
print("\n>>> Shutdown signal received. Closing terminals...")
manager.stop()
print(">>> System safely closed.")
if __name__ == '__main__':
# 使用 -u 参数运行是最佳实践: python -u run.py
# 但这里也在代码里强制 flush 了
# 最佳实践:使用 python -u run.py 运行以获得实时日志输出
main()

View File

@@ -21,13 +21,13 @@ title QMT 自动化交易系统 [监控中]
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
:: 优化日期获取逻辑,防止由于区域设置导致的非法文件名字符
for /f "tokens=1-3 delims=-/ " %%a in ("%date%") do (
set "Y=%%a"
set "M=%%b"
set "D=%%c"
)
for /f "tokens=2 delims==" %%a in ('wmic os get localdatetime /value') do set "dt=%%a"
:: 提取前 8 位YYYYMMDD
set "Y=%dt:~0,4%"
set "M=%dt:~4,2%"
set "D=%dt:~6,2%"
set "TODAY=%Y%-%M%-%D%"
set "LOG_FILE=%LOG_DIR%\launcher_%TODAY%.log"
set "LOG_FILE=%LOG_DIR%\%TODAY%.log"
cls
echo ==================================================