优化AI解读提示词
This commit is contained in:
parent
cf9b9c5d74
commit
3e2e2ffc90
@ -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)}元`
|
||||||
|
}
|
||||||
|
|||||||
@ -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 兼容)请求体 */
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user