更新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 fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
from qmt_engine import QMTEngine, QMTStatus # 导入新的管理器类
from qmt_engine import MultiEngineManager, TerminalStatus
# ================= Pydantic模型 ================= # ================= 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): 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]] virtual_positions: Dict[str, Dict[str, str]]
class LogsResponse(BaseModel): class LogsResponse(BaseModel):
"""日志响应模型""" """日志响应模型"""
logs: List[str] logs: List[str]
# ================= FastAPI应用 ================= # ================= FastAPI应用 =================
class QMTAPIServer: class QMTAPIServer:
"""QMT API服务器""" """多终端 QMT API服务器"""
def __init__(self, qmt_engine: QMTEngine): def __init__(self, manager: MultiEngineManager):
self.app = FastAPI(title="QMT Monitor") self.app = FastAPI(title="QMT Multi-Terminal Monitor")
self.qmt_engine = qmt_engine self.manager = manager
self._setup_middleware() self._setup_middleware()
self._setup_routes() self._setup_routes()
@@ -61,41 +70,78 @@ class QMTAPIServer:
return FileResponse("dashboard.html") return FileResponse("dashboard.html")
return {"error": "Dashboard not found"} 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(): def get_status():
"""获取QMT连接状态和系统信息""" """获取所有 QMT 终端的连接状态"""
status = self.qmt_engine.get_status() 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( return StatusResponse(
running=status.is_running, running=self.manager.is_running,
qmt_connected=status.is_connected, start_time=self.manager.start_time,
start_time=status.start_time, terminals=terminals
last_loop_update=status.last_heartbeat,
account_id=status.account_id
) )
@self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息") @self.app.get("/api/positions", response_model=PositionsResponse, summary="获取持仓信息")
def get_positions(): 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( return PositionsResponse(
real_positions=positions["real_positions"], real_positions=real_pos_data,
virtual_positions=positions["virtual_positions"] virtual_positions=virtual_pos_data
) )
@self.app.get("/api/logs", response_model=LogsResponse, summary="获取日志") @self.app.get("/api/logs", response_model=LogsResponse, summary="获取系统日志")
def get_logs(lines: int = Query(50, ge=1, le=1000, description="返回日志行数")): def get_logs(lines: int = Query(50, ge=1, le=1000)):
"""获取最近的交易日志""" """获取最近的系统运行日志"""
logs = self.qmt_engine.get_logs(lines) logs = self.manager.get_logs(lines)
return LogsResponse(logs=logs) return LogsResponse(logs=logs)
@self.app.get("/api/health", summary="健康检查") @self.app.get("/api/health", summary="健康检查")
def health_check(): def health_check():
"""健康检查接口""" """健康检查:只要有一个终端在线即视为正常"""
status = self.qmt_engine.get_status() terminal_data = self.manager.get_all_status()
if status.is_running and status.is_connected: 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')} return {"status": "healthy", "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
else: 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: def get_app(self) -> FastAPI:
"""获取FastAPI应用实例""" """获取FastAPI应用实例"""
@@ -103,7 +149,8 @@ class QMTAPIServer:
# ================= 辅助函数 ================= # ================= 辅助函数 =================
def create_api_server(qmt_engine: QMTEngine) -> FastAPI:
"""创建API服务器""" def create_api_server(manager: MultiEngineManager) -> FastAPI:
server = QMTAPIServer(qmt_engine) """创建API服务器入口"""
return server.get_app() server = QMTAPIServer(manager)
return server.get_app()

View File

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

View File

@@ -16,32 +16,27 @@ from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount from xtquant.xttype import StockAccount
from xtquant import xtconstant from xtquant import xtconstant
# ================= 0. Windows 控制台防卡死补丁 ================= # ================= 0. Windows 补丁 =================
try: try:
import ctypes import ctypes
kernel32 = ctypes.windll.kernel32 kernel32 = ctypes.windll.kernel32
# 禁用快速编辑模式 (0x0040),防止鼠标点击终端导致程序挂起
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 128) kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 128)
except: except:
pass pass
@dataclass @dataclass
class QMTStatus: class TerminalStatus:
"""系统状态封装""" """终端实例状态封装"""
is_connected: bool qmt_id: str
start_time: str alias: str
last_heartbeat: str
account_id: str account_id: str
is_running: bool is_connected: bool
last_heartbeat: str
# ================= 1. 业务逻辑辅助类 =================
# ================= 1. 虚拟持仓与对账辅助类 =================
class PositionManager: class PositionManager:
"""Redis 持仓管理器:负责维护每个子策略的虚拟仓位""" """Redis 虚拟持仓管理(全局单例)"""
def __init__(self, r_client): def __init__(self, r_client):
self.r = r_client self.r = r_client
@@ -49,18 +44,15 @@ class PositionManager:
return f"POS:{strategy_name}" return f"POS:{strategy_name}"
def mark_holding(self, strategy_name, code): def mark_holding(self, strategy_name, code):
"""下单时先在 Redis 占位0股占用一个槽位"""
self.r.hsetnx(self._get_key(strategy_name), code, 0) self.r.hsetnx(self._get_key(strategy_name), code, 0)
def rollback_holding(self, strategy_name, code): def rollback_holding(self, strategy_name, code):
"""报单失败时回滚,释放 Redis 占位"""
key = self._get_key(strategy_name) key = self._get_key(strategy_name)
val = self.r.hget(key, code) val = self.r.hget(key, code)
if val is not None and int(val) == 0: if val is not None and int(val) == 0:
self.r.hdel(key, code) self.r.hdel(key, code)
def update_actual_volume(self, strategy_name, code, delta_vol): def update_actual_volume(self, strategy_name, code, delta_vol):
"""成交回调时更新 Redis 实际股数"""
key = self._get_key(strategy_name) key = self._get_key(strategy_name)
new_vol = self.r.hincrby(key, code, int(delta_vol)) new_vol = self.r.hincrby(key, code, int(delta_vol))
if new_vol <= 0: if new_vol <= 0:
@@ -69,12 +61,10 @@ class PositionManager:
return new_vol return new_vol
def get_position(self, strategy_name, code): def get_position(self, strategy_name, code):
"""获取某个策略下某只股票的虚拟持仓"""
vol = self.r.hget(self._get_key(strategy_name), code) vol = self.r.hget(self._get_key(strategy_name), code)
return int(vol) if vol else 0 return int(vol) if vol else 0
def get_holding_count(self, strategy_name): def get_holding_count(self, strategy_name):
"""获取当前策略已占用的槽位总数"""
return self.r.hlen(self._get_key(strategy_name)) return self.r.hlen(self._get_key(strategy_name))
def get_all_virtual_positions(self, strategy_name): def get_all_virtual_positions(self, strategy_name):
@@ -83,98 +73,120 @@ class PositionManager:
def force_delete(self, strategy_name, code): def force_delete(self, strategy_name, code):
self.r.hdel(self._get_key(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: class DailySettlement:
"""收盘对账逻辑""" """终端级别的日终对账"""
def __init__(self, unit):
def __init__(self, xt_trader, acc, pos_mgr, strategies_config): self.unit = unit
self.trader = xt_trader
self.acc = acc
self.pos_mgr = pos_mgr
self.strategies_config = strategies_config
self.has_settled = False self.has_settled = False
def run_settlement(self): def run_settlement(self):
"""收盘后强制同步 Redis 和实盘持仓""" trader = self.unit.xt_trader
real_positions = self.trader.query_stock_positions(self.acc) 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 {} 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(): manager = MultiEngineManager()
virtual = self.pos_mgr.get_all_virtual_positions(strategy) 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(): for code, v_str in virtual.items():
v = int(v_str)
if code not in real_pos_map: if code not in real_pos_map:
self.pos_mgr.force_delete(strategy, code) manager.pos_manager.force_delete(s_name, code)
elif v == 0 and code in real_pos_map: elif int(v_str) == 0 and code in real_pos_map:
self.pos_mgr.update_actual_volume(strategy, code, real_pos_map[code]) manager.pos_manager.update_actual_volume(s_name, code, real_pos_map[code])
self.has_settled = True self.has_settled = True
def reset_flag(self): def reset_flag(self):
"""重置结算标志,以便第二天重新执行"""
self.has_settled = False self.has_settled = False
# ================= 2. 执行单元 (TradingUnit) =================
# ================= 2. QMT 核心引擎 ================= class UnitCallback(XtQuantTraderCallback):
def __init__(self, unit):
class MyXtQuantTraderCallback(XtQuantTraderCallback): self.unit = unit
"""交易回调事件监听"""
def __init__(self, pos_mgr):
self.pos_mgr = pos_mgr
self.is_connected = False self.is_connected = False
self.logger = logging.getLogger("QMT_Engine")
def on_disconnected(self): def on_disconnected(self):
self.logger.warning(">> 回调通知: 交易端连接断开") logging.getLogger("QMT_Engine").warning(f"终端 {self.unit.alias}({self.unit.qmt_id}) 物理连接断开")
self.is_connected = False self.is_connected = False
def on_stock_trade(self, trade): def on_stock_trade(self, trade):
try: try:
# QMTEngine 是单例,可直接通过类访问 cache_info = self.unit.order_cache.get(trade.order_id)
cache_info = QMTEngine().order_cache.get(trade.order_id)
if not cache_info: return if not cache_info: return
strategy, _, action = cache_info s_name, _, action = cache_info
self.logger.info(f">>> [成交] {strategy} | {trade.stock_code} | {trade.traded_volume}") manager = MultiEngineManager()
if action == 'BUY': 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': 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: except:
traceback.print_exc() logging.getLogger("QMT_Engine").error(traceback.format_exc())
def on_order_error(self, err): def on_order_error(self, err):
cache = self.unit.order_cache.get(err.order_id)
if cache and cache[2] == 'BUY':
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: try:
self.logger.error(f"下单失败回调: {err.error_msg} ID:{err.order_id}") # 采用动态 Session ID 避免冲突
cache = QMTEngine().order_cache.get(err.order_id) session_id = int(time.time()) + hash(self.qmt_id) % 1000
if cache and cache[2] == 'BUY': self.xt_trader = XtQuantTrader(self.path, session_id)
self.pos_mgr.rollback_holding(cache[0], cache[1]) self.acc_obj = StockAccount(self.account_id, self.account_type)
if err.order_id in QMTEngine().order_cache: self.callback = UnitCallback(self)
del QMTEngine().order_cache[err.order_id]
except: self.xt_trader.register_callback(self.callback)
pass 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 QMTEngine: class MultiEngineManager:
"""QMT 交易引擎单例"""
_instance = None _instance = None
_lock = threading.Lock() _lock = threading.Lock()
@@ -187,244 +199,196 @@ class QMTEngine:
def __init__(self): def __init__(self):
if hasattr(self, '_initialized'): return if hasattr(self, '_initialized'): return
self.logger = None self.units: Dict[str, TradingUnit] = {}
self.config = {} self.config = {}
self.xt_trader = None
self.acc = None
self.pos_manager = None
self.callback = None
self.is_running = True self.is_running = True
self.start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 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 self._initialized = True
def initialize(self, config_file='config.json'): def initialize(self, config_file='config.json'):
self._setup_logger() self._setup_logger()
self.config = self._load_config(config_file) with open(config_file, 'r', encoding='utf-8') as f:
# 初始化 Redis self.config = json.load(f)
try:
self.redis_client = redis.Redis(**self.config['redis'], decode_responses=True) self.r = redis.Redis(**self.config['redis'], decode_responses=True)
self.redis_client.ping() self.pos_manager = PositionManager(self.r)
self.pos_manager = PositionManager(self.redis_client)
self.logger.info("Redis 建立连接成功") for t_cfg in self.config.get('qmt_terminals', []):
except Exception as e: unit = TradingUnit(t_cfg)
self.logger.critical(f"Redis 连接失败: {e}") unit.connect()
raise self.units[unit.qmt_id] = unit
self._reconnect_qmt()
def _setup_logger(self): def _setup_logger(self):
log_dir = "logs" log_dir = "logs"
if not os.path.exists(log_dir): os.makedirs(log_dir) 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") log_file = os.path.join(log_dir, f"{datetime.date.today().strftime('%Y-%m-%d')}.log")
self.logger = logging.getLogger("QMT_Engine") logger = logging.getLogger("QMT_Engine")
self.logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
if self.logger.handlers: # 确保日志流为 UTF-8
for h in self.logger.handlers[:]: h.close(); self.logger.removeHandler(h) fmt = logging.Formatter('[%(asctime)s] [%(threadName)s] %(message)s', '%H:%M:%S')
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 = logging.FileHandler(log_file, mode='a', encoding='utf-8')
fh.setFormatter(fmt) fh.setFormatter(fmt)
sh = logging.StreamHandler(sys.stdout) sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(fmt) sh.setFormatter(fmt)
self.logger.addHandler(fh); logger.addHandler(fh)
self.logger.addHandler(sh) logger.addHandler(sh)
def _load_config(self, config_file): def get_strategies_by_terminal(self, qmt_id):
base = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname( return [s for s, cfg in self.config['strategies'].items() if cfg.get('qmt_id') == qmt_id]
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): def run_trading_loop(self):
"""交易主线程循环""" self.logger = logging.getLogger("QMT_Engine")
self.logger.info(">>> 交易主循环线程已启动 <<<") self.logger.info(">>> 多终端交易主循环线程已启动 <<<")
last_check = 0 last_check = 0
while self.is_running: while self.is_running:
try: try:
self.last_heartbeat = datetime.datetime.now().strftime('%H:%M:%S') now_t = time.time()
# 健康检查 curr_hms = datetime.datetime.now().strftime('%H%M%S')
if time.time() - last_check > 15:
last_check = time.time() # --- 健康检查与自动修复 ---
try: if now_t - last_check > 25:
if not (self.xt_trader and self.acc and self.xt_trader.query_stock_asset(self.acc)): last_check = now_t
self._reconnect_qmt() for unit in self.units.values():
except: is_unit_alive = False
self._reconnect_qmt() if unit.xt_trader and unit.acc_obj:
try:
# 物理探测:通过查资产确认连接有效性
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:
is_unit_alive = False
# 交易时间判断 # 断线重连策略
curr = datetime.datetime.now().strftime('%H%M%S') if not is_unit_alive:
is_trading = ('091500' <= curr <= '113000') or ('130000' <= curr <= '150000') # 避让 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() is_trading = ('091500' <= curr_hms <= '113030') or ('130000' <= curr_hms <= '150030')
for s in self.config.get('strategies', {}).keys(): if is_trading:
self.process_strategy_queue(s) for s_name in self.config['strategies'].keys():
elif '150500' <= curr <= '151500' and self.settler and not self.settler.has_settled: self.process_route(s_name)
self.settler.run_settlement()
# --- 收盘结算与标志位重置 ---
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) time.sleep(1 if is_trading else 5)
except Exception as e: except:
self.logger.error(f"主循环异常: {e}") self.logger.error("主循环异常")
self.logger.error(traceback.format_exc())
time.sleep(10) 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: msg_json = self.r.lpop(f"{strategy_name}_real")
conn = self.callback.is_connected if self.callback else False if not msg_json: return
return QMTStatus(conn, self.start_time, self.last_heartbeat,
self.acc.account_id if self.acc else "Unknown", self.is_running) try:
data = json.loads(msg_json)
# 严格校验消息日期
if data.get('timestamp', '').split(' ')[0] != datetime.date.today().strftime('%Y-%m-%d'):
return
def get_positions(self) -> Dict[str, Any]: if data['action'] == 'BUY':
real = [] self._execute_buy(unit, strategy_name, data)
if self.callback and self.callback.is_connected: elif data['action'] == 'SELL':
pos = self.xt_trader.query_stock_positions(self.acc) self._execute_sell(unit, strategy_name, data)
if pos: except:
real = [{"code": p.stock_code, "volume": p.volume, "can_use": p.can_use_volume, "value": p.market_value} pass
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): def _execute_buy(self, unit, strategy_name, data):
log_path = os.path.join("logs", f"{datetime.date.today().strftime('%Y-%m-%d')}.log") strat_cfg = self.config['strategies'][strategy_name]
if not os.path.exists(log_path): return ["今日暂无日志"] # 1. 槽位校验
with open(log_path, 'r', encoding='utf-8') as f: if data['total_slots'] != strat_cfg['total_slots']:
return [l.strip() for l in f.readlines()[-lines:]] 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): def stop(self):
self.is_running = False 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 # 设置 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)) session_id = int(random.randint(100000, 999999))
# 创建 XtQuantTrader 实例并启动 # 创建 XtQuantTrader 实例并启动
@@ -22,7 +22,7 @@ else:
exit() exit()
# 设置账户信息 # 设置账户信息
account = StockAccount('8886100517') account = StockAccount('8176081580')
# 订阅账户 # 订阅账户
res = xt_trader.subscribe(account) res = xt_trader.subscribe(account)
@@ -30,7 +30,3 @@ if res == 0:
print('订阅成功') print('订阅成功')
else: else:
print('订阅失败') 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 # coding:utf-8
""" """
QMT交易系统启动器 QMT多终端交易系统启动器
用于直接运行,避免包导入问题 版本V2.0 (Multi-Terminal Edition)
""" """
import sys import sys
import os import os
import threading
import uvicorn
# 将当前目录添加到Python路径 # 将当前目录添加到Python路径,确保模块导入正常
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path: if current_dir not in sys.path:
sys.path.insert(0, current_dir) sys.path.insert(0, current_dir)
# 导入模块 # 导入升级后的多终端管理器
from qmt_engine import QMTEngine from qmt_engine import MultiEngineManager
from api_server import create_api_server from api_server import create_api_server
import threading
import uvicorn
def main(): def main():
"""主函数 - 启动QMT交易引擎和API服务器""" """主函数 - 启动多终端QMT交易引擎管理中心和API服务器"""
print(">>> 系统正在启动...") # 强制设置环境变量确保Python在Windows控制台输出不因编码崩溃
os.environ["PYTHONUTF8"] = "1"
# 创建QMT引擎实例 print("==================================================")
engine = QMTEngine() print(" QMT Multi-Terminal System Starting... ")
print("==================================================")
# 1. 获取多终端管理器单例
manager = MultiEngineManager()
try: try:
# 初始化引擎 # 2. 初始化引擎加载配置、连接Redis、初始化各终端执行单元
engine.initialize('config.json') manager.initialize('config.json')
print("QMT引擎初始化成功") print("Done: Multi-Manager initialized successfully.")
except Exception as e: except Exception as e:
print(f"QMT引擎初始化失败: {e}") print(f"Error: System initialization failed: {repr(e)}")
import traceback
traceback.print_exc()
sys.exit(1) sys.exit(1)
# 启动交易线程 # 3. 启动全局监控与交易路由主循环线程
trading_thread = threading.Thread(target=engine.run_trading_loop, daemon=True) # 该线程负责:终端健康检查、断线重连、消息路由、收盘结算
trading_thread = threading.Thread(target=manager.run_trading_loop, name="MainTradeLoop", daemon=True)
trading_thread.start() trading_thread.start()
print("交易线程启动成功") print("Done: Global trading loop thread started.")
# 创建API服务器 # 4. 创建适配多终端的API服务器
app = create_api_server(engine) app = create_api_server(manager)
print("API服务器创建成功") print("Done: API server created with multi-terminal support.")
# 启动Web服务 # 5. 启动Web服务
print(">>> Web服务启动: http://localhost:8001") print(">>> Web Dashboard: http://localhost:8001")
try: try:
# 建议关闭 access_log 以减少控制台刷屏
uvicorn.run( uvicorn.run(
app, app,
host="0.0.0.0", host="0.0.0.0",
@@ -53,12 +61,10 @@ def main():
access_log=False access_log=False
) )
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n>>> 正在关闭系统...") print("\n>>> Shutdown signal received. Closing terminals...")
engine.stop() manager.stop()
print(">>> 系统已关闭") print(">>> System safely closed.")
if __name__ == '__main__': if __name__ == '__main__':
# 使用 -u 参数运行是最佳实践: python -u run.py # 最佳实践:使用 python -u run.py 运行以获得实时日志输出
# 但这里也在代码里强制 flush 了 main()
main()

View File

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