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