feat(web): 模拟盘前端(点亮入口+router+New/Result页+归因/拒单展示)

This commit is contained in:
2026-07-07 12:50:55 +08:00
parent cacdb5ae24
commit 36b4299934
5 changed files with 222 additions and 2 deletions
+80
View File
@@ -0,0 +1,80 @@
import { apiClient } from './client'
export interface StrategyCfg {
name: string
params: Record<string, unknown>
match_session: string
symbol: string
listing_days?: number
}
export interface PaperCreate {
mode: string
interval: string
symbols: string[]
strategies: StrategyCfg[]
initial_capital: number
start: string
end: string
}
export interface PaperAccount {
id: number
name: string
mode: string
interval: string
status: string
}
export interface BalancePoint {
date: string
cash: number
market_value: number
total_equity: number
is_checkpoint: number
}
export interface PaperTrade {
strategy_id: string
symbol: string
direction: string
price: number
volume: number
commission: number
rejected: number
reject_reason: string
bar_date: string
}
export interface StrategySummary {
strategy_id: string
total_orders: number
filled: number
rejected: number
commission: number
}
export async function createPaper(req: PaperCreate): Promise<number> {
const { data } = await apiClient.post<{ account_id: number }>('/paper/create', req)
return data.account_id
}
export async function getPaper(aid: number): Promise<PaperAccount> {
const { data } = await apiClient.get<PaperAccount>(`/paper/${aid}`)
return data
}
export async function getEquity(aid: number): Promise<BalancePoint[]> {
const { data } = await apiClient.get<BalancePoint[]>(`/paper/${aid}/equity`)
return data
}
export async function getTrades(aid: number): Promise<PaperTrade[]> {
const { data } = await apiClient.get<PaperTrade[]>(`/paper/${aid}/trades`)
return data
}
export async function getStrategies(aid: number): Promise<StrategySummary[]> {
const { data } = await apiClient.get<StrategySummary[]>(`/paper/${aid}/strategies`)
return data
}
+2
View File
@@ -16,6 +16,8 @@ const routes: RouteRecordRaw[] = [
{ path: 'factor/new', name: 'fc-new', component: () => import('@/views/factor/New.vue') },
{ path: 'factor/progress/:id', name: 'fc-progress', component: () => import('@/views/backtest/Progress.vue') },
{ path: 'factor/result/:id', name: 'fc-result', component: () => import('@/views/factor/Result.vue') },
{ path: 'paper/new', name: 'paper-new', component: () => import('@/views/paper/New.vue') },
{ path: 'paper/result/:id', name: 'paper-result', component: () => import('@/views/paper/Result.vue') },
],
},
]
+2 -2
View File
@@ -29,8 +29,8 @@ function onLogout(): void {
<el-menu-item index="/factor/new">
<span>🔬 投研</span>
</el-menu-item>
<el-menu-item index="sim" disabled>
<span>🧪 模拟 <em class="muted">C </em></span>
<el-menu-item index="/paper/new">
<span>🧪 模拟</span>
</el-menu-item>
<el-menu-item index="live" disabled>
<span>💰 实盘 <em class="muted">D · 国金 QMT</em></span>
+76
View File
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { createPaper, type PaperCreate } from '@/api/paper'
const router = useRouter()
const loading = ref(false)
const form = ref<PaperCreate>({
mode: 'replay',
interval: 'd',
symbols: ['600000'],
strategies: [{ name: 'DoubleMa', params: {}, match_session: 'next_open', symbol: '600000' }],
initial_capital: 1_000_000,
start: '2024-01-01',
end: '2024-06-30',
})
async function onSubmit(): Promise<void> {
loading.value = true
try {
const aid = await createPaper(form.value)
ElMessage.success(`已创建模拟盘 #${aid}`)
router.push(`/paper/result/${aid}`)
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '创建失败')
} finally {
loading.value = false
}
}
function onSymbols(v: string): void {
form.value.symbols = v.split(',').map((s) => s.trim()).filter(Boolean)
}
</script>
<template>
<el-card header="新建模拟盘">
<el-form :model="form" label-width="120px">
<el-form-item label="模式">
<el-select v-model="form.mode">
<el-option label="回放(历史重放)" value="replay" />
<el-option label="实走(每日定时)" value="live" />
</el-select>
</el-form-item>
<el-form-item label="频率">
<el-select v-model="form.interval">
<el-option label="日频" value="d" />
<el-option label="15 分钟" value="15m" />
</el-select>
</el-form-item>
<el-form-item label="标的(逗号分隔)">
<el-input :model-value="form.symbols.join(',')" placeholder="600000,000001" @update:model-value="onSymbols" />
</el-form-item>
<el-form-item label="策略名">
<el-input v-model="form.strategies[0].name" />
</el-form-item>
<el-form-item label="撮合时点">
<el-select v-model="form.strategies[0].match_session">
<el-option label="次日开盘(next_open,收盘型)" value="next_open" />
<el-option label="当日收盘(current_close,尾盘型)" value="current_close" />
</el-select>
</el-form-item>
<el-form-item label="起始资金">
<el-input-number v-model="form.initial_capital" :min="10000" :step="100000" />
</el-form-item>
<el-form-item label="区间">
<el-col :span="8"><el-input v-model="form.start" placeholder="YYYY-MM-DD" /></el-col>
<el-col :span="8"><el-input v-model="form.end" placeholder="YYYY-MM-DD" /></el-col>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="onSubmit">创建模拟盘</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
+62
View File
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import {
getEquity, getTrades, getStrategies,
type BalancePoint, type PaperTrade, type StrategySummary,
} from '@/api/paper'
const route = useRoute()
const aid = Number(route.params.id)
const equity = ref<BalancePoint[]>([])
const trades = ref<PaperTrade[]>([])
const strategies = ref<StrategySummary[]>([])
onMounted(async () => {
equity.value = await getEquity(aid)
trades.value = await getTrades(aid)
strategies.value = await getStrategies(aid)
})
function rowClass({ row }: { row: PaperTrade }): string {
return row.rejected ? 'rejected-row' : ''
}
</script>
<template>
<div>
<el-card header="账户净值" style="margin-bottom: 16px">
<el-table :data="equity" size="small" max-height="240">
<el-table-column prop="date" label="日期" />
<el-table-column prop="total_equity" label="总净值" />
<el-table-column prop="cash" label="现金" />
<el-table-column prop="market_value" label="市值" />
</el-table>
</el-card>
<el-card header="分策略归因" style="margin-bottom: 16px">
<el-table :data="strategies" size="small">
<el-table-column prop="strategy_id" label="策略" />
<el-table-column prop="filled" label="成交" />
<el-table-column prop="rejected" label="拒单" />
<el-table-column prop="commission" label="费用" />
</el-table>
</el-card>
<el-card header="成交明细(拒单高亮)">
<el-table :data="trades" size="small" :row-class-name="rowClass">
<el-table-column prop="bar_date" label="日期" />
<el-table-column prop="strategy_id" label="策略" />
<el-table-column prop="symbol" label="标的" />
<el-table-column prop="direction" label="方向" />
<el-table-column prop="price" label="价格" />
<el-table-column prop="volume" label="数量" />
<el-table-column prop="reject_reason" label="拒因" />
</el-table>
</el-card>
</div>
</template>
<style scoped>
:deep(.rejected-row) {
color: #f56c6c;
}
</style>