feat(web): 模拟盘前端(点亮入口+router+New/Result页+归因/拒单展示)
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: 'factor/new', name: 'fc-new', component: () => import('@/views/factor/New.vue') },
|
{ 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/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: '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') },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ function onLogout(): void {
|
|||||||
<el-menu-item index="/factor/new">
|
<el-menu-item index="/factor/new">
|
||||||
<span>🔬 投研</span>
|
<span>🔬 投研</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="sim" disabled>
|
<el-menu-item index="/paper/new">
|
||||||
<span>🧪 模拟 <em class="muted">(C 期)</em></span>
|
<span>🧪 模拟</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="live" disabled>
|
<el-menu-item index="live" disabled>
|
||||||
<span>💰 实盘 <em class="muted">(D 期 · 国金 QMT)</em></span>
|
<span>💰 实盘 <em class="muted">(D 期 · 国金 QMT)</em></span>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user