更新qmt代码,支持多端qmt登录
This commit is contained in:
@@ -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()
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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=''))
|
|
||||||
|
|||||||
68
qmt/run.py
68
qmt/run.py
@@ -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()
|
|
||||||
@@ -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 ==================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user