优化AI解读提示词

This commit is contained in:
dongzp 2026-07-25 12:49:08 +08:00
parent cf9b9c5d74
commit 3e2e2ffc90
3 changed files with 419 additions and 48 deletions

View File

@ -1,4 +1,16 @@
import type { AnalysisSnapshot } from '../../stores/quota' import type { AnalysisSnapshot } from '../../stores/quota'
import type {
ChangeRiskItem,
MainForceTrendItem,
NewsItem,
PositionAnomalyItem,
PositionRow,
PositionsData,
ProfitRow,
QuoteData,
TradeTick,
} from '../../types'
import { toAnomalySections } from '../jqka/mapAnomaly'
import type { AiAnalysisPayload } from './types' import type { AiAnalysisPayload } from './types'
const OUTPUT_SCHEMA = `{ const OUTPUT_SCHEMA = `{
@ -9,9 +21,18 @@ const OUTPUT_SCHEMA = `{
"reasons": ["理由1", "理由2", "理由3", "理由4"] "reasons": ["理由1", "理由2", "理由3", "理由4"]
}` }`
const POSITION_TOP_N = 10
const NEWS_TOP_N = 20
const INTRADAY_TAIL = 120
const CANDLE_DAY_TAIL = 60
/** 主力全市场列表:当前品种优先后截断 */
const TRACE_RISK_LIMIT = 12
const TRACE_TREND_LIMIT = 16
const ANOMALY_PER_SECTION = 5
/** /**
* 将行情 / 新闻 / 持仓格式化规则数据拼成 DeepSeek messages。 * 将行情 / 新闻 / 持仓格式化规则数据拼成 DeepSeek messages。
* keywords 作为 system 角色设定;snapshot 作为 user 侧事实数据。 * keywords 作为 system 角色设定;其余按 UI 关键字文案提交,分时/K 线保留 JSON。
*/ */
export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
role: 'system' | 'user' role: 'system' | 'user'
@ -19,44 +40,372 @@ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
}> { }> {
const system = [ const system = [
payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。', payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。',
'请仅基于用户提供的行情、盘口、新闻、机构持仓、主力追踪(变盘预警、主力趋势机会)与主力异动数据给出交易建议。', '请仅基于用户提供的行情、盘口、大单分析、新闻、机构持仓、主力追踪(变盘预警、主力趋势机会)与主力异动数据给出交易建议。',
'主力追踪与主力异动为全市场数据,请优先关注与当前合约品种相关的条目,并结合整体主力动向综合判断。', '主力追踪与主力异动为全市场数据,请优先关注标注为「当前品种」的条目,并结合整体主力动向综合判断。',
'不要编造未提供的数据;信息不足时偏向观望并说明原因。', '文案中的单位与页面一致(手、万手、万、亿、百分比);不要编造未提供的数据;信息不足时偏向观望并说明原因。',
'必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。', '必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。',
`JSON 格式:${OUTPUT_SCHEMA}`, `JSON 格式:${OUTPUT_SCHEMA}`,
].join('\n') ].join('\n')
const user = [
'以下为当前合约的格式化分析数据(JSON),请据此给出建议:',
JSON.stringify(slimSnapshot(payload.snapshot), null, 2),
].join('\n\n')
return [ return [
{ role: 'system', content: system }, { role: 'system', content: system },
{ role: 'user', content: user }, { role: 'user', content: buildAnalysisUserContent(payload) },
] ]
} }
/** /** 供调试:与提交给模型的 user 内容一致 */
* 压缩超长数组,保留分析关键字段,避免 prompt 过大。 export function buildAnalysisUserContent(payload: AiAnalysisPayload): string {
* 盘口、统计、持仓、新闻、主力追踪、主力异动完整保留;分时/成交/K 线做尾部截断。 const { snapshot, largeOrderLots, variety } = payload
*/ const sections = [
function slimSnapshot(snapshot: AnalysisSnapshot): AnalysisSnapshot { formatContract(snapshot),
const quote = snapshot.quote formatQuoteStats(snapshot.quote),
if (!quote) return snapshot formatOrderBook(snapshot.quote),
formatLargeOrder(snapshot.quote?.trades ?? [], largeOrderLots),
formatIntradayJson(snapshot.quote),
formatCandlesJson(snapshot.quote),
formatPositions(snapshot.positions),
formatMainForceTrace(snapshot, variety),
formatPositionAnomaly(snapshot, variety),
formatNews(snapshot.news),
].filter(Boolean)
return { return [
...snapshot, `抓取时间:${snapshot.fetchedAt || '—'}`,
quote: { '以下为当前合约的分析数据(与页面展示一致的关键字文案;分时/K 线为 JSON),请据此给出建议:',
...quote, ...sections,
intraday: quote.intraday.slice(-120), ].join('\n\n')
trades: quote.trades.slice(-80), }
candles: {
day: quote.candles.day.slice(-60), function formatContract(snapshot: AnalysisSnapshot): string {
week: quote.candles.week.slice(-40), const c = snapshot.contract
month: quote.candles.month.slice(-36), const q = snapshot.quote
}, const lines = [
}, '【合约】',
news: snapshot.news.slice(0, 30), `名称:${q?.name || c.name || '—'}`,
} `代码:${q?.code || c.code || '—'}`,
`交易所:${q?.exchange || c.exchange || '—'}`,
]
if (q) {
lines.push(
`最新价:${fmtPrice(q.last)}`,
`涨跌:${fmtSigned(q.change)}(${fmtSigned(q.changePercent)}%)`,
`状态:${q.status || '—'}`,
`更新:${q.updatedAt || '—'}`,
)
}
return lines.join('\n')
}
function formatQuoteStats(quote: QuoteData | null): string {
if (!quote) return '【行情统计】\n暂无数据'
return [
'【行情统计】',
`开盘:${fmtPrice(quote.open)}`,
`最高:${fmtPrice(quote.high)}`,
`最低:${fmtPrice(quote.low)}`,
`昨收:${fmtPrice(quote.prevClose)}`,
`结算:${fmtPrice(quote.settlement)}`,
`昨结:${fmtPrice(quote.prevSettlement)}`,
`均价:${fmtPrice(quote.avg)}`,
`涨跌:${fmtSigned(quote.change)}`,
`持仓量:${fmtWan(quote.openInterest)}万`,
`成交量:${fmtWan(quote.volume)}万手`,
`成交额:${quote.amount}亿`,
`振幅:${quote.amplitude.toFixed(2)}%`,
`外盘:${fmtWan(quote.outerVol)}万`,
`内盘:${fmtWan(quote.innerVol)}万`,
`日增:${formatWanSigned(quote.amountDelta)}`,
].join('\n')
}
function formatOrderBook(quote: QuoteData | null): string {
if (!quote) return '【盘口】\n暂无数据'
const lines = [
'【盘口】',
`买盘占比:${quote.buyRatio}%`,
`卖盘占比:${quote.sellRatio}%`,
]
for (const ask of quote.orderBook.asks) {
lines.push(`卖${ask.level}:价格 ${fmtPrice(ask.price)},量 ${ask.volume}`)
}
for (const bid of quote.orderBook.bids) {
lines.push(`买${bid.level}:价格 ${fmtPrice(bid.price)},量 ${bid.volume}`)
}
return lines.join('\n')
}
function formatLargeOrder(trades: TradeTick[], lots: number): string {
const threshold = Math.max(0, Number(lots) || 0)
let largeBuy = 0
let largeSell = 0
let retailBuy = 0
let retailSell = 0
for (const t of trades) {
const isLarge = t.volume > threshold
if (t.side === 'B') {
if (isLarge) largeBuy += t.volume
else retailBuy += t.volume
} else if (isLarge) largeSell += t.volume
else retailSell += t.volume
}
const total = largeBuy + largeSell + retailBuy + retailSell
const pct = (v: number) => (total > 0 ? ((v / total) * 100).toFixed(0) : '0')
return [
'【大单分析】(分时成交汇总,非逐笔)',
`大单手数阈值:现手大于 ${threshold} 手计为大单`,
`大单主买:${largeBuy}手(${pct(largeBuy)}%)`,
`散单主买:${retailBuy}手(${pct(retailBuy)}%)`,
`大单主卖:${largeSell}手(${pct(largeSell)}%)`,
`散单主卖:${retailSell}手(${pct(retailSell)}%)`,
`合计:${total}手`,
].join('\n')
}
function formatIntradayJson(quote: QuoteData | null): string {
if (!quote) return '【分时图 JSON】\n[]'
const points = quote.intraday.slice(-INTRADAY_TAIL)
return ['【分时图 JSON】', JSON.stringify(points)].join('\n')
}
function formatCandlesJson(quote: QuoteData | null): string {
const day = quote?.candles.day.slice(-CANDLE_DAY_TAIL) ?? []
return ['【日K线 JSON】', JSON.stringify(day)].join('\n')
}
function formatPositions(data: PositionsData | null): string {
if (!data) return '【机构持仓】\n暂无数据'
const lines = [
'【机构持仓】',
`数据日期:${data.updatedAt || '—'}`,
'',
`成交量前${POSITION_TOP_N}名:`,
...formatPositionRows(data.volume, '成交量'),
'',
`净多持仓前${POSITION_TOP_N}名:`,
...formatPositionRows(data.netLong, '净持仓'),
'',
`净空持仓前${POSITION_TOP_N}名:`,
...formatPositionRows(data.netShort, '净持仓'),
'',
`盈利机构前${POSITION_TOP_N}名:`,
...formatProfitRows(data.profitGain),
'',
`亏损机构前${POSITION_TOP_N}名:`,
...formatProfitRows(data.profitLoss),
]
return lines.join('\n')
}
function formatPositionRows(list: PositionRow[], qtyLabel: string): string[] {
const top = list.slice(0, POSITION_TOP_N)
if (!top.length) return ['(无)']
return top.map(
(row) =>
`${row.rank}. ${row.name}|${qtyLabel} ${row.qty}|增减 ${fmtSignedInt(row.change)}`,
)
}
function formatProfitRows(list: ProfitRow[]): string[] {
const top = list.slice(0, POSITION_TOP_N)
if (!top.length) return ['(无)']
return top.map(
(row) =>
`${row.rank}. ${row.name}|盈利金额 ${formatProfit(row.profit)}|净持仓 ${row.netQty}|增减 ${fmtSignedInt(row.change)}`,
)
}
function formatMainForceTrace(snapshot: AnalysisSnapshot, variety: string): string {
const data = snapshot.mainForceTrace
if (!data) return '【主力追踪】\n暂无数据'
const riskList = takeVarietyFirst(data.changeRiskList, variety, TRACE_RISK_LIMIT)
const trendList = takeVarietyFirst(data.mainForceTrendList, variety, TRACE_TREND_LIMIT)
const lines = [
'【主力追踪】',
`数据日期:${data.date || '—'}`,
'',
'变盘预警:',
]
if (!riskList.length) lines.push('(无)')
else {
for (const row of riskList) {
lines.push(formatChangeRiskLine(row, variety))
}
}
lines.push('', '主力趋势机会:')
if (!trendList.length) lines.push('(无)')
else {
for (const row of trendList) {
lines.push(formatTrendLine(row, variety))
}
}
return lines.join('\n')
}
function formatChangeRiskLine(row: ChangeRiskItem, variety: string): string {
const tag = isCurrentVariety(row.variety, variety) ? '【当前品种】' : ''
return [
`${tag}${row.varietyName}(${row.variety})`,
`${row.actionLabel}${row.actionLots}手`,
`价格 ${formatTracePrice(row.linkClosePrice)}`,
`涨跌幅 ${formatSignedPct(row.percent)}`,
`变盘风险值 ${formatPct(row.changeRisk)}`,
].join('|')
}
function formatTrendLine(row: MainForceTrendItem, variety: string): string {
const tag = isCurrentVariety(row.variety, variety) ? '【当前品种】' : ''
const side = row.side === 'long' ? '多' : '空'
return [
`${tag}${row.varietyName}(${row.variety})`,
`机构 ${row.company}`,
`涨跌幅 ${formatSignedPct(row.percent)}`,
`单日${formatPct(row.mood)}流${side}`,
`单日加${side}${Math.abs(row.netChange)}手`,
`流${side}${formatFlowAbs(row.pupilFlow)}`,
`近一周胜率 ${formatPct(row.weekRate)}`,
`近一年盈利 ${formatProfitAbs(row.yearProfit)}`,
].join('|')
}
function formatPositionAnomaly(snapshot: AnalysisSnapshot, variety: string): string {
const data = snapshot.positionAnomaly
if (!data) return '【主力异动】\n暂无数据'
const lines = ['【主力异动】', `数据日期:${data.date || '—'}`]
for (const section of toAnomalySections(data)) {
lines.push('', `${section.title}:`)
const items = takeVarietyFirst(section.items, variety, ANOMALY_PER_SECTION)
if (!items.length) {
lines.push('(无)')
continue
}
items.forEach((row, idx) => {
lines.push(formatAnomalyLine(row, variety, idx + 1))
})
}
return lines.join('\n')
}
function formatAnomalyLine(
row: PositionAnomalyItem,
variety: string,
rank: number,
): string {
const tag = isCurrentVariety(row.variety, variety) ? '【当前品种】' : ''
return [
`${rank}. ${tag}${row.varietyName}(${row.variety})`,
`主导 ${row.maxCompanyName}`,
`增减 ${fmtSignedInt(row.netPositionChange)}`,
`增幅 ${fmtSigned(row.netPositionChangeRate)}%`,
`净持仓 ${fmtSignedInt(row.netPosition)}`,
].join('|')
}
function formatNews(news: NewsItem[]): string {
const top = news.slice(0, NEWS_TOP_N)
if (!top.length) return '【新闻】\n暂无数据'
const lines = ['【新闻】']
for (const item of top) {
const summary = item.summary?.trim()
lines.push(
summary
? `[${item.source} ${item.time}] ${item.title}|${summary}`
: `[${item.source} ${item.time}] ${item.title}`,
)
}
return lines.join('\n')
}
function takeVarietyFirst<T extends { variety: string }>(
list: T[],
variety: string,
limit: number,
): T[] {
const key = variety.trim().toUpperCase()
if (!list.length) return []
if (!key) return list.slice(0, limit)
const matched: T[] = []
const rest: T[] = []
for (const item of list) {
if (item.variety.toUpperCase() === key) matched.push(item)
else rest.push(item)
}
const merged = [...matched, ...rest]
return merged.slice(0, Math.max(limit, matched.length))
}
function isCurrentVariety(itemVariety: string, current: string): boolean {
const key = current.trim().toUpperCase()
return Boolean(key) && itemVariety.toUpperCase() === key
}
function fmtPrice(n: number): string {
return Number.isFinite(n) ? n.toFixed(2) : '—'
}
function fmtSigned(n: number): string {
if (!Number.isFinite(n)) return '—'
return `${n > 0 ? '+' : ''}${n}`
}
function fmtSignedInt(n: number): string {
if (!Number.isFinite(n)) return '—'
return `${n > 0 ? '+' : ''}${n}`
}
function fmtWan(hands: number): string {
return (hands / 10000).toFixed(2)
}
function formatWanSigned(hands: number): string {
const wan = hands / 10000
const sign = wan > 0 ? '+' : ''
return `${sign}${wan.toFixed(2)}万手`
}
function formatProfit(n: number): string {
const sign = n > 0 ? '+' : n < 0 ? '-' : ''
const abs = Math.abs(n)
if (abs >= 1e8) return `${sign}${(abs / 1e8).toFixed(2)}亿`
if (abs >= 1e4) return `${sign}${(abs / 1e4).toFixed(2)}万`
return `${sign}${abs.toFixed(0)}`
}
function formatPct(n: number): string {
return `${(n * 100).toFixed(1)}%`
}
function formatSignedPct(n: number): string {
const pct = n * 100
if (Math.abs(pct) < 0.005) return '0.00%'
const sign = pct > 0 ? '+' : ''
return `${sign}${pct.toFixed(2)}%`
}
function formatTracePrice(n: number): string {
if (!Number.isFinite(n) || n === 0) return '—'
return n >= 100 ? n.toFixed(0) : n.toFixed(2)
}
function formatFlowAbs(n: number): string {
const abs = Math.abs(n)
if (abs >= 1e8) return `${(abs / 1e8).toFixed(1)}亿`
if (abs >= 1e4) return `${(abs / 1e4).toFixed(0)}万`
return abs.toFixed(0)
}
function formatProfitAbs(n: number): string {
const abs = Math.abs(n)
if (abs >= 1e8) return `${(abs / 1e8).toFixed(2)}亿元`
if (abs >= 1e4) return `${(abs / 1e4).toFixed(2)}万元`
return `${abs.toFixed(0)}元`
} }

View File

@ -6,6 +6,10 @@ export interface AiAnalysisPayload {
keywords: string keywords: string
/** 行情 + 新闻 + 机构持仓 + 主力追踪 + 主力异动等接口快照 */ /** 行情 + 新闻 + 机构持仓 + 主力追踪 + 主力异动等接口快照 */
snapshot: AnalysisSnapshot snapshot: AnalysisSnapshot
/** 大单手数阈值,与 UI 大单分析一致 */
largeOrderLots: number
/** 品种代码(如 FG),主力数据优先标注当前品种 */
variety: string
} }
/** DeepSeek Chat Completions(OpenAI 兼容)请求体 */ /** DeepSeek Chat Completions(OpenAI 兼容)请求体 */

View File

@ -2,6 +2,7 @@ import { onUnmounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { AiAdvice } from '../types' import type { AiAdvice } from '../types'
import { fetchDeepSeekAdvice } from '../api/deepseek/analyze' import { fetchDeepSeekAdvice } from '../api/deepseek/analyze'
import { buildAnalysisUserContent } from '../api/deepseek/prompt'
import type { AiAnalysisPayload } from '../api/deepseek/types' import type { AiAnalysisPayload } from '../api/deepseek/types'
import { getSharedData, putSharedData } from '../api/uuquant/sharedData' import { getSharedData, putSharedData } from '../api/uuquant/sharedData'
import { adviceSharedKey } from '../api/uuquant/keys' import { adviceSharedKey } from '../api/uuquant/keys'
@ -9,7 +10,11 @@ import type { AdvicePayload } from '../api/uuquant/models'
import { useQuotaStore, type KlinePeriod } from '../stores/quota' import { useQuotaStore, type KlinePeriod } from '../stores/quota'
import { useContractStore } from '../stores/contract' import { useContractStore } from '../stores/contract'
import { useSettingsStore } from '../stores/settings' import { useSettingsStore } from '../stores/settings'
import { isTradingDay, isWsTradingSession } from '../utils/tradingDate' import {
extractVariety,
isTradingDay,
isWsTradingSession,
} from '../utils/tradingDate'
import { useTitleBlink } from './useTitleBlink' import { useTitleBlink } from './useTitleBlink'
/** 全局额外关键字 + 当前合约关键字(合约段追加在后) */ /** 全局额外关键字 + 当前合约关键字(合约段追加在后) */
@ -138,26 +143,22 @@ export function useAiAdvice() {
/** /**
* 一键获取建议:收集行情 / 新闻 / 持仓后提交 DeepSeek,返回真实 AI 建议。 * 一键获取建议:收集行情 / 新闻 / 持仓后提交 DeepSeek,返回真实 AI 建议。
* @param opts.silent 定时任务时静默:无 Key / 周末 / 非开盘 / 进行中则跳过,成功不弹 toast * @param opts.silent 定时任务时静默:无 Key / 周末 / 非开盘 / 进行中则跳过,成功不弹 toast
* 手动点击会先组装并 console.log 分析关键字(不受周末/开盘限制),再决定是否调用 API。
*/ */
async function refresh(opts?: { silent?: boolean }) { async function refresh(opts?: { silent?: boolean }) {
const silent = Boolean(opts?.silent) const silent = Boolean(opts?.silent)
// 周末不分析(定时静默跳过;手动点击提示)
if (!isTradingDay()) {
if (!silent) ElMessage.warning('周末休市,暂不进行 AI 分析')
return
}
// 定时分析仅在开盘时段执行(与行情 WS 时段一致)
if (silent && !isWsTradingSession()) return
const apiKey = settings.apiKey.trim() // 定时:周末 / 非开盘 / 无 Key 直接跳过
if (!apiKey) { if (silent) {
if (!silent) ElMessage.warning('请先在设置中配置 DeepSeek API Key') if (!isTradingDay()) return
return if (!isWsTradingSession()) return
if (!settings.apiKey.trim()) return
} }
if (loading.value) return if (loading.value) return
const epoch = analysisEpoch const epoch = analysisEpoch
const contractCode = contractStore.current?.code || '' const contract = contractStore.current
const contractCode = contract?.code || ''
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
@ -167,13 +168,30 @@ export function useAiAdvice() {
if (epoch !== analysisEpoch) return if (epoch !== analysisEpoch) return
const payload: AiAnalysisPayload = { const payload: AiAnalysisPayload = {
keywords: mergeKeywords(settings.keywords, contractStore.current.keywords), keywords: mergeKeywords(settings.keywords, contract?.keywords || ''),
snapshot: quota.getAnalysisSnapshot(), snapshot: quota.getAnalysisSnapshot(),
largeOrderLots: contract?.largeOrderLots ?? 0,
variety:
(contract?.variety || extractVariety(contractCode)).toUpperCase(),
} }
// 勿 console.log 完整 snapshot:DevTools 会长期持有大对象导致内存暴涨 // 手动点击:始终打印关键字(周末/休市也可核对 prompt)
if (import.meta.env.DEV) { if (!silent) {
console.log('[AI] 分析', payload.snapshot.contract.code, payload.keywords.slice(0, 80)) const userContent = buildAnalysisUserContent(payload)
console.log('[AI] 角色关键字', payload.keywords)
console.log('[AI] 分析数据关键字', userContent)
}
// 周末:手动可看关键字,但不调 API
if (!isTradingDay()) {
if (!silent) ElMessage.warning('周末休市,暂不进行 AI 分析(已输出分析关键字到控制台)')
return
}
const apiKey = settings.apiKey.trim()
if (!apiKey) {
if (!silent) ElMessage.warning('请先在设置中配置 DeepSeek API Key')
return
} }
const advice = await fetchDeepSeekAdvice(apiKey, payload) const advice = await fetchDeepSeekAdvice(apiKey, payload)