feat(shadow-desk): P1-b/c 影子柜台常驻进程+本地撮合broker+前端引擎选择: ShadowBroker(实时价±滑点即时成交/佣金印花最低佣金/A股整手/T+1日锁/资金不足拒单/均价加权/duck-typed BrokerBase协议); runner挂bullet_trade LiveEngine同实盘唯一差=broker_factory换影子(双轨对账基础),成交落paper_trades+30s快照落持仓净值; CLI单实例文件锁(python -m sanguo_trader.shadow); paper_accounts加engine列(eod_replay/shadow迁移); 前端:模拟盘新建组合卡撮合引擎单选+列表影子/日终徽标; 10 broker单测 [vps]
CI/CD / test (push) Successful in 12s
CI/CD / nas-deploy (push) Successful in 25s
CI/CD / nas-verify (push) Successful in 9s

This commit is contained in:
2026-08-14 12:16:05 +08:00
parent 861c7faa8c
commit 8e7e7282de
12 changed files with 632 additions and 4 deletions
+3
View File
@@ -21,6 +21,8 @@ export interface PaperCreate {
pool?: string
max_pool?: number
benchmark?: string
// 撮合引擎(影子柜台 P1):eod_replay=日终回放 / shadow=影子柜台
engine?: string
}
export interface PaperAccount {
@@ -30,6 +32,7 @@ export interface PaperAccount {
interval: string
status: string
strategy_type?: string
engine?: string
symbols?: string
initial_capital?: number
start_date?: string
+2
View File
@@ -13,6 +13,8 @@ export interface PortfolioBacktestReq {
stamp_duty_rate?: number
min_commission?: number
slippage?: number
// K线周期(组合回放暂仅日线 d)
interval?: string
}
export interface EquityPoint {
+6
View File
@@ -34,6 +34,12 @@
background: rgba(255, 176, 0, 0.1);
border-color: rgba(255, 176, 0, 0.3);
}
.chip-shadow {
color: var(--brand);
background: rgba(0, 229, 255, 0.1);
border-color: rgba(0, 229, 255, 0.3);
margin-left: 4px;
}
.chip-portfolio {
color: var(--amber);
background: rgba(255, 176, 0, 0.08);
+6 -1
View File
@@ -188,7 +188,12 @@ async function saveEdit(): Promise<void> {
</template>
</el-table-column>
<el-table-column label="模式" width="80">
<template #default="{ row }"><span class="chip chip-paper">{{ modeLabel[row.mode] || row.mode }}</span></template>
<template #default="{ row }">
<span class="chip chip-paper">{{ modeLabel[row.mode] || row.mode }}</span>
<span v-if="row.strategy_type === 'portfolio'" class="chip chip-shadow" title="影子柜台=盘中实时本地撮合 / 日终回放=每晚全量重放">
{{ row.engine === 'shadow' ? '影子' : '日终' }}
</span>
</template>
</el-table-column>
<el-table-column label="频率" width="70">
<template #default="{ row }"><span class="mono">{{ row.interval }}</span></template>
+17 -1
View File
@@ -115,6 +115,8 @@ onMounted(async () => {
const isPortfolio = computed(() => strategyType.value === 'portfolio')
const portfolioStrategy = ref('all_weather')
// :(NAS 20:30) / (VPS )
const engine = ref<'eod_replay' | 'shadow'>('eod_replay')
function setMode(m: string): void {
//
@@ -149,9 +151,12 @@ async function onSubmit(): Promise<void> {
payload.pool = poolForm.pool
payload.max_pool = Number(poolForm.max_pool)
payload.benchmark = poolForm.benchmark
payload.engine = engine.value
}
const aid = await createPaper(payload)
ElMessage.success(`已创建模拟盘 #${aid}(今晚 20:30 起每日结算)`)
ElMessage.success(strategyType.value === 'portfolio' && engine.value === 'shadow'
? `已创建影子柜台模拟盘 #${aid}VPS 柜台运行期间盘中实时结算)`
: `已创建模拟盘 #${aid}(今晚 20:30 起每日结算)`)
router.push(payload.mode === 'live' ? `/paper/live/${aid}` : `/paper/result/${aid}`)
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '创建失败')
@@ -245,6 +250,17 @@ function onSymbols(v: string): void {
<el-option v-for="b in BENCH_OPTIONS" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
</el-form-item>
<el-form-item label="撮合引擎">
<el-radio-group v-model="engine">
<el-radio-button value="eod_replay">日终回放</el-radio-button>
<el-radio-button value="shadow">影子柜台</el-radio-button>
</el-radio-group>
<span class="muted form-hint">
{{ engine === 'shadow'
? 'VPS 盘中实时行情本地撮合(需 VPS 影子柜台进程运行中)'
: '每晚 20:30 全量重放结算(NAS)' }}
</span>
</el-form-item>
</el-form>
<el-form v-else :model="form" label-width="120px">
<el-form-item label="标的(逗号分隔)">