Files
sanguo_vnpy_v2/frontend/src/views/live/New.vue
T

324 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { createLive, type LiveCreateRequest } from '@/api/live'
import { apiClient } from '@/api/client'
import { INTERVAL_OPTIONS } from '@/constants/intervals'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const fromInstance = ref('')
// 策略类型:cta=个股(vnpy engine) / portfolio=组合(bullet_trade LiveEngine 子进程)
const strategyType = ref<'cta' | 'portfolio'>('cta')
const isPortfolio = computed(() => strategyType.value === 'portfolio')
const portfolioOptions = ref<string[]>([])
const portfolioStrategy = ref('all_weather')
const poolForm = ref({ pool: 'hs300_subset', max_pool: 30, benchmark: '000300.XSHG' })
const POOL_OPTIONS = [
{ label: 'HS300 子集(小范围验证)', value: 'hs300_subset' },
{ label: '全市场(慢,非 MVP)', value: 'all' },
]
const BENCH_OPTIONS = [
{ label: '沪深300', value: '000300.XSHG' },
{ label: '中证500', value: '000905.XSHG' },
{ label: '中证1000', value: '000852.XSHG' },
{ label: '中证2000', value: '932000.XSHG' },
]
const PORTFOLIO_LABELS: Record<string, string> = {
all_weather: '全天候轮动',
momentum_timing: '牛熊动量',
value_selection: '价值精选',
small_cap: '小市值轮动',
}
onMounted(async () => {
try {
const { data } = await apiClient.get<{ files: { name: string; type: string }[] }>('/strategy/files')
portfolioOptions.value = data.files
.filter((f) => f.type === 'portfolio')
.map((f) => f.name.replace(/\.py$/, ''))
} catch {
/* 下拉加载失败不阻塞表单 */
}
})
const form = ref<LiveCreateRequest>({
name: 'live-600000',
account: '66639661',
vt_symbol: '600000.SSE',
strategy_class: 'AShareDoubleMaStrategy',
strategy_name: 'AShareDoubleMa_600000',
setting: {
fast_window: 5,
slow_window: 20,
window: 15,
size: 100,
forbid_short: true,
},
interval: 'd',
initial_capital: 1_000_000,
connect_wait_sec: 10,
init_wait_sec: 60,
mini_path: 'C:\\国金QMT交易端模拟\\userdata_mini',
})
const strategyClassOptions = [
{ value: 'AShareDoubleMaStrategy', label: 'AShareDoubleMaStrategy(双均线 A 股策略)' },
]
onMounted(async () => {
const instId = route.query.instance
if (!instId) return
try {
const [{ data: ir }, { data: fr }] = await Promise.all([
apiClient.get<{ instance: { name?: string; code_file: string; symbol_or_pool: string; interval: string; params: Record<string, unknown> } }>(`/strategy/instances/${instId}`),
apiClient.get<{ files: { name: string; class_name: string }[] }>('/strategy/files'),
])
const inst = ir.instance
fromInstance.value = inst.name || String(instId)
const file = fr.files.find((f) => f.name === inst.code_file)
const cls = file?.class_name || inst.code_file
form.value.strategy_class = cls
form.value.vt_symbol = inst.symbol_or_pool || form.value.vt_symbol
form.value.strategy_name = inst.name || form.value.strategy_name
form.value.interval = inst.interval || form.value.interval
form.value.setting = { ...form.value.setting, ...(inst.params as object) }
} catch {
/* 预填失败走默认 */
}
})
async function onSubmit(): Promise<void> {
if (!form.value.name.trim()) {
ElMessage.warning('请填写实例名')
return
}
if (!form.value.account.trim()) {
ElMessage.warning('请填写交易账号')
return
}
if (isPortfolio.value && !portfolioStrategy.value) {
ElMessage.warning('请选择组合策略')
return
}
if (!isPortfolio.value && !form.value.strategy_name.trim()) {
ElMessage.warning('请填写策略实例名')
return
}
loading.value = true
try {
const payload: LiveCreateRequest = { ...form.value }
if (isPortfolio.value) {
payload.strategy_type = 'portfolio'
payload.strategy_class = portfolioStrategy.value
payload.strategy_name = `portfolio_${portfolioStrategy.value}`
payload.pool = poolForm.value.pool
payload.max_pool = Number(poolForm.value.max_pool)
payload.benchmark = poolForm.value.benchmark
} else {
payload.strategy_type = 'cta'
}
const res = await createLive(payload)
ElMessage.success(`已创建实盘实例 #${res.accountId}(stopped),请到列表点"启动"`)
router.push('/live')
} catch (e: unknown) {
ElMessage.error(e instanceof Error ? e.message : '创建失败')
} finally {
loading.value = false
}
}
</script>
<template>
<div class="page live-new">
<div class="page-head">
<div>
<h2 class="page-title">新建实盘</h2>
<p class="page-subtitle">miniQMT 直连 · 个股 CTA / 组合策略实盘(supervisor 轮询)</p>
</div>
</div>
<el-alert
type="info"
:closable="false"
title="创建后状态为 stopped,需到列表点『启动』才会启动(supervisor 轮询发现后拉起)"
/>
<el-card class="blk" shadow="never">
<template #header><span class="section-title">基本配置</span></template>
<div class="seg-label">策略类型</div>
<div class="seg-row">
<div class="seg-card" :class="{ active: strategyType === 'cta' }" @click="strategyType = 'cta'">
<div class="seg-title">CTA 个股策略</div>
<div class="seg-desc">单标的信号型,vnpy engine 进程内跑</div>
</div>
<div class="seg-card" :class="{ active: strategyType === 'portfolio' }" @click="strategyType = 'portfolio'">
<div class="seg-title">组合策略</div>
<div class="seg-desc">选股轮动型,bullet_trade LiveEngine 子进程</div>
</div>
</div>
<el-form label-width="140px" style="margin-top:16px">
<el-form-item label="实例名" required>
<el-input v-model="form.name" placeholder="live-600000" style="width: 320px" />
<span class="muted form-hint">页面显示用</span>
</el-form-item>
<el-form-item label="交易账号" required>
<el-input v-model="form.account" placeholder="66639661" style="width: 320px" />
<span class="muted form-hint">QMT 账号</span>
</el-form-item>
<el-form-item label="起始资金">
<el-input-number v-model="form.initial_capital" :min="10000" :step="100000" style="width: 240px" />
<span class="muted form-hint">单位:</span>
</el-form-item>
<el-form-item label="mini_path">
<el-input v-model="form.mini_path" placeholder="C:\\国金QMT交易端模拟\\userdata_mini" style="width: 480px" />
<span class="muted form-hint">miniQMT userdata_mini 路径;空时后端用 env SANGUO_QMT_PATH 或内置默认</span>
</el-form-item>
</el-form>
</el-card>
<!-- CTA 表单 -->
<template v-if="!isPortfolio">
<el-card class="blk" shadow="never">
<template #header><span class="section-title">标的与策略</span></template>
<el-form :model="form" label-width="140px">
<el-form-item label="策略类">
<el-select v-model="form.strategy_class" style="width: 360px">
<el-option
v-for="opt in strategyClassOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<span class="muted form-hint">实盘需 A 股适配(定寸/禁空/分钟合成),目前仅 DoubleMa 完成适配,其余策略陆续扩展</span>
</el-form-item>
<el-form-item label="策略实例名" required>
<el-input v-model="form.strategy_name" placeholder="AShareDoubleMa_600000" style="width: 320px" />
<span class="muted form-hint">engine 内唯一</span>
</el-form-item>
<el-form-item label="标的(vt_symbol)">
<el-input v-model="form.vt_symbol" placeholder="600000 或 600000.SSE" style="width: 320px" />
<span class="muted form-hint">可只写 6 位代码,自动补交易所后缀(.SSE / .SZSE)</span>
</el-form-item>
<el-form-item label="K 线周期">
<el-select v-model="form.interval" style="width: 220px">
<el-option v-for="o in INTERVAL_OPTIONS" :key="o.value" :value="o.value" :label="o.label">
<span>{{ o.label }}</span>
<span v-if="!o.replayOk" class="muted" style="float: right; font-size: 12px">影子柜台实时可用·回放暂无本地数据</span>
</el-option>
</el-select>
<span class="muted form-hint">miniQMT 成品K线,默认日线</span>
</el-form-item>
</el-form>
</el-card>
<el-card class="blk" shadow="never">
<template #header><span class="section-title">策略参数(setting)</span></template>
<el-form :model="form.setting" label-width="140px">
<el-form-item label="fast_window">
<el-input-number v-model="form.setting.fast_window as number" :min="1" :step="1" style="width: 200px" />
<span class="muted form-hint">快均线周期</span>
</el-form-item>
<el-form-item label="slow_window">
<el-input-number v-model="form.setting.slow_window as number" :min="1" :step="1" style="width: 200px" />
<span class="muted form-hint">慢均线周期</span>
</el-form-item>
<el-form-item label="window">
<el-input-number v-model="form.setting.window as number" :min="1" :step="1" style="width: 200px" />
<span class="muted form-hint">辅助计算窗口</span>
</el-form-item>
<el-form-item label="size">
<el-input-number v-model="form.setting.size as number" :min="1" :step="100" style="width: 200px" />
<span class="muted form-hint">定寸股数(A 100 的倍数)</span>
</el-form-item>
<el-form-item label="forbid_short">
<el-switch v-model="form.setting.forbid_short as boolean" />
<span class="muted form-hint">禁止做空(A 股默认开)</span>
</el-form-item>
</el-form>
</el-card>
<el-card class="blk" shadow="never">
<template #header><span class="section-title">连接参数(可选)</span></template>
<el-form :model="form" label-width="140px">
<el-form-item label="connect_wait_sec">
<el-input-number v-model="form.connect_wait_sec as number" :min="1" :step="5" style="width: 200px" />
<span class="muted form-hint">连接 QMT 等待秒数</span>
</el-form-item>
<el-form-item label="init_wait_sec">
<el-input-number v-model="form.init_wait_sec as number" :min="1" :step="10" style="width: 200px" />
<span class="muted form-hint">策略初始化等待秒数</span>
</el-form-item>
</el-form>
</el-card>
</template>
<!-- 组合策略表单 -->
<el-card v-else class="blk" shadow="never">
<template #header><span class="section-title">组合策略与选股</span></template>
<el-form :model="poolForm" label-width="140px">
<el-form-item label="组合策略">
<el-select v-model="portfolioStrategy" style="width: 320px">
<el-option
v-for="p in portfolioOptions" :key="p"
:label="PORTFOLIO_LABELS[p] ? `${PORTFOLIO_LABELS[p]}${p}` : p"
:value="p"
/>
</el-select>
<span class="muted form-hint">supervisor 拉起 runner_live 子进程(bullet_trade LiveEngine)</span>
</el-form-item>
<el-form-item label="标的池">
<el-select v-model="poolForm.pool" style="width: 320px">
<el-option v-for="o in POOL_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="选股池上限">
<el-input-number v-model="poolForm.max_pool" :min="0" :step="10" :controls="false" style="width: 220px" />
<span class="muted form-hint">0=不限, N=前N只默认30</span>
</el-form-item>
<el-form-item label="比较基准">
<el-select v-model="poolForm.benchmark" style="width: 220px">
<el-option v-for="b in BENCH_OPTIONS" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
</el-form-item>
</el-form>
</el-card>
<div class="submit-bar">
<el-button type="primary" size="large" :loading="loading" @click="onSubmit">
创建实盘实例
</el-button>
<el-button size="large" @click="router.push('/live')">取消</el-button>
</div>
</div>
</template>
<style scoped>
.live-new { display: flex; flex-direction: column; gap: 16px; }
.blk { border: 1px solid var(--border-2); }
.form-hint { margin-left: 10px; }
.submit-bar { padding: 4px 0; display: flex; gap: 12px; }
.seg-label { font-size: 12px; color: var(--text-3); margin-bottom: 8px; }
.seg-row { display: flex; gap: 12px; }
.seg-card {
flex: 1;
max-width: 280px;
border: 1px solid var(--border-2);
border-radius: var(--r-md);
padding: 12px 14px;
cursor: pointer;
transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.seg-card:hover { border-color: var(--brand); background: var(--bg-hover); }
.seg-card.active { border-color: var(--brand); background: rgba(24, 144, 255, 0.10); }
.seg-title { font-size: 14px; font-weight: 600; color: var(--text); }
.seg-desc { font-size: 12px; color: var(--text-3); margin-top: 4px; }
</style>