feat(paper/live): 生命周期管理补全: paper停止/恢复(实走20:30 step跳过stopped)/删除(连带净值成交持仓挂单)/编辑(名称标的资金); live删除(运行中拒绝)/编辑(stopped才可改); 前端列表+结果页按钮/编辑弹窗/删除确认 [vps]
CI/CD / test (push) Successful in 10s
CI/CD / nas-deploy (push) Failing after 11s
CI/CD / nas-verify (push) Has been skipped

This commit is contained in:
2026-08-13 18:27:32 +08:00
parent d8c156e6de
commit ccf958c5d6
8 changed files with 488 additions and 8 deletions
+18
View File
@@ -124,3 +124,21 @@ export async function getLiveStatus(aid: number): Promise<LiveStatus> {
const { data } = await apiClient.get<LiveStatus>(`/live/${aid}/status`)
return data
}
export async function deleteLive(aid: number): Promise<{ account_id: number; deleted: boolean }> {
const { data } = await apiClient.delete(`/live/${aid}`)
return data
}
export interface LiveUpdateReq {
name?: string
account?: string
vt_symbol?: string
strategy_name?: string
interval?: string
}
export async function updateLive(aid: number, req: LiveUpdateReq): Promise<{ updated: boolean }> {
const { data } = await apiClient.put(`/live/${aid}`, req)
return data
}
+26
View File
@@ -124,3 +124,29 @@ export async function getPending(aid: number): Promise<PendingOrder[]> {
const { data } = await apiClient.get<PendingOrder[]>(`/paper/${aid}/pending`)
return data
}
export async function stopPaper(aid: number): Promise<{ account_id: number; status: string }> {
const { data } = await apiClient.post(`/paper/${aid}/stop`)
return data
}
export async function resumePaper(aid: number): Promise<{ account_id: number; status: string }> {
const { data } = await apiClient.post(`/paper/${aid}/resume`)
return data
}
export async function deletePaper(aid: number): Promise<{ account_id: number; deleted: boolean }> {
const { data } = await apiClient.delete(`/paper/${aid}`)
return data
}
export interface PaperUpdateReq {
name?: string
symbols?: string[]
initial_capital?: number
}
export async function updatePaper(aid: number, req: PaperUpdateReq): Promise<{ updated: boolean }> {
const { data } = await apiClient.put(`/paper/${aid}`, req)
return data
}
+66 -2
View File
@@ -2,8 +2,11 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listLives, startLive, stopLive,
listLives, startLive, stopLive, deleteLive, updateLive,
type LiveAccount,
} from '@/api/live'
@@ -73,6 +76,51 @@ function goNew(): void {
router.push('/live/new')
}
async function onDelete(a: LiveAccount): Promise<void> {
try {
await ElMessageBox.confirm(
`删除实盘实例 #${a.id}${a.name})?成交/持仓/余额数据将一并删除,不可恢复。`,
'删除确认',
{ type: 'warning' },
)
} catch {
return
}
actionLoading.value = a.id
try {
await deleteLive(a.id)
ElMessage.success(`#${a.id} 已删除`)
await refreshSilent()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '删除失败')
} finally {
actionLoading.value = null
}
}
// 编辑(仅 stopped 可改;后端运行中会 400)
const editVisible = ref(false)
const editForm = reactive({ id: 0, name: '', account: '', vt_symbol: '', strategy_name: '', interval: '' })
function openEdit(a: LiveAccount): void {
editForm.id = a.id
editForm.name = a.name
editForm.account = a.account
editForm.vt_symbol = a.vt_symbol
editForm.strategy_name = a.strategy_name
editForm.interval = a.interval
editVisible.value = true
}
async function saveEdit(): Promise<void> {
try {
await updateLive(editForm.id, { ...editForm })
ElMessage.success(`#${editForm.id} 已更新`)
editVisible.value = false
await refreshSilent()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '更新失败')
}
}
async function onStart(a: LiveAccount): Promise<void> {
actionLoading.value = a.id
try {
@@ -178,7 +226,7 @@ async function onStop(a: LiveAccount): Promise<void> {
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<el-table-column label="操作" width="240">
<template #default="{ row }">
<el-button
v-if="row.status !== 'running'"
@@ -193,10 +241,26 @@ async function onStop(a: LiveAccount): Promise<void> {
@click="onStop(row)"
>停止</el-button>
<el-button link type="primary" size="small" @click="goMonitor(row)">监控</el-button>
<el-button v-if="row.status !== 'running'" link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
<el-button v-if="row.status !== 'running'" link type="danger" size="small" :loading="actionLoading === row.id" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="editVisible" title="编辑实盘实例" width="480px">
<el-form :model="editForm" label-width="90px">
<el-form-item label="名称"><el-input v-model="editForm.name" /></el-form-item>
<el-form-item label="交易账号"><el-input v-model="editForm.account" /></el-form-item>
<el-form-item label="标的"><el-input v-model="editForm.vt_symbol" /></el-form-item>
<el-form-item label="策略实例名"><el-input v-model="editForm.strategy_name" /></el-form-item>
<el-form-item label="频率"><el-input v-model="editForm.interval" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
+119 -5
View File
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { listPapers, type PaperAccount } from '@/api/paper'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listPapers, stopPaper, resumePaper, deletePaper, updatePaper,
type PaperAccount,
} from '@/api/paper'
const router = useRouter()
const accounts = ref<PaperAccount[]>([])
@@ -10,6 +13,7 @@ const loading = ref(false)
const kw = ref('')
const modeFilter = ref('')
const statusFilter = ref('')
const actionLoading = ref(0)
onMounted(refresh)
@@ -62,6 +66,84 @@ function open(a: PaperAccount): void {
function goNew(): void {
router.push('/paper/new')
}
async function onStop(a: PaperAccount): Promise<void> {
actionLoading.value = a.id
try {
await stopPaper(a.id)
ElMessage.success(`#${a.id} 已停止,今晚起不再结算`)
await refresh()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '停止失败')
} finally {
actionLoading.value = 0
}
}
async function onResume(a: PaperAccount): Promise<void> {
actionLoading.value = a.id
try {
await resumePaper(a.id)
ElMessage.success(`#${a.id} 已恢复,今晚 20:30 起继续结算`)
await refresh()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '恢复失败')
} finally {
actionLoading.value = 0
}
}
async function onDelete(a: PaperAccount): Promise<void> {
try {
await ElMessageBox.confirm(
`删除模拟盘 #${a.id}${a.name})?净值/成交/持仓数据将一并删除,不可恢复。`,
'删除确认',
{ type: 'warning' },
)
} catch {
return
}
actionLoading.value = a.id
try {
await deletePaper(a.id)
ElMessage.success(`#${a.id} 已删除`)
await refresh()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '删除失败')
} finally {
actionLoading.value = 0
}
}
// 编辑(MVP:名称/标的/初始资金)
const editVisible = ref(false)
const editForm = reactive({ id: 0, name: '', symbols: '', initial_capital: 0 })
function openEdit(a: PaperAccount): void {
editForm.id = a.id
editForm.name = a.name
const syms: string = typeof a.symbols === 'string' ? a.symbols : JSON.stringify(a.symbols ?? [])
try {
editForm.symbols = (JSON.parse(syms) as string[]).join(',')
} catch {
editForm.symbols = syms
}
editForm.initial_capital = a.initial_capital
editVisible.value = true
}
async function saveEdit(): Promise<void> {
try {
await updatePaper(editForm.id, {
name: editForm.name,
symbols: editForm.symbols.split(',').map((s) => s.trim()).filter(Boolean),
initial_capital: Number(editForm.initial_capital),
})
ElMessage.success(`#${editForm.id} 已更新(策略参数自下次结算生效)`)
editVisible.value = false
await refresh()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '更新失败')
}
}
</script>
<template>
@@ -131,11 +213,43 @@ function goNew(): void {
<el-table-column label="状态" width="84">
<template #default="{ row }"><span class="chip" :class="`st-${row.status}`">{{ statusLabel[row.status] || row.status }}</span></template>
</el-table-column>
<el-table-column label="操作" width="76">
<template #default="{ row }"><el-button link type="primary" @click="open(row)">查看</el-button></template>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="open(row)">查看</el-button>
<el-button
v-if="row.mode === 'live' && row.status === 'running'"
link type="warning" :loading="actionLoading === row.id"
@click="onStop(row)"
>停止</el-button>
<el-button
v-if="row.mode === 'live' && row.status === 'stopped'"
link type="success" :loading="actionLoading === row.id"
@click="onResume(row)"
>恢复</el-button>
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" :loading="actionLoading === row.id" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="editVisible" title="编辑模拟盘" width="480px">
<el-form :model="editForm" label-width="90px">
<el-form-item label="名称">
<el-input v-model="editForm.name" />
</el-form-item>
<el-form-item label="标的">
<el-input v-model="editForm.symbols" placeholder="逗号分隔" />
</el-form-item>
<el-form-item label="初始资金">
<el-input-number v-model="editForm.initial_capital" :min="10000" :step="100000" :controls="false" style="width: 200px" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
+36 -1
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
getPaper, getEquity, getTrades, getStrategies,
getPaper, getEquity, getTrades, getStrategies, stopPaper, resumePaper,
type BalancePoint, type PaperTrade, type StrategySummary, type PaperAccount,
} from '@/api/paper'
import type { EquityPoint } from '@/api/backtest'
@@ -105,6 +106,32 @@ function rowClass({ row }: { row: PaperTrade }): string {
function toLive(): void {
router.push(`/paper/live/${aid}`)
}
const lifecycleBusy = ref(false)
async function onStop(): Promise<void> {
lifecycleBusy.value = true
try {
await stopPaper(aid)
ElMessage.success('已停止,今晚起不再结算')
await load()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '停止失败')
} finally {
lifecycleBusy.value = false
}
}
async function onResume(): Promise<void> {
lifecycleBusy.value = true
try {
await resumePaper(aid)
ElMessage.success('已恢复,今晚 20:30 起继续结算')
await load()
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '恢复失败')
} finally {
lifecycleBusy.value = false
}
}
</script>
<template>
@@ -124,6 +151,14 @@ function toLive(): void {
? '实走中 · 每日 20:30 结算'
: statusLabel(account.status)
}}</span>
<el-button
v-if="account?.mode === 'live' && account.status === 'running'"
type="warning" :loading="lifecycleBusy" @click="onStop"
>停止</el-button>
<el-button
v-if="account?.mode === 'live' && account.status === 'stopped'"
type="success" :loading="lifecycleBusy" @click="onResume"
>恢复</el-button>
<el-button v-if="account?.mode === 'live'" type="primary" @click="toLive">实走监控 </el-button>
</div>
</div>