Files
easy_tdx_max/web-ui/src/aiPrompt.ts
T
GitHub 497ac21e5a 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)
2026-09-03 23:54:29 +08:00

501 lines
23 KiB
TypeScript
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.
// AI 解读 Prompt 生成器:把单标的回测报告组装成一段结构化提示词,
// 用户复制后发给任意 LLMChatGPT / Claude / DeepSeek / 豆包…)即可获得针对性解读。
//
// 约束:本文件只允许 **type-only** 的本地导入(编译期擦除),
// 保证 node --test 可直跑(与 grading/__tests__ 同一套自检方式);
// 运行时逻辑全部自包含,不依赖其他模块的值。
//
// 文案口径与后端对齐:指标含义见 src/data/glossary.ts
// 评分权重见 scoring.pyWF/体检语义见 walkforward.py / fitness.py。
import type {
BacktestResult,
Category,
EquityPoint,
EvaluateReport,
ExecutionMode,
Performance,
PortfolioResult,
PortfolioTrade,
Trade,
WalkForwardResult,
} from './types'
import type { GradeResult } from './grading/types'
// ── 输入 ─────────────────────────────────────────────────────────────────────
export interface AiPromptInput {
/** 完整标的代码(带市场前缀,如 "SZ:000001" */
symbol: string
category: Category
startDate: string
endDate: string
/** K 线根数(store.ohlcv.length */
bars: number
/** 策略中文名(如「双均线交叉」) */
strategyLabel: string
params: Record<string, number | string | boolean>
cash: number
commission: number
slippage: number
execution: ExecutionMode
result: BacktestResult
/** 附加分析(未勾选/未跑完时传 null,对应段落自动省略) */
wf?: WalkForwardResult | null
evaluate?: EvaluateReport | null
grade?: GradeResult | null
/** 评级档位的一句话含义(GRADE_META[grade].hint,由组件传入) */
gradeHint?: string
}
export interface PortfolioAiPromptInput {
/** 完整标的代码列表(带市场前缀,如 ["SZ:000001", "SH:600519"]);
* multi 模式下传「策略@标的」明细列表(如 ["双均线@SH:601088", …] */
stocks: string[]
category: Category
startDate: string
endDate: string
strategyLabel: string
params: Record<string, number | string | boolean>
cash: number
commission: number
slippage: number
execution: ExecutionMode
result: PortfolioResult
/** 组合形态:portfolio = 一个策略 × 多只标的(默认);
* multi = 多个策略各跑各自的原标的(策略库「重跑到今天」) */
mode?: 'portfolio' | 'multi'
/** 附加分析(未勾选/未跑完时传 null,对应段落自动省略) */
wf?: WalkForwardResult | null
evaluate?: EvaluateReport | null
grade?: GradeResult | null
/** 评级档位的一句话含义(GRADE_META[grade].hint,由组件传入) */
gradeHint?: string
}
// ── 展示辅助(自包含,避免运行时依赖其他模块)────────────────────────────────
const CATEGORY_LABELS: Record<Category, string> = {
DAY: '日线',
WEEK: '周线',
MONTH: '月线',
MIN_5: '5 分钟',
MIN_15: '15 分钟',
MIN_30: '30 分钟',
MIN_60: '60 分钟',
MIN_120: '120 分钟',
}
const EXECUTION_LABELS: Record<ExecutionMode, string> = {
next_open: '次日开盘价',
next_close: '次日收盘价',
}
/** 与 MetricTable 同源同序的 25 项指标清单(label + 数字格式) */
const METRIC_LINES: Array<{
key: keyof Performance
label: string
format: 'percent' | 'ratio' | 'int' | 'days'
group: string
}> = [
{ key: 'total_return', label: '总收益率', format: 'percent', group: '收益' },
{ key: 'annual_return', label: '年化收益', format: 'percent', group: '收益' },
{ key: 'sharpe', label: '夏普比率', format: 'ratio', group: '收益' },
{ key: 'sortino', label: '索提诺比率', format: 'ratio', group: '收益' },
{ key: 'calmar', label: '卡玛比率', format: 'ratio', group: '收益' },
{ key: 'max_drawdown', label: '最大回撤', format: 'percent', group: '风险' },
{ key: 'max_dd_duration', label: '回撤持续', format: 'days', group: '风险' },
{ key: 'volatility', label: '波动率(年化)', format: 'percent', group: '风险' },
{ key: 'ulcer_index', label: 'Ulcer 指数', format: 'percent', group: '风险' },
{ key: 'var_95', label: '日 VaR (95%)', format: 'percent', group: '风险' },
{ key: 'cvar_95', label: '日 CVaR (95%)', format: 'percent', group: '风险' },
{ key: 'total_trades', label: '总交易数', format: 'int', group: '交易' },
{ key: 'win_trades', label: '盈利次数', format: 'int', group: '交易' },
{ key: 'lose_trades', label: '亏损次数', format: 'int', group: '交易' },
{ key: 'win_rate', label: '胜率', format: 'percent', group: '交易' },
{ key: 'profit_factor', label: '盈亏比(利润因子)', format: 'ratio', group: '交易' },
{ key: 'avg_win', label: '平均盈利', format: 'percent', group: '交易' },
{ key: 'avg_loss', label: '平均亏损', format: 'percent', group: '交易' },
{ key: 'max_win', label: '最大盈利', format: 'percent', group: '交易' },
{ key: 'max_loss', label: '最大亏损', format: 'percent', group: '交易' },
{ key: 'avg_holding_days', label: '平均持仓天数', format: 'ratio', group: '交易' },
{ key: 'sqn', label: 'SQN 系统质量', format: 'ratio', group: '交易' },
{ key: 'max_consecutive_wins', label: '最大连胜', format: 'int', group: '交易' },
{ key: 'max_consecutive_losses', label: '最大连亏', format: 'int', group: '交易' },
{ key: 'rejected_trades', label: '拒单数', format: 'int', group: '交易' },
]
function fmtMetric(format: 'percent' | 'ratio' | 'int' | 'days', v: number | undefined): string {
if (v === undefined || v === null || !Number.isFinite(v)) return '-'
if (format === 'percent') return `${(v * 100).toFixed(2)}%`
if (format === 'int') return String(Math.round(v))
if (format === 'days') return `${v.toFixed(0)} 天`
return v.toFixed(3)
}
function pct(v: number | undefined | null): string {
if (v === undefined || v === null || !Number.isFinite(v)) return '-'
return `${v >= 0 ? '+' : ''}${(v * 100).toFixed(2)}%`
}
function ratio(v: number | undefined | null): string {
if (v === undefined || v === null || !Number.isFinite(v)) return '-'
return v.toFixed(2)
}
function fmtParams(params: Record<string, number | string | boolean>): string {
const entries = Object.entries(params)
if (entries.length === 0) return '(默认参数)'
return entries.map(([k, v]) => `${k}=${v}`).join(', ')
}
function fmtMoney(v: number): string {
return v.toLocaleString('zh-CN', { maximumFractionDigits: 0 })
}
/** "2025-06-23T00:00:00" → "2025-06-23"(截掉时间部分,提示词更整洁) */
function fmtDate(dt: string): string {
return dt.slice(0, 10)
}
/** WF 窗口字段防御性转数字:老版本后端曾把有限 float 序列化成字符串
* (如 "-0.2078"),Number() 对数字原值透明、对字符串强制转换、非法值给 NaN。 */
function n(v: number | string | null | undefined): number | undefined {
const x = Number(v)
return Number.isFinite(x) ? x : undefined
}
// ── 各段落构建 ───────────────────────────────────────────────────────────────
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 [
'# 角色设定',
'',
'你是一位做了十几年量化交易的老手,说话直来直去,心是好的。现在你正和一个刚入门的朋友(我)聊天,我什么都不懂,你解释什么都要说人话。',
'',
'# 任务',
'',
intro,
'',
'1. **先给结论**:这策略现在处于什么状态——「可以继续往下走」「底子不错但还差几步」还是「问题不小,得大改」?一句话说清,再讲理由;',
'2. **优点和毛病都要讲**:先说说它强在哪(哪些数字是真的好看、说明策略做对了什么),再讲你担心什么。别只挑刺,也别光报喜——我是想知道这策略能不能用,不是来听审判也不是来听表扬的。挑最有说服力的几组数字讲,不用面面俱到;',
'3. **说说持有体验**:真拿钱跑这个策略,过程大概什么感受——多久交易一次、最惨的时候有多惨、普通人拿不拿得住;',
'4. **判断是规律还是运气**:从分时段数据(Walk-Forward 各窗收益、训练/验证/测试三段、和死拿不动的对比)找证据。有担心就直说,但像朋友提醒那样说,别像下判决书;',
step5,
'6. **最后打个分**:给这个策略一个 0-10 的「信心分」,代表你现在有多大把握它值得继续投入。打分要和前面说的话一致(前面夸的多就别打低分,反过来也一样),再用一两句话说说为什么是这个分、到几分你会建议我拿小仓位试试。参考刻度:0-3 建议放弃,4-6 值得继续改(说清往哪改),7-8 可以小仓位试错,9 以上才谈逐步加仓。',
'',
'# 说话方式(很重要)',
'',
'- 像靠谱朋友给建议:直接、坦诚,但不刻薄、不吓唬人。指出问题是为了帮我做决定,不是逞口舌之快;',
'- 别像写报告:不要一堆小标题、密集加粗和列表堆砌,自然分段,最关键的结论偶尔加粗就行;',
'- 禁止八股句式:「事实是」「我的推断是」「总的来说」「综上所述」「作为你的…」「好的,收到」这类全部不要;不确定的地方自然地说「我猜」「大概率」,不要贴标签;',
'- 不要客套开场,也不要结尾祝词,上来就说正事,说完就停;',
'- 全文控制在 800 字以内,一屏读完,句句有用;宁可少讲两点,也不要注水;',
'- 只引用报告里有的数字,报告里没有的不要臆造;要估计就明说是估计;',
'- 专业词第一次出现时顺口解释一句,别让我再去查。',
'',
'参考语气:「这策略胜率近八成、盈亏比四倍,底子是好的;但我最担心的是最近两年基本没赚钱——测试段只涨了 0.16%,这就是我不敢给高分的主要原因。」',
'',
].join('\n')
}
function sectionConfig(i: AiPromptInput): string {
const lines = [
'# 回测配置',
'',
`- 标的:${i.symbol}${CATEGORY_LABELS[i.category] ?? i.category}`,
`- 回测区间:${i.startDate} ~ ${i.endDate}(共 ${i.bars} 根 K 线)`,
`- 策略:${i.strategyLabel}`,
`- 参数:${fmtParams(i.params)}`,
`- 初始资金:${fmtMoney(i.cash)} 元;佣金 ${i.commission};滑点 ${i.slippage};成交价:${EXECUTION_LABELS[i.execution] ?? i.execution}`,
'',
]
return lines.join('\n')
}
function sectionMetrics(perf: Performance): string {
const lines = ['# 绩效指标', '']
for (const g of ['收益', '风险', '交易']) {
lines.push(`**${g}类**`)
for (const m of METRIC_LINES.filter((x) => x.group === g)) {
lines.push(`- ${m.label}${fmtMetric(m.format, perf[m.key] as number | undefined)}`)
}
lines.push('')
}
return lines.join('\n')
}
function sectionEquity(result: BacktestResult): string {
return sectionEquityPoints(result.equity_curve)
}
function sectionEquityPoints(eq: EquityPoint[] | undefined): string {
if (!eq || eq.length === 0) return ''
let peak = eq[0]
let trough = eq[0]
for (const p of eq) {
if (p.total > peak.total) peak = p
if (p.total < trough.total) trough = p
}
return [
'# 净值概览',
'',
`- 期初资产:${fmtMoney(eq[0].total)} 元(${fmtDate(eq[0].datetime)}`,
`- 期末资产:${fmtMoney(eq[eq.length - 1].total)} 元(${fmtDate(eq[eq.length - 1].datetime)}`,
`- 峰值:${fmtMoney(peak.total)} 元(${fmtDate(peak.datetime)});谷值:${fmtMoney(trough.total)} 元(${fmtDate(trough.datetime)}`,
'',
].join('\n')
}
function sectionWf(wf: WalkForwardResult): string {
if (!wf || wf.windows.length === 0) return ''
const lines = [
'# Walk-Forward 样本外验证(同参数跨时段稳定性)',
'',
`- 窗口数:${wf.n_windows};前 ${(wf.warmup_ratio * 100).toFixed(0)}% 数据为预热区不参与评估;每窗从空仓独立开仓`,
`- 盈利窗占比:${(wf.consistency * 100).toFixed(0)}%${wf.windows.filter((w) => (n(w.total_return) ?? 0) > 0).length}/${wf.windows.length}`,
`- 连乘收益(各窗复利衔接):${pct(wf.chained_return)}`,
`- 最差窗 / 最好窗:${pct(wf.worst_window)} / ${pct(wf.best_window)};平均夏普:${ratio(wf.mean_sharpe)};总交易:${wf.total_trades} 笔`,
'',
'逐窗收益(时间升序,红涨绿跌口径无关,正=赚):',
'',
]
for (const w of wf.windows) {
lines.push(
`- 窗${w.index + 1}${w.start} ~ ${w.end}):${pct(n(w.total_return))},夏普 ${ratio(n(w.sharpe))},最大回撤 ${pct(n(w.max_drawdown))}${w.total_trades} 笔(胜率 ${pct(n(w.win_rate))}`,
)
}
lines.push('')
return lines.join('\n')
}
function sectionEvaluate(ev: EvaluateReport): string {
const lines = ['# 一条龙评估', '']
// 综合评分
const labels: Record<string, string> = {
total_return: '收益',
sharpe: '夏普',
max_drawdown: '回撤',
sortino: '索提诺',
wf_consistency: 'WF 一致性',
}
lines.push(`**综合评分:${ev.score.total.toFixed(1)} / 100**(权重:${Object.entries(ev.score.weights_used)
.map(([k, w]) => `${labels[k] ?? k} ${(w * 100).toFixed(0)}%`)
.join(' + ')}`)
lines.push(`- 分项:${Object.entries(ev.score.components)
.map(([k, v]) => `${labels[k] ?? k} ${v.toFixed(0)}`)
.join('')}`)
lines.push('')
// 基准对比
const b = ev.benchmark
lines.push('**对比买入持有基准**')
lines.push(`- 策略总收益 ${pct(ev.performance.total_return)} vs 买入持有 ${pct(b.buy_hold.total_return)},超额收益 ${pct(b.excess_return)}`)
if (b.alpha !== undefined || b.beta !== undefined) {
lines.push(
`- α(年化超额)${pct(b.alpha)};β(敏感度)${ratio(b.beta)};信息比率 ${ratio(b.information_ratio)};跟踪误差 ${pct(b.tracking_error)}`,
)
}
lines.push(`- 买入持有基准:年化 ${pct(b.buy_hold.annual_return)},最大回撤 ${pct(b.buy_hold.max_drawdown)},夏普 ${ratio(b.buy_hold.sharpe)},卡玛 ${ratio(b.buy_hold.calmar)},波动率 ${pct(b.buy_hold.volatility)}`)
lines.push('')
// 适配性体检
const f = ev.fitness
lines.push(
`**适配性体检:${f.passed_count}/${f.total_checks} 通过(train/valid/test = ${f.split.map((s) => (s * 100).toFixed(0)).join('/')}${f.high_fitness ? ',达到「高适配」' : ''}**`,
)
for (const c of f.checks) {
lines.push(`- ${c.passed ? '✓' : '✗'} ${c.detail}`)
}
lines.push('')
return lines.join('\n')
}
function sectionGrade(grade: GradeResult, hint: string | undefined): string {
const lines = [
'# 评级(不看收益率,面向「普通人拿不拿得住」)',
'',
`- 档位:**${grade.grade}**(总分 ${grade.score.toFixed(1)}/100${hint ? `——${hint}` : ''}`,
]
if (grade.dimensions.length > 0) {
lines.push(
`- 维度得分:${grade.dimensions
.map((d) => `${d.label} ${d.score.toFixed(0)}/100(权重 ${(d.weight * 100).toFixed(0)}%`)
.join('')}`,
)
}
if (grade.vetoes.length > 0) {
lines.push(`- ⚠ 一票否决:${grade.vetoes.map((v) => v.reason).join('')}`)
}
if (grade.insufficientSample) {
lines.push('- ⚠ 交易样本有限(< 10 笔),胜率/盈亏比已降权处理')
}
if (grade.isLosing) {
lines.push('- ⚠ 系统亏损(利润因子 < 1)')
}
lines.push('')
return lines.join('\n')
}
function sectionTrades(trades: Trade[]): string {
const recent = trades.slice(-8)
if (recent.length === 0) return ''
const lines = ['# 最近成交(最后 8 笔)', '']
for (const t of recent) {
const dir = t.direction === 'BUY' ? '买入' : '卖出'
const pnl = t.direction === 'SELL' && t.pnl !== 0 ? `,本笔盈亏 ${t.pnl >= 0 ? '+' : ''}${fmtMoney(t.pnl)} 元` : ''
lines.push(`- ${fmtDate(t.datetime)} ${dir} ${Math.round(t.size)} 股 @ ${t.price.toFixed(2)}${pnl}`)
}
lines.push('')
return lines.join('\n')
}
function sectionFooter(): string {
return [
'# 背景与免责',
'',
'以上数据来自 easy-tdx 的历史 K 线回测(已计入佣金与滑点)。历史回测存在幸存者偏差与未来不确定性,不构成投资建议,你的解读也以研究学习为目的。',
'数据里缺失的项(显示 - 或整段没有的)直接跳过,不用专门解释局限。',
'好了,开始吧。',
'# 重要提醒',
'禁止使用状语',
].join('\n')
}
// ── 组合版段落 ───────────────────────────────────────────────────────────────
function sectionPortfolioConfig(i: PortfolioAiPromptInput): string {
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, multi = false): string {
const entries = Object.entries(result.individual_results)
if (entries.length === 0) return ''
const sorted = entries
.map(([symbol, r]) => ({ symbol, perf: r.performance }))
.sort((a, b) => (b.perf.total_return ?? 0) - (a.perf.total_return ?? 0))
const shown =
sorted.length <= 12
? sorted
: [...sorted.slice(0, 6), ...sorted.slice(sorted.length - 6)]
const what = multi ? '各策略槽位' : '各标的'
const lines = [
`# ${what}表现(按收益降序;` +
(sorted.length <= 12 ? '全部' : `省略中间 ${sorted.length - 12} 个,其余为最好/最差各 6 个`) +
'',
'',
]
for (const { symbol, perf } of shown) {
lines.push(
`- ${symbol}:总收益 ${pct(perf.total_return)},最大回撤 ${pct(perf.max_drawdown)},夏普 ${ratio(perf.sharpe)}${Math.round(perf.total_trades ?? 0)} 笔(胜率 ${pct(perf.win_rate)}`,
)
}
lines.push('')
return lines.join('\n')
}
function sectionPortfolioTrades(trades: PortfolioTrade[] | undefined): string {
if (!trades || trades.length === 0) return ''
const recent = trades.slice(-8)
const lines = ['# 最近成交(组合合计的最后 8 笔)', '']
for (const t of recent) {
const dir = t.direction === 'BUY' ? '买入' : '卖出'
const pnl =
t.direction === 'SELL' && t.pnl !== 0 ? `,本笔盈亏 ${t.pnl >= 0 ? '+' : ''}${fmtMoney(t.pnl)} 元` : ''
lines.push(`- ${t.symbol} ${fmtDate(t.datetime)} ${dir} ${Math.round(t.size)} 股 @ ${t.price.toFixed(2)}${pnl}`)
}
lines.push('')
return lines.join('\n')
}
// ── 主函数 ───────────────────────────────────────────────────────────────────
/** 组装 AI 解读 Promptmarkdown 结构,任意 LLM 可直接消费)。 */
export function buildAiPrompt(input: AiPromptInput): string {
const parts: string[] = [
sectionRole(),
sectionConfig(input),
sectionMetrics(input.result.performance),
sectionEquity(input.result),
]
if (input.wf) parts.push(sectionWf(input.wf))
if (input.evaluate) parts.push(sectionEvaluate(input.evaluate))
if (input.grade) parts.push(sectionGrade(input.grade, input.gradeHint))
const trades = sectionTrades(input.result.trades)
if (trades) parts.push(trades)
parts.push(sectionFooter())
return parts.join('\n')
}
/** 组装组合回测的 AI 解读 Prompt(与单标的同构,段落随附加分析增减)。 */
export function buildPortfolioAiPrompt(input: PortfolioAiPromptInput): string {
const multi = input.mode === 'multi'
const parts: string[] = [
sectionRole(multi ? 'multi' : 'portfolio'),
sectionPortfolioConfig(input),
sectionMetrics(input.result.total_performance),
sectionEquityPoints(input.result.combined_equity),
]
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))
if (input.grade) parts.push(sectionGrade(input.grade, input.gradeHint))
const trades = sectionPortfolioTrades(input.result.trades)
if (trades) parts.push(trades)
parts.push(sectionFooter())
return parts.join('\n')
}