Files
sanguo_vnpy_v2/frontend/src/views/paper/Reconcile.vue
T

184 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getReconcilePairs, type ReconcilePair } from '@/api/paper'
const pairs = ref<ReconcilePair[]>([])
const loading = ref(false)
const date = ref<string>(new Date().toISOString().slice(0, 10))
async function refresh(): Promise<void> {
loading.value = true
try {
pairs.value = await getReconcilePairs(date.value || undefined)
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '加载失败')
} finally {
loading.value = false
}
}
onMounted(refresh)
function num(v: number | null | undefined): string {
if (v == null || !Number.isFinite(v)) return '—'
return Math.round(v).toLocaleString('zh-CN')
}
function bps(v: number | null | undefined): string {
if (v == null || !Number.isFinite(v)) return '—'
return v.toFixed(1) + ' bps'
}
function pct(v: number | null | undefined): string {
if (v == null || !Number.isFinite(v)) return '—'
return v.toFixed(3) + '%'
}
function sideText(s: string): string {
return s === 'B' ? '买' : '卖'
}
</script>
<template>
<div v-loading="loading" class="page paper-reconcile">
<div class="page-head">
<div>
<h2 class="page-title">双轨对账</h2>
<p class="page-subtitle">影子柜台 vs 实盘模拟 · 同策略同参数并跑差异即真实滑点成本</p>
</div>
<div class="head-right">
<el-date-picker v-model="date" type="date" value-format="YYYY-MM-DD"
placeholder="对账日期" :clearable="false" style="width: 150px" />
<el-button type="primary" :loading="loading" @click="refresh">查询</el-button>
</div>
</div>
<el-alert v-if="!loading && pairs.length === 0" type="info" :closable="false"
title="暂无双轨配对"
description="需要一对同策略的「实盘 + 影子」账户同时运行中才产生配对;数据从并跑首个交易日起累积。" />
<div v-for="p in pairs" :key="`${p.live_account_id}-${p.shadow_account_id}`" class="pair-block">
<div class="pair-head">
<span class="pair-title mono">{{ p.strategy }}</span>
<span class="muted">实盘 #{{ p.live_account_id }} vs 影子 #{{ p.shadow_account_id }} · {{ p.report.date }}</span>
<el-tag :type="p.report.passed ? 'success' : 'danger'" effect="dark">
{{ p.report.passed ? '对账通过' : '存在偏差' }}
</el-tag>
</div>
<!-- 四指标 -->
<div class="stat-row">
<div class="stat">
<span class="stat-label">成交笔数实盘 / 影子</span>
<span class="stat-value mono">{{ p.report.trades.live_count }} / {{ p.report.trades.shadow_count }}</span>
<el-tag size="small" :type="p.report.trades.count_match ? 'success' : 'danger'">
{{ p.report.trades.count_match ? '完全相同' : '笔数不一致' }}
</el-tag>
</div>
<div class="stat">
<span class="stat-label">每笔价差平均 &lt;10bps</span>
<span class="stat-value mono">{{ bps(p.report.trades.avg_price_diff_bps) }}</span>
<el-tag size="small" :type="p.report.trades.pass_price ? 'success' : 'danger'">
{{ p.report.trades.pass_price ? '达标' : '超限' }}
</el-tag>
</div>
<div class="stat">
<span class="stat-label">收盘持仓逐只对比</span>
<span class="stat-value mono">{{ p.report.positions.rows.length }} </span>
<el-tag size="small" :type="p.report.positions.match ? 'success' : 'danger'">
{{ p.report.positions.match ? '完全相同' : '存在分歧' }}
</el-tag>
</div>
<div class="stat">
<span class="stat-label">净值偏差月累计 &lt;0.5%</span>
<span class="stat-value mono">{{ pct(p.report.nav.mtd_deviation_pct) }}</span>
<el-tag v-if="p.report.nav.pass_nav !== null" size="small"
:type="p.report.nav.pass_nav ? 'success' : 'danger'">
{{ p.report.nav.pass_nav ? '达标' : '超限' }}
</el-tag>
<el-tag v-else size="small" type="info">净值序列不全</el-tag>
</div>
<div class="stat">
<span class="stat-label">代码版本一致§12.6 快照</span>
<span class="stat-value mono">{{ p.code_match == null ? '—' : '双端快照' }}</span>
<el-tag v-if="p.code_match === true" size="small" type="success">一致</el-tag>
<el-tag v-else-if="p.code_match === false" size="small" type="danger">不一致先查这个</el-tag>
<el-tag v-else size="small" type="info">无快照</el-tag>
</div>
</div>
<div class="pair-nav mono muted" v-if="p.report.nav.note">
{{ p.report.nav.note }} · 实盘总值 {{ num(p.report.nav.live_total) }} / 影子总值 {{ num(p.report.nav.shadow_total) }}
</div>
<!-- 成交明细 -->
<el-card shadow="never">
<template #header><span class="section-title">成交对比按标的+方向聚合 vwap</span></template>
<el-table :data="p.report.trades.rows" size="small" empty-text="当日无成交">
<el-table-column prop="symbol" label="标的" min-width="100" class-name="mono" />
<el-table-column label="方向" width="70">
<template #default="{ row }">{{ sideText(row.side) }}</template>
</el-table-column>
<el-table-column prop="live_volume" label="实盘量" width="110" class-name="num" />
<el-table-column prop="shadow_volume" label="影子量" width="110" class-name="num" />
<el-table-column prop="live_vwap" label="实盘均价" width="110" class-name="num" />
<el-table-column prop="shadow_vwap" label="影子均价" width="110" class-name="num" />
<el-table-column label="价差" width="110" class-name="num">
<template #default="{ row }">
<span v-if="row.price_diff_bps != null"
:class="Math.abs(row.price_diff_bps) > 10 ? 'down' : ''">{{ bps(row.price_diff_bps) }}</span>
<span v-else class="muted"></span>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 持仓明细 -->
<el-card shadow="never">
<template #header><span class="section-title">收盘持仓对比</span></template>
<el-table :data="p.report.positions.rows" size="small" empty-text="双方均无持仓">
<el-table-column prop="symbol" label="标的" min-width="100" class-name="mono" />
<el-table-column prop="live_volume" label="实盘持仓" width="130" class-name="num" />
<el-table-column prop="shadow_volume" label="影子持仓" width="130" class-name="num" />
<el-table-column label="数量差" width="130" class-name="num">
<template #default="{ row }">
<span :class="row.volume_diff !== 0 ? 'down' : ''">{{ row.volume_diff }}</span>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<style scoped>
.paper-reconcile { display: flex; flex-direction: column; gap: 16px; }
.head-right { display: flex; align-items: center; gap: 12px; }
.pair-block { display: flex; flex-direction: column; gap: 12px; }
.pair-head { display: flex; align-items: center; gap: 12px; }
.pair-title { font-size: 15px; font-weight: 700; color: var(--text); }
.pair-nav { font-size: 12px; }
.stat-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--sp-3);
}
.stat {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.stat-label { font-size: 11px; color: var(--text-3); letter-spacing: 0.3px; }
.stat-value { font-size: 17px; color: var(--text); font-weight: 600; }
@media (max-width: 1200px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
</style>