mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 14:34:18 +08:00
fix: 策略库「重跑到今天」补齐组合分析 — 多策略组合级WF/一条龙/AI解读
策略库(/strategies)多策略组合卡片此前只有主回测:本轮把 v1.31.0 的组合分析链路延伸到多策略组合(N 策略 × 各自原标的): - walkforward:组合 WF 泛化为槽位模型(_ComboSlot/_ComboWalkForwardBase), PortfolioWalkForwardEngine 行为不变;新增 MultiStrategyWalkForwardEngine (N 个策略各跑各自原标的,key 形如 label@symbol),复用切窗语义与 WalkForwardResult 结构(前端 WalkForwardPanel 直接渲染) - benchmark:新增 evaluate_multi 一条龙(MultiStrategyEngine 回测 + 多策略 组合 WF + 逐槽位三段体检多数口径聚合 + 综合评分 + 组合评级 + 各槽位标的 等权买入持有基准对比),报告结构与单标的 evaluate_strategy 同构 - Web:新增 POST /backtest/multi-strategy/wf/run/async 与 /backtest/multi-strategy/evaluate/run/async;多策略组合回测响应附带 grade/score(与单标的/多标的组合响应同构) - 前端 StrategiesView:组合结果区新增「WF 样本外验证 / 一条龙评估 / AI 解读」 按钮与同构面板(按需触发,复用最近一次组合回测的 items/cash); 绩效指标表补齐 v1.28 深度 6 项(SQN/最大连胜连亏/Ulcer/VaR/CVaR); aiPrompt 新增 multi 模式(策略明细语境 + 槽位表现段) - 测试:多策略 WF 引擎 3 例、evaluate_multi 3 例、新端点 Web 级 2 例、 aiPrompt multi 模式 1 例(pytest 1611 绿、node --test 5/5、E2E 9/9)
This commit is contained in:
@@ -330,3 +330,35 @@ test('组合版:WF / 一条龙 / 评级按需拼接', () => {
|
||||
assert.match(p, /# 评级(不看收益率,面向「普通人拿不拿得住」)/)
|
||||
assert.match(p, /档位:\*\*D\*\*/)
|
||||
})
|
||||
|
||||
test('组合版 multi 模式:N 个策略各跑原标的的语境与槽位明细', () => {
|
||||
const p = buildPortfolioAiPrompt({
|
||||
stocks: ['双均线交叉@SH:601088', 'MACD 金叉@SZ:000001'],
|
||||
category: 'DAY',
|
||||
startDate: '2023-01-02',
|
||||
endDate: '2026-09-03',
|
||||
strategyLabel: '2 策略组合',
|
||||
params: {},
|
||||
cash: 500000,
|
||||
commission: 0.0003,
|
||||
slippage: 0,
|
||||
execution: 'next_open',
|
||||
mode: 'multi',
|
||||
result: {
|
||||
...PORTFOLIO_RESULT,
|
||||
individual_results: {
|
||||
'双均线交叉@SH:601088': RESULT,
|
||||
'MACD 金叉@SZ:000001': RESULT,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 角色设定为多策略语境(N 个策略各跑各自的原标的)
|
||||
assert.match(p, /N 个策略各跑各自的原标的,资金均分/)
|
||||
// 配置段列策略明细而非标的清单,不再出现单一策略/参数行
|
||||
assert.match(p, /2 个策略各跑各自的原标的,资金均分(各拿总额的 50\.0%)/)
|
||||
assert.match(p, /双均线交叉@SH:601088、MACD 金叉@SZ:000001/)
|
||||
assert.ok(!p.includes('- 策略:2 策略组合'))
|
||||
// 槽位表现段标题为「各策略槽位」
|
||||
assert.match(p, /# 各策略槽位表现(按收益降序;全部)/)
|
||||
})
|
||||
|
||||
+63
-32
@@ -49,7 +49,8 @@ export interface AiPromptInput {
|
||||
}
|
||||
|
||||
export interface PortfolioAiPromptInput {
|
||||
/** 完整标的代码列表(带市场前缀,如 ["SZ:000001", "SH:600519"]) */
|
||||
/** 完整标的代码列表(带市场前缀,如 ["SZ:000001", "SH:600519"]);
|
||||
* multi 模式下传「策略@标的」明细列表(如 ["双均线@SH:601088", …]) */
|
||||
stocks: string[]
|
||||
category: Category
|
||||
startDate: string
|
||||
@@ -61,6 +62,9 @@ export interface PortfolioAiPromptInput {
|
||||
slippage: number
|
||||
execution: ExecutionMode
|
||||
result: PortfolioResult
|
||||
/** 组合形态:portfolio = 一个策略 × 多只标的(默认);
|
||||
* multi = 多个策略各跑各自的原标的(策略库「重跑到今天」) */
|
||||
mode?: 'portfolio' | 'multi'
|
||||
/** 附加分析(未勾选/未跑完时传 null,对应段落自动省略) */
|
||||
wf?: WalkForwardResult | null
|
||||
evaluate?: EvaluateReport | null
|
||||
@@ -163,15 +167,22 @@ function n(v: number | string | null | undefined): number | undefined {
|
||||
|
||||
// ── 各段落构建 ───────────────────────────────────────────────────────────────
|
||||
|
||||
function sectionRole(kind: 'single' | 'portfolio' = 'single'): string {
|
||||
const intro =
|
||||
kind === 'portfolio'
|
||||
? '下面是我跑出来的组合回测报告(同一个策略分别跑在一篮子标的上,资金均分、各标的独立回测后净值加总),帮我看看这个组合策略到底行不行。内容上要说到这六件事,顺序随意,用你自然的说话方式组织:'
|
||||
: '下面是我跑出来的回测报告,帮我看看这个策略到底行不行。内容上要说到这六件事,顺序随意,用你自然的说话方式组织:'
|
||||
const step5 =
|
||||
kind === 'portfolio'
|
||||
? '5. **给可执行的下一步**:几条我马上能做的事(改什么参数、加什么过滤、换哪些标的、先做什么测试再谈实盘),别空谈;'
|
||||
: '5. **给可执行的下一步**:几条我马上能做的事(改什么参数、加什么过滤、先做什么测试再谈实盘),别空谈;'
|
||||
function sectionRole(kind: 'single' | 'portfolio' | 'multi' = 'single'): string {
|
||||
let intro =
|
||||
'下面是我跑出来的回测报告,帮我看看这个策略到底行不行。内容上要说到这六件事,顺序随意,用你自然的说话方式组织:'
|
||||
let step5 =
|
||||
'5. **给可执行的下一步**:几条我马上能做的事(改什么参数、加什么过滤、先做什么测试再谈实盘),别空谈;'
|
||||
if (kind === 'portfolio') {
|
||||
intro =
|
||||
'下面是我跑出来的组合回测报告(同一个策略分别跑在一篮子标的上,资金均分、各标的独立回测后净值加总),帮我看看这个组合策略到底行不行。内容上要说到这六件事,顺序随意,用你自然的说话方式组织:'
|
||||
step5 =
|
||||
'5. **给可执行的下一步**:几条我马上能做的事(改什么参数、加什么过滤、换哪些标的、先做什么测试再谈实盘),别空谈;'
|
||||
} else if (kind === 'multi') {
|
||||
intro =
|
||||
'下面是我跑出来的组合回测报告(N 个策略各跑各自的原标的,资金均分、各策略独立回测后净值加总),帮我看看这个策略组合到底行不行。内容上要说到这六件事,顺序随意,用你自然的说话方式组织:'
|
||||
step5 =
|
||||
'5. **给可执行的下一步**:几条我马上能做的事(改哪些策略的参数、换掉拖后腿的策略、加什么过滤、先做什么测试再谈实盘),别空谈;'
|
||||
}
|
||||
return [
|
||||
'# 角色设定',
|
||||
'',
|
||||
@@ -370,28 +381,46 @@ function sectionFooter(): string {
|
||||
// ── 组合版段落 ───────────────────────────────────────────────────────────────
|
||||
|
||||
function sectionPortfolioConfig(i: PortfolioAiPromptInput): string {
|
||||
const stockList =
|
||||
i.stocks.length <= 12
|
||||
? i.stocks.join('、')
|
||||
: `${i.stocks.slice(0, 12).join('、')} 等 ${i.stocks.length} 只`
|
||||
const lines = [
|
||||
'# 组合回测配置',
|
||||
'',
|
||||
`- 组合形式:同一个策略分别跑在 ${i.stocks.length} 只标的上,资金均分(各拿总额的 ${(
|
||||
100 / i.stocks.length
|
||||
).toFixed(1)}%),标的间独立回测、净值按日加总`,
|
||||
`- 标的列表:${stockList}`,
|
||||
`- 回测区间:${i.startDate} ~ ${i.endDate}(${CATEGORY_LABELS[i.category] ?? i.category})`,
|
||||
`- 策略:${i.strategyLabel}`,
|
||||
`- 参数:${fmtParams(i.params)}`,
|
||||
const multi = i.mode === 'multi'
|
||||
const lines = ['# 组合回测配置', '']
|
||||
if (multi) {
|
||||
const detail =
|
||||
i.stocks.length <= 12
|
||||
? i.stocks.join('、')
|
||||
: `${i.stocks.slice(0, 12).join('、')} 等 ${i.stocks.length} 个策略槽位`
|
||||
lines.push(
|
||||
`- 组合形式:${i.stocks.length} 个策略各跑各自的原标的,资金均分(各拿总额的 ${(
|
||||
100 / i.stocks.length
|
||||
).toFixed(1)}%),策略间独立回测、净值按日加总`,
|
||||
)
|
||||
lines.push(`- 策略明细:${detail}`)
|
||||
} else {
|
||||
const stockList =
|
||||
i.stocks.length <= 12
|
||||
? i.stocks.join('、')
|
||||
: `${i.stocks.slice(0, 12).join('、')} 等 ${i.stocks.length} 只`
|
||||
lines.push(
|
||||
`- 组合形式:同一个策略分别跑在 ${i.stocks.length} 只标的上,资金均分(各拿总额的 ${(
|
||||
100 / i.stocks.length
|
||||
).toFixed(1)}%),标的间独立回测、净值按日加总`,
|
||||
)
|
||||
lines.push(`- 标的列表:${stockList}`)
|
||||
lines.push(`- 策略:${i.strategyLabel}`)
|
||||
lines.push(`- 参数:${fmtParams(i.params)}`)
|
||||
}
|
||||
if (multi) {
|
||||
lines.push('- (各策略的参数见各策略在策略库中保存的配置,此处不逐一展开)')
|
||||
}
|
||||
lines.push(`- 回测区间:${i.startDate} ~ ${i.endDate}(${CATEGORY_LABELS[i.category] ?? i.category})`)
|
||||
lines.push(
|
||||
`- 组合总资金:${fmtMoney(i.cash)} 元;佣金 ${i.commission};滑点 ${i.slippage};成交价:${EXECUTION_LABELS[i.execution] ?? i.execution}`,
|
||||
'',
|
||||
]
|
||||
)
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/** 各标的表现摘要:按收益降序,超过 12 只时只列最好 6 只 + 最差 6 只。 */
|
||||
function sectionStocksSummary(result: PortfolioResult): string {
|
||||
/** 各槽位表现摘要:按收益降序,超过 12 个时只列最好 6 个 + 最差 6 个。 */
|
||||
function sectionStocksSummary(result: PortfolioResult, multi = false): string {
|
||||
const entries = Object.entries(result.individual_results)
|
||||
if (entries.length === 0) return ''
|
||||
const sorted = entries
|
||||
@@ -401,9 +430,10 @@ function sectionStocksSummary(result: PortfolioResult): string {
|
||||
sorted.length <= 12
|
||||
? sorted
|
||||
: [...sorted.slice(0, 6), ...sorted.slice(sorted.length - 6)]
|
||||
const what = multi ? '各策略槽位' : '各标的'
|
||||
const lines = [
|
||||
'# 各标的表现(按收益降序;' +
|
||||
(sorted.length <= 12 ? '全部' : `省略中间 ${sorted.length - 12} 只,其余为最好/最差各 6 只`) +
|
||||
`# ${what}表现(按收益降序;` +
|
||||
(sorted.length <= 12 ? '全部' : `省略中间 ${sorted.length - 12} 个,其余为最好/最差各 6 个`) +
|
||||
')',
|
||||
'',
|
||||
]
|
||||
@@ -451,13 +481,14 @@ export function buildAiPrompt(input: AiPromptInput): string {
|
||||
|
||||
/** 组装组合回测的 AI 解读 Prompt(与单标的同构,段落随附加分析增减)。 */
|
||||
export function buildPortfolioAiPrompt(input: PortfolioAiPromptInput): string {
|
||||
const multi = input.mode === 'multi'
|
||||
const parts: string[] = [
|
||||
sectionRole('portfolio'),
|
||||
sectionRole(multi ? 'multi' : 'portfolio'),
|
||||
sectionPortfolioConfig(input),
|
||||
sectionMetrics(input.result.total_performance),
|
||||
sectionEquityPoints(input.result.combined_equity),
|
||||
]
|
||||
const stocksSummary = sectionStocksSummary(input.result)
|
||||
const stocksSummary = sectionStocksSummary(input.result, multi)
|
||||
if (stocksSummary) parts.push(stocksSummary)
|
||||
if (input.wf) parts.push(sectionWf(input.wf))
|
||||
if (input.evaluate) parts.push(sectionEvaluate(input.evaluate))
|
||||
|
||||
@@ -241,6 +241,33 @@ export async function submitMultiStrategyTask(
|
||||
return (await resp.json()) as TaskSubmitResponse
|
||||
}
|
||||
|
||||
/** 提交多策略组合级 Walk-Forward 样本外验证后台任务(n_windows 默认 7)。 */
|
||||
export async function submitMultiStrategyWalkforwardTask(
|
||||
req: MultiStrategyBacktestRequest,
|
||||
nWindows = 7,
|
||||
): Promise<TaskSubmitResponse> {
|
||||
const resp = await fetch(`${BASE}/backtest/multi-strategy/wf/run/async?n_windows=${nWindows}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
if (!resp.ok) await throwError(resp)
|
||||
return (await resp.json()) as TaskSubmitResponse
|
||||
}
|
||||
|
||||
/** 提交多策略组合级一条龙评估后台任务(组合回测+WF+适配性+评分+基准对比)。 */
|
||||
export async function submitMultiStrategyEvaluateTask(
|
||||
req: MultiStrategyBacktestRequest,
|
||||
): Promise<TaskSubmitResponse> {
|
||||
const resp = await fetch(`${BASE}/backtest/multi-strategy/evaluate/run/async`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
if (!resp.ok) await throwError(resp)
|
||||
return (await resp.json()) as TaskSubmitResponse
|
||||
}
|
||||
|
||||
/** 提交参数网格寻优后台任务,返回 task_id。 */
|
||||
export async function submitOptimizeTask(
|
||||
req: OptimizeBacktestRequest,
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
submitOptimizeAllTask,
|
||||
submitOptimizeTask,
|
||||
submitMultiStrategyTask,
|
||||
submitMultiStrategyEvaluateTask,
|
||||
submitMultiStrategyWalkforwardTask,
|
||||
submitWalkforwardTask,
|
||||
submitEvaluateTask,
|
||||
fetchTask,
|
||||
@@ -255,6 +257,8 @@ export const useBacktestStore = defineStore('backtest', () => {
|
||||
multiStrategyRunning.value = true
|
||||
error.value = ''
|
||||
multiStrategyResult.value = null
|
||||
// 新一次组合回测开始时清掉上一轮的附加分析(WF/一条龙面板随主结果一起刷新)
|
||||
clearMultiStrategyExtraAnalysis()
|
||||
try {
|
||||
const { task_id } = await submitMultiStrategyTask(req)
|
||||
const start = Date.now()
|
||||
@@ -281,9 +285,66 @@ export const useBacktestStore = defineStore('backtest', () => {
|
||||
|
||||
function clearMultiStrategy() {
|
||||
multiStrategyResult.value = null
|
||||
clearMultiStrategyExtraAnalysis()
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
// ── 多策略组合附加分析:组合级 Walk-Forward / 一条龙评估 ──────────────────
|
||||
const multiWfResult = ref<WalkForwardResult | null>(null)
|
||||
const multiWfRunning = ref(false)
|
||||
const multiWfError = ref<string>('')
|
||||
const multiEvaluateResult = ref<EvaluateReport | null>(null)
|
||||
const multiEvaluateRunning = ref(false)
|
||||
const multiEvaluateError = ref<string>('')
|
||||
|
||||
/** 提交多策略组合级 WF 样本外验证后台任务并轮询(N 槽位 × N 窗,较慢)。 */
|
||||
async function runMultiStrategyWalkforward(req: MultiStrategyBacktestRequest, nWindows = 7) {
|
||||
multiWfRunning.value = true
|
||||
multiWfError.value = ''
|
||||
multiWfResult.value = null
|
||||
try {
|
||||
const { task_id } = await submitMultiStrategyWalkforwardTask(req, nWindows)
|
||||
const body = await pollTask<{ walkforward: WalkForwardResult }>(
|
||||
task_id,
|
||||
300_000,
|
||||
'组合 WF 验证',
|
||||
)
|
||||
multiWfResult.value = body.walkforward
|
||||
} catch (e) {
|
||||
multiWfError.value = formatError(e)
|
||||
multiWfResult.value = null
|
||||
} finally {
|
||||
multiWfRunning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交多策略组合级一条龙评估后台任务并轮询(组合回测+WF+适配性+评分+基准对比)。 */
|
||||
async function runMultiStrategyEvaluate(req: MultiStrategyBacktestRequest) {
|
||||
multiEvaluateRunning.value = true
|
||||
multiEvaluateError.value = ''
|
||||
multiEvaluateResult.value = null
|
||||
try {
|
||||
const { task_id } = await submitMultiStrategyEvaluateTask(req)
|
||||
multiEvaluateResult.value = await pollTask<EvaluateReport>(
|
||||
task_id,
|
||||
600_000,
|
||||
'组合一条龙评估',
|
||||
)
|
||||
} catch (e) {
|
||||
multiEvaluateError.value = formatError(e)
|
||||
multiEvaluateResult.value = null
|
||||
} finally {
|
||||
multiEvaluateRunning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearMultiStrategyExtraAnalysis() {
|
||||
multiWfResult.value = null
|
||||
multiWfError.value = ''
|
||||
multiEvaluateResult.value = null
|
||||
multiEvaluateError.value = ''
|
||||
}
|
||||
|
||||
// ── 参数网格寻优(Phase 4) ─────────────────────────────────────────────
|
||||
const optimizeResult = ref<OptimizeResult | null>(null)
|
||||
const optimizeRunning = ref(false)
|
||||
@@ -385,6 +446,12 @@ export const useBacktestStore = defineStore('backtest', () => {
|
||||
portfolioEvaluateError,
|
||||
multiStrategyResult,
|
||||
multiStrategyRunning,
|
||||
multiWfResult,
|
||||
multiWfRunning,
|
||||
multiWfError,
|
||||
multiEvaluateResult,
|
||||
multiEvaluateRunning,
|
||||
multiEvaluateError,
|
||||
optimizeResult,
|
||||
optimizeRunning,
|
||||
optimizeContext,
|
||||
@@ -413,6 +480,9 @@ export const useBacktestStore = defineStore('backtest', () => {
|
||||
clearPortfolioExtraAnalysis,
|
||||
runMultiStrategy,
|
||||
clearMultiStrategy,
|
||||
runMultiStrategyWalkforward,
|
||||
runMultiStrategyEvaluate,
|
||||
clearMultiStrategyExtraAnalysis,
|
||||
runOptimize,
|
||||
runOptimizeAll,
|
||||
setOptimizeContext,
|
||||
|
||||
@@ -6,20 +6,24 @@
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import AiInterpretModal from '../components/AiInterpretModal.vue'
|
||||
import EquityChart from '../components/EquityChart.vue'
|
||||
import EvaluatePanel from '../components/EvaluatePanel.vue'
|
||||
import GradeDetails from '../components/GradeDetails.vue'
|
||||
import MetricTable from '../components/MetricTable.vue'
|
||||
import PortfolioCompareChart from '../components/PortfolioCompareChart.vue'
|
||||
import PortfolioSummaryTable from '../components/PortfolioSummaryTable.vue'
|
||||
import WalkForwardPanel from '../components/WalkForwardPanel.vue'
|
||||
import {
|
||||
deleteSavedStrategy,
|
||||
fetchSavedStrategies,
|
||||
formatError,
|
||||
saveStrategy,
|
||||
} from '../api'
|
||||
import { gradePortfolio } from '../grading'
|
||||
import { buildPortfolioAiPrompt } from '../aiPrompt'
|
||||
import { GRADE_META, gradePortfolio } from '../grading'
|
||||
import { detectMarket } from '../market'
|
||||
import type { MultiStrategyItem, Performance, SavedStrategy } from '../types'
|
||||
import type { Category, MultiStrategyItem, Performance, SavedStrategy } from '../types'
|
||||
import { useBacktestStore } from '../stores/backtest'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -380,8 +384,9 @@ const holdingViews = computed<HoldingView[]>(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
// 组合整体绩效(19 项指标)。后端 total_performance 现含完整指标,转成
|
||||
// MetricTable 需要的 Performance 类型(缺失字段补 0 兜底,保证渲染不崩)。
|
||||
// 组合整体绩效(25 项指标,与单标的 MetricTable 同口径)。后端
|
||||
// total_performance 含完整指标,转成 MetricTable 需要的 Performance 类型
|
||||
// (缺失字段补 0 兜底,保证老结果渲染不崩)。
|
||||
const comboPerf = computed<Performance | null>(() => {
|
||||
const tp = store.multiStrategyResult?.total_performance
|
||||
if (!tp) return null
|
||||
@@ -409,6 +414,12 @@ const comboPerf = computed<Performance | null>(() => {
|
||||
max_loss: get('max_loss'),
|
||||
avg_holding_days: get('avg_holding_days'),
|
||||
volatility: get('volatility'),
|
||||
ulcer_index: get('ulcer_index'),
|
||||
var_95: get('var_95'),
|
||||
cvar_95: get('cvar_95'),
|
||||
sqn: get('sqn'),
|
||||
max_consecutive_wins: get('max_consecutive_wins'),
|
||||
max_consecutive_losses: get('max_consecutive_losses'),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -417,6 +428,69 @@ const comboPerf = computed<Performance | null>(() => {
|
||||
const comboGrade = computed(() =>
|
||||
store.multiStrategyResult ? gradePortfolio(store.multiStrategyResult) : null,
|
||||
)
|
||||
|
||||
// ── 组合附加分析:组合级 Walk-Forward / 一条龙 / AI 解读 ─────────────────────
|
||||
// 与单标的/组合回测页同构;按钮在组合结果区按需触发(复用最近一次
|
||||
// 组合回测的 items/cash,区间保持不变——检验的是"这组配置"的稳定性)。
|
||||
|
||||
function multiComboRequest() {
|
||||
return { items: lastComboItems.value, cash: lastComboCash.value }
|
||||
}
|
||||
|
||||
async function onComboWf() {
|
||||
if (lastComboItems.value.length === 0) return
|
||||
await store.runMultiStrategyWalkforward(multiComboRequest())
|
||||
}
|
||||
|
||||
async function onComboEvaluate() {
|
||||
if (lastComboItems.value.length === 0) return
|
||||
await store.runMultiStrategyEvaluate(multiComboRequest())
|
||||
}
|
||||
|
||||
// AI 解读:组合版 Prompt(multi 模式:N 个策略各跑各自的原标的)
|
||||
const showAiModal = ref(false)
|
||||
|
||||
const comboStrategyLabel = computed(() =>
|
||||
lastComboItems.value.length > 0
|
||||
? `${lastComboItems.value.length} 策略组合`
|
||||
: '策略组合',
|
||||
)
|
||||
|
||||
const aiPromptText = computed(() => {
|
||||
if (!store.multiStrategyResult) return ''
|
||||
return buildPortfolioAiPrompt({
|
||||
stocks: lastComboItems.value.map(
|
||||
(it) => `${it.strategy_label || it.strategy}@${it.symbol}`,
|
||||
),
|
||||
category: (lastComboItems.value[0]?.category as Category) ?? 'DAY',
|
||||
startDate: (lastComboItems.value[0]?.start_date as string) || '',
|
||||
endDate: (lastComboItems.value[0]?.end_date as string) || isoToday(),
|
||||
strategyLabel: comboStrategyLabel.value,
|
||||
params: {},
|
||||
cash: lastComboCash.value,
|
||||
commission: 0.0003,
|
||||
slippage: 0,
|
||||
execution: 'next_open',
|
||||
result: store.multiStrategyResult,
|
||||
mode: 'multi',
|
||||
wf: store.multiWfResult,
|
||||
evaluate: store.multiEvaluateResult,
|
||||
grade: comboGrade.value,
|
||||
gradeHint: comboGrade.value ? GRADE_META[comboGrade.value.grade].hint : undefined,
|
||||
})
|
||||
})
|
||||
|
||||
/** 随解读落历史库的策略上下文(历史页「去回测」引导用) */
|
||||
const aiContext = computed(() => ({
|
||||
strategy: 'multi',
|
||||
strategy_label: comboStrategyLabel.value,
|
||||
kind: 'multi',
|
||||
symbol: lastComboItems.value.map((it) => it.symbol).join(','),
|
||||
category: (lastComboItems.value[0]?.category as string) || 'DAY',
|
||||
params: {},
|
||||
start_date: (lastComboItems.value[0]?.start_date as string) || '',
|
||||
end_date: isoToday(),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -631,10 +705,30 @@ const comboGrade = computed(() =>
|
||||
· {{ store.multiStrategyResult.total_performance.total_stocks }} 个策略 ·
|
||||
总资金 {{ store.multiStrategyResult.total_performance.total_cash.toFixed(0) }}
|
||||
</span>
|
||||
<span v-if="store.multiStrategyResult && !store.multiStrategyRunning" class="combo-extra-actions">
|
||||
<button
|
||||
class="save-combo-btn"
|
||||
:disabled="store.multiWfRunning"
|
||||
title="按全部槽位日期并集切窗,每窗各策略独立回测后合成组合净值,检验跨时段稳定性"
|
||||
@click="onComboWf"
|
||||
>
|
||||
{{ store.multiWfRunning ? 'WF 验证中…' : '🔬 WF 样本外验证' }}
|
||||
</button>
|
||||
<button
|
||||
class="save-combo-btn"
|
||||
:disabled="store.multiEvaluateRunning"
|
||||
title="组合回测+组合WF+跨策略适配性体检+综合评分+等权买入持有基准对比,一份报告"
|
||||
@click="onComboEvaluate"
|
||||
>
|
||||
{{ store.multiEvaluateRunning ? '评估中…' : '📋 一条龙评估' }}
|
||||
</button>
|
||||
<button class="save-combo-btn" @click="showAiModal = true">🤖 AI 解读</button>
|
||||
<button class="save-combo-btn" @click="openSaveCombo">💾 保存为组合</button>
|
||||
</span>
|
||||
<button
|
||||
v-if="store.multiStrategyResult && !store.multiStrategyRunning"
|
||||
v-else-if="store.multiStrategyResult && store.multiStrategyRunning"
|
||||
class="save-combo-btn"
|
||||
@click="openSaveCombo"
|
||||
disabled
|
||||
>
|
||||
💾 保存为组合
|
||||
</button>
|
||||
@@ -674,6 +768,42 @@ const comboGrade = computed(() =>
|
||||
<EquityChart :equity="store.multiStrategyResult.combined_equity" />
|
||||
</div>
|
||||
|
||||
<!-- 附加分析:组合级 Walk-Forward(与单标的/组合回测页同构面板) -->
|
||||
<div
|
||||
v-if="store.multiWfRunning || store.multiWfResult || store.multiWfError"
|
||||
class="combo-chart-block"
|
||||
>
|
||||
<h4>Walk-Forward 样本外验证</h4>
|
||||
<p v-if="store.multiWfRunning" class="empty-text">
|
||||
验证中…(策略数 × 窗口数 次回测,约需十几秒)
|
||||
</p>
|
||||
<div v-else-if="store.multiWfError" class="warn-box">
|
||||
⚠ {{ store.multiWfError }}
|
||||
</div>
|
||||
<WalkForwardPanel v-else-if="store.multiWfResult" :wf="store.multiWfResult" />
|
||||
</div>
|
||||
|
||||
<!-- 附加分析:组合级一条龙评估(与单标的/组合回测页同构面板) -->
|
||||
<div
|
||||
v-if="
|
||||
store.multiEvaluateRunning || store.multiEvaluateResult || store.multiEvaluateError
|
||||
"
|
||||
class="combo-chart-block"
|
||||
>
|
||||
<h4>一条龙评估</h4>
|
||||
<p v-if="store.multiEvaluateRunning" class="empty-text">
|
||||
评估中…(组合回测 + 组合WF + 跨策略适配性 + 基准对比,可能需要一两分钟)
|
||||
</p>
|
||||
<div v-else-if="store.multiEvaluateError" class="warn-box">
|
||||
⚠ {{ store.multiEvaluateError }}
|
||||
</div>
|
||||
<EvaluatePanel
|
||||
v-else-if="store.multiEvaluateResult"
|
||||
:report="store.multiEvaluateResult"
|
||||
:grade-override="comboGrade"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="comboPerf" class="combo-chart-block">
|
||||
<h4>绩效指标</h4>
|
||||
<MetricTable :perf="comboPerf" />
|
||||
@@ -747,6 +877,16 @@ const comboGrade = computed(() =>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- AI 解读 Prompt 对话框(单标的/组合/策略组合通用组件) -->
|
||||
<AiInterpretModal
|
||||
v-if="showAiModal && store.multiStrategyResult"
|
||||
:prompt="aiPromptText"
|
||||
:filename="`AI解读_${comboStrategyLabel}.md`"
|
||||
:context="aiContext"
|
||||
tip="建议先点「WF 样本外验证」「一条龙评估」,跑完再打开此弹窗,数据会一并打包。"
|
||||
@close="showAiModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1184,6 +1324,22 @@ const comboGrade = computed(() =>
|
||||
.save-combo-btn:hover {
|
||||
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||
}
|
||||
.save-combo-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
/* 组合结果区附加分析按钮组(WF/一条龙/AI 解读/保存):内联排列,窄屏可换行 */
|
||||
.combo-extra-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 10px;
|
||||
flex-wrap: wrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.combo-extra-actions .save-combo-btn {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 警示条基础类(过拟合 / 免责共享) */
|
||||
.warn-box {
|
||||
|
||||
Reference in New Issue
Block a user