对接到deepseek分析
This commit is contained in:
parent
0f04b1370a
commit
398dfdf487
102
src/api/deepseek/analyze.ts
Normal file
102
src/api/deepseek/analyze.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import type { AiAdvice, AdviceDirection } from '../../types'
|
||||
import { deepseekHttp } from '../http'
|
||||
import type { AiAnalysisPayload } from './types'
|
||||
import { buildAnalysisMessages } from './prompt'
|
||||
import type {
|
||||
DeepSeekAdviceJson,
|
||||
DeepSeekChatRequest,
|
||||
DeepSeekChatResponse,
|
||||
} from './types'
|
||||
|
||||
/** 当前推荐模型(deepseek-chat 将于 2026-07-24 弃用) */
|
||||
export const DEEPSEEK_MODEL = 'deepseek-v4-flash'
|
||||
|
||||
/**
|
||||
* 调用 DeepSeek,用关键词 + 格式化行情/新闻/持仓数据生成交易建议。
|
||||
*/
|
||||
export async function fetchDeepSeekAdvice(
|
||||
apiKey: string,
|
||||
payload: AiAnalysisPayload,
|
||||
): Promise<AiAdvice> {
|
||||
const body: DeepSeekChatRequest = {
|
||||
model: DEEPSEEK_MODEL,
|
||||
messages: buildAnalysisMessages(payload),
|
||||
temperature: 0.3,
|
||||
response_format: { type: 'json_object' },
|
||||
thinking: { type: 'disabled' },
|
||||
stream: false,
|
||||
}
|
||||
|
||||
const { data } = await deepseekHttp.post<DeepSeekChatResponse>(
|
||||
'/chat/completions',
|
||||
body,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
timeout: 90000,
|
||||
},
|
||||
)
|
||||
|
||||
const content = data.choices?.[0]?.message?.content
|
||||
if (!content?.trim()) {
|
||||
throw new Error('DeepSeek 返回空内容')
|
||||
}
|
||||
|
||||
return parseAdviceContent(content)
|
||||
}
|
||||
|
||||
function parseAdviceContent(content: string): AiAdvice {
|
||||
const jsonText = extractJsonObject(content)
|
||||
let raw: DeepSeekAdviceJson
|
||||
try {
|
||||
raw = JSON.parse(jsonText) as DeepSeekAdviceJson
|
||||
} catch {
|
||||
throw new Error(`DeepSeek 返回非 JSON:${content.slice(0, 200)}`)
|
||||
}
|
||||
|
||||
const direction = normalizeDirection(raw.direction)
|
||||
const confidence = clampConfidence(raw.confidence)
|
||||
const reasons = Array.isArray(raw.reasons)
|
||||
? raw.reasons.map(String).filter(Boolean).slice(0, 8)
|
||||
: []
|
||||
|
||||
return {
|
||||
action: String(raw.action || directionToAction(direction)).trim() || '观望',
|
||||
direction,
|
||||
confidence,
|
||||
summary: String(raw.summary || '').trim() || '暂无摘要',
|
||||
reasons: reasons.length ? reasons : ['模型未给出具体理由'],
|
||||
updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
}
|
||||
|
||||
function extractJsonObject(text: string): string {
|
||||
const trimmed = text.trim()
|
||||
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
||||
if (fence?.[1]) return fence[1].trim()
|
||||
|
||||
const start = trimmed.indexOf('{')
|
||||
const end = trimmed.lastIndexOf('}')
|
||||
if (start >= 0 && end > start) return trimmed.slice(start, end + 1)
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function normalizeDirection(value: unknown): AdviceDirection {
|
||||
const v = String(value || '').toLowerCase()
|
||||
if (v === 'long' || v === 'buy' || v === '多' || v === '买入') return 'long'
|
||||
if (v === 'short' || v === 'sell' || v === '空' || v === '卖出') return 'short'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
function clampConfidence(value: unknown): number {
|
||||
const n = Number(value)
|
||||
if (!Number.isFinite(n)) return 50
|
||||
return Math.max(0, Math.min(100, Math.round(n)))
|
||||
}
|
||||
|
||||
function directionToAction(direction: AdviceDirection): string {
|
||||
if (direction === 'long') return '买入'
|
||||
if (direction === 'short') return '卖出'
|
||||
return '观望'
|
||||
}
|
||||
61
src/api/deepseek/prompt.ts
Normal file
61
src/api/deepseek/prompt.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import type { AnalysisSnapshot } from '../../stores/quota'
|
||||
import type { AiAnalysisPayload } from './types'
|
||||
|
||||
const OUTPUT_SCHEMA = `{
|
||||
"action": "买入 | 卖出 | 观望(或更具体的操作建议,如「轻仓试多」)",
|
||||
"direction": "long | short | neutral",
|
||||
"confidence": 0-100 的整数,
|
||||
"summary": "一句话结论,面向交易员",
|
||||
"reasons": ["理由1", "理由2", "理由3", "理由4"]
|
||||
}`
|
||||
|
||||
/**
|
||||
* 将行情 / 新闻 / 持仓格式化规则数据拼成 DeepSeek messages。
|
||||
* keywords 作为 system 角色设定;snapshot 作为 user 侧事实数据。
|
||||
*/
|
||||
export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
|
||||
role: 'system' | 'user'
|
||||
content: string
|
||||
}> {
|
||||
const system = [
|
||||
payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。',
|
||||
'请仅基于用户提供的行情、盘口、新闻与机构持仓数据给出交易建议。',
|
||||
'不要编造未提供的数据;信息不足时偏向观望并说明原因。',
|
||||
'必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。',
|
||||
`JSON 格式:${OUTPUT_SCHEMA}`,
|
||||
].join('\n')
|
||||
|
||||
const user = [
|
||||
'以下为当前合约的格式化分析数据(JSON),请据此给出建议:',
|
||||
JSON.stringify(slimSnapshot(payload.snapshot), null, 2),
|
||||
].join('\n\n')
|
||||
|
||||
return [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user },
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩超长数组,保留分析关键字段,避免 prompt 过大。
|
||||
* 盘口、统计、持仓、新闻完整保留;分时/成交/K 线做尾部截断。
|
||||
*/
|
||||
function slimSnapshot(snapshot: AnalysisSnapshot): AnalysisSnapshot {
|
||||
const quote = snapshot.quote
|
||||
if (!quote) return snapshot
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
quote: {
|
||||
...quote,
|
||||
intraday: quote.intraday.slice(-120),
|
||||
trades: quote.trades.slice(-80),
|
||||
candles: {
|
||||
day: quote.candles.day.slice(-60),
|
||||
week: quote.candles.week.slice(-40),
|
||||
month: quote.candles.month.slice(-36),
|
||||
},
|
||||
},
|
||||
news: snapshot.news.slice(0, 30),
|
||||
}
|
||||
}
|
||||
52
src/api/deepseek/types.ts
Normal file
52
src/api/deepseek/types.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import type { AnalysisSnapshot } from '../../stores/quota'
|
||||
|
||||
/** 提交给 AI 的完整载荷(格式化后的规则数据 + 关键词) */
|
||||
export interface AiAnalysisPayload {
|
||||
/** 设置中的角色/分析关键词 */
|
||||
keywords: string
|
||||
/** 行情 + 新闻 + 机构持仓等接口快照 */
|
||||
snapshot: AnalysisSnapshot
|
||||
}
|
||||
|
||||
/** DeepSeek Chat Completions(OpenAI 兼容)请求体 */
|
||||
export interface DeepSeekChatRequest {
|
||||
model: string
|
||||
messages: Array<{
|
||||
role: 'system' | 'user' | 'assistant'
|
||||
content: string
|
||||
}>
|
||||
temperature?: number
|
||||
response_format?: { type: 'json_object' | 'text' }
|
||||
stream?: boolean
|
||||
/** V4 默认开启 thinking;分析建议关闭以加快 JSON 输出 */
|
||||
thinking?: { type: 'enabled' | 'disabled' }
|
||||
}
|
||||
|
||||
export interface DeepSeekChatChoice {
|
||||
index: number
|
||||
message: {
|
||||
role: string
|
||||
content: string | null
|
||||
}
|
||||
finish_reason: string | null
|
||||
}
|
||||
|
||||
export interface DeepSeekChatResponse {
|
||||
id: string
|
||||
model: string
|
||||
choices: DeepSeekChatChoice[]
|
||||
usage?: {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
}
|
||||
}
|
||||
|
||||
/** 要求模型严格返回的 JSON 结构(与 AiAdvice 对齐) */
|
||||
export interface DeepSeekAdviceJson {
|
||||
action: string
|
||||
direction: 'long' | 'short' | 'neutral'
|
||||
confidence: number
|
||||
summary: string
|
||||
reasons: string[]
|
||||
}
|
||||
@ -11,3 +11,9 @@ export const jqkaHttp = axios.create({
|
||||
baseURL: '/jqka',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
/** 开发环境走 Vite 代理 /deepseek → https://api.deepseek.com */
|
||||
export const deepseekHttp = axios.create({
|
||||
baseURL: '/deepseek',
|
||||
timeout: 90000,
|
||||
})
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="expanded" class="body">
|
||||
<div class="hint">基于行情、盘口、新闻与持仓的综合分析(当前为 Mock)</div>
|
||||
<div class="hint">基于行情、盘口、新闻与持仓,由 DeepSeek 综合分析</div>
|
||||
<ul>
|
||||
<li v-for="(r, i) in data.reasons" :key="i">{{ r }}</li>
|
||||
</ul>
|
||||
@ -25,9 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { AiAdvice } from '../../types'
|
||||
import { useSettingsStore } from '../../stores/settings'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@ -40,13 +38,9 @@ withDefaults(
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
}>()
|
||||
const settings = useSettingsStore()
|
||||
const expanded = ref(false)
|
||||
|
||||
function onRefresh() {
|
||||
if (!settings.apiKey) {
|
||||
ElMessage.warning('请先在设置中配置 DeepSeek API Key(本阶段仍返回 Mock 建议)')
|
||||
}
|
||||
emit('refresh')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { AiAdvice } from '../types'
|
||||
import { aiAdviceMock } from '../mocks/aiAdvice'
|
||||
import { useQuotaStore, type AnalysisSnapshot, type KlinePeriod } from '../stores/quota'
|
||||
import { fetchDeepSeekAdvice } from '../api/deepseek/analyze'
|
||||
import type { AiAnalysisPayload } from '../api/deepseek/types'
|
||||
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
|
||||
/** 提交给 AI 的完整载荷(格式化后的规则数据 + 关键词) */
|
||||
export interface AiAnalysisPayload {
|
||||
/** 设置中的角色/分析关键词 */
|
||||
keywords: string
|
||||
/** 行情 + 新闻 + 机构持仓等接口快照 */
|
||||
snapshot: AnalysisSnapshot
|
||||
}
|
||||
|
||||
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
|
||||
|
||||
export function useAiAdvice() {
|
||||
@ -35,14 +30,19 @@ export function useAiAdvice() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键获取建议:先收集行情 / 新闻 / 持仓(含 K 线),
|
||||
* 再拼装提交给 AI 的关键词与格式化快照;当前阶段仅 console.log,仍返回 Mock 建议。
|
||||
* 一键获取建议:收集行情 / 新闻 / 持仓后提交 DeepSeek,返回真实 AI 建议。
|
||||
*/
|
||||
async function refresh() {
|
||||
const apiKey = settings.apiKey.trim()
|
||||
if (!apiKey) {
|
||||
ElMessage.warning('请先在设置中配置 DeepSeek API Key')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await quota.fetchAll()
|
||||
await quota.fetchForAnalysis()
|
||||
await ensureKlines()
|
||||
|
||||
const payload: AiAnalysisPayload = {
|
||||
@ -50,18 +50,16 @@ export function useAiAdvice() {
|
||||
snapshot: quota.getAnalysisSnapshot(),
|
||||
}
|
||||
|
||||
// 本阶段:打印将提交给 AI 的关键词与全部接口规则数据
|
||||
console.log('[AI] 提交给 AI 的关键词', payload.keywords)
|
||||
console.log('[AI] 提交给 AI 的数据(格式化规则快照)', payload.snapshot)
|
||||
console.log('[AI] 完整载荷', payload)
|
||||
console.log('[AI] 提交给 DeepSeek 的关键词', payload.keywords)
|
||||
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
|
||||
|
||||
data.value = {
|
||||
...structuredClone(aiAdviceMock),
|
||||
updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
data.value = await fetchDeepSeekAdvice(apiKey, payload)
|
||||
ElMessage.success('AI 建议已更新')
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
const msg = formatError(e)
|
||||
console.error('[AI] 一键获取建议失败', e)
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -69,3 +67,15 @@ export function useAiAdvice() {
|
||||
|
||||
return { data, loading, error, refresh }
|
||||
}
|
||||
|
||||
function formatError(e: unknown): string {
|
||||
if (typeof e === 'object' && e && 'response' in e) {
|
||||
const resp = (e as { response?: { status?: number; data?: { error?: { message?: string } } } })
|
||||
.response
|
||||
const apiMsg = resp?.data?.error?.message
|
||||
if (apiMsg) return `DeepSeek:${apiMsg}`
|
||||
if (resp?.status) return `DeepSeek 请求失败(HTTP ${resp.status})`
|
||||
}
|
||||
if (e instanceof Error) return e.message
|
||||
return '获取 AI 建议失败'
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
import type { Candle, NewsItem, PositionsData, QuoteData } from '../types'
|
||||
import { quoteMock } from '../mocks/quote'
|
||||
import { newsMock } from '../mocks/news'
|
||||
@ -70,6 +70,15 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
|
||||
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
|
||||
const hasNews = computed(() => news.value.length > 0)
|
||||
/** 新闻 / 持仓是否已成功拉取过真实接口(有则 AI 刷新可跳过) */
|
||||
const newsLoaded = ref(false)
|
||||
const positionsLoaded = ref(false)
|
||||
const hasPositions = computed(
|
||||
() =>
|
||||
positions.value.long.length > 0 ||
|
||||
positions.value.short.length > 0 ||
|
||||
positions.value.volume.length > 0,
|
||||
)
|
||||
|
||||
async function fetchQuote() {
|
||||
quoteLoading.value = true
|
||||
@ -120,6 +129,7 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
try {
|
||||
const list = await getFuturesNews({ code: contractConfig.code })
|
||||
news.value = mapBaiduNewsToItems(list)
|
||||
newsLoaded.value = news.value.length > 0
|
||||
} catch (e) {
|
||||
newsError.value = e
|
||||
console.error('[quota] 拉取新闻失败', e)
|
||||
@ -168,6 +178,7 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
}
|
||||
|
||||
positions.value = mapped
|
||||
positionsLoaded.value = true
|
||||
} catch (e) {
|
||||
positionsError.value = e
|
||||
console.error('[quota] 拉取持仓失败', e)
|
||||
@ -181,6 +192,25 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
await Promise.all([fetchQuote(), fetchNews(), fetchPositions()])
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 分析前取数:行情每次重新拉取;
|
||||
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
|
||||
*/
|
||||
async function fetchForAnalysis() {
|
||||
const tasks: Promise<void>[] = [fetchQuote()]
|
||||
if (!newsLoaded.value) tasks.push(fetchNews())
|
||||
if (!positionsLoaded.value) tasks.push(fetchPositions())
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝可序列化数据。Pinia/ref 内是 Proxy,structuredClone 直接克隆会 DataCloneError,
|
||||
* 需先 toRaw 再 JSON 往返,得到脱离响应式的纯对象。
|
||||
*/
|
||||
function clonePlain<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(toRaw(value))) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前全部接口数据快照,供下一步 AI 分析拼接 Prompt。
|
||||
* 返回深拷贝,避免分析过程中被行情刷新污染。
|
||||
@ -192,9 +222,9 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
name: quote.value.name || contractConfig.name,
|
||||
exchange: quote.value.exchange || contractConfig.exchange,
|
||||
},
|
||||
quote: quote.value ? structuredClone(quote.value) : null,
|
||||
news: structuredClone(news.value),
|
||||
positions: positions.value ? structuredClone(positions.value) : null,
|
||||
quote: quote.value ? clonePlain(quote.value) : null,
|
||||
news: clonePlain(news.value),
|
||||
positions: positions.value ? clonePlain(positions.value) : null,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
@ -215,12 +245,16 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
// computed
|
||||
hasQuote,
|
||||
hasNews,
|
||||
hasPositions,
|
||||
newsLoaded,
|
||||
positionsLoaded,
|
||||
// actions
|
||||
fetchQuote,
|
||||
loadKline,
|
||||
fetchNews,
|
||||
fetchPositions,
|
||||
fetchAll,
|
||||
fetchForAnalysis,
|
||||
getAnalysisSnapshot,
|
||||
}
|
||||
})
|
||||
|
||||
@ -26,6 +26,11 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/jqka/, ''),
|
||||
},
|
||||
'/deepseek': {
|
||||
target: 'https://api.deepseek.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/deepseek/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user