184 lines
7.9 KiB
Vue
184 lines
7.9 KiB
Vue
<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">每笔价差(平均 <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">净值偏差(月累计 <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>
|