From ccf958c5d6c82001bfd37f02b5cee410ccdd6b3c Mon Sep 17 00:00:00 2001 From: claude_dev Date: Thu, 13 Aug 2026 18:27:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(paper/live):=20=E7=94=9F=E5=91=BD=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E7=AE=A1=E7=90=86=E8=A1=A5=E5=85=A8:=20paper=E5=81=9C?= =?UTF-8?q?=E6=AD=A2/=E6=81=A2=E5=A4=8D(=E5=AE=9E=E8=B5=B020:30=20step?= =?UTF-8?q?=E8=B7=B3=E8=BF=87stopped)/=E5=88=A0=E9=99=A4(=E8=BF=9E?= =?UTF-8?q?=E5=B8=A6=E5=87=80=E5=80=BC=E6=88=90=E4=BA=A4=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E6=8C=82=E5=8D=95)/=E7=BC=96=E8=BE=91(=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E6=A0=87=E7=9A=84=E8=B5=84=E9=87=91);=20live=E5=88=A0=E9=99=A4?= =?UTF-8?q?(=E8=BF=90=E8=A1=8C=E4=B8=AD=E6=8B=92=E7=BB=9D)/=E7=BC=96?= =?UTF-8?q?=E8=BE=91(stopped=E6=89=8D=E5=8F=AF=E6=94=B9);=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E5=88=97=E8=A1=A8+=E7=BB=93=E6=9E=9C=E9=A1=B5?= =?UTF-8?q?=E6=8C=89=E9=92=AE/=E7=BC=96=E8=BE=91=E5=BC=B9=E7=AA=97/?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=A1=AE=E8=AE=A4=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/live.ts | 18 ++++ frontend/src/api/paper.ts | 26 ++++++ frontend/src/views/live/List.vue | 68 ++++++++++++++- frontend/src/views/paper/List.vue | 124 ++++++++++++++++++++++++++-- frontend/src/views/paper/Result.vue | 37 ++++++++- sanguo_api/routes_live.py | 66 +++++++++++++++ sanguo_api/routes_paper.py | 85 +++++++++++++++++++ tests/api/test_paper_lifecycle.py | 72 ++++++++++++++++ 8 files changed, 488 insertions(+), 8 deletions(-) create mode 100644 tests/api/test_paper_lifecycle.py diff --git a/frontend/src/api/live.ts b/frontend/src/api/live.ts index feb1f11..50aca53 100644 --- a/frontend/src/api/live.ts +++ b/frontend/src/api/live.ts @@ -124,3 +124,21 @@ export async function getLiveStatus(aid: number): Promise { const { data } = await apiClient.get(`/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 +} diff --git a/frontend/src/api/paper.ts b/frontend/src/api/paper.ts index b3ecc15..e67a12b 100644 --- a/frontend/src/api/paper.ts +++ b/frontend/src/api/paper.ts @@ -124,3 +124,29 @@ export async function getPending(aid: number): Promise { const { data } = await apiClient.get(`/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 +} diff --git a/frontend/src/views/live/List.vue b/frontend/src/views/live/List.vue index 401c2b2..a05fa31 100644 --- a/frontend/src/views/live/List.vue +++ b/frontend/src/views/live/List.vue @@ -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 { + 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 { + 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 { actionLoading.value = a.id try { @@ -178,7 +226,7 @@ async function onStop(a: LiveAccount): Promise { - + + + + + + + + + + + + diff --git a/frontend/src/views/paper/List.vue b/frontend/src/views/paper/List.vue index c227961..db3830e 100644 --- a/frontend/src/views/paper/List.vue +++ b/frontend/src/views/paper/List.vue @@ -1,8 +1,11 @@ diff --git a/frontend/src/views/paper/Result.vue b/frontend/src/views/paper/Result.vue index e021bf4..a9f2619 100644 --- a/frontend/src/views/paper/Result.vue +++ b/frontend/src/views/paper/Result.vue @@ -1,8 +1,9 @@