新增AI评分

This commit is contained in:
dongzp 2026-07-27 13:59:37 +08:00
parent d88fc616f3
commit 4738c1c57a
10 changed files with 546 additions and 12 deletions

View File

@ -73,6 +73,7 @@
v-if="advice" v-if="advice"
:data="advice" :data="advice"
:loading="adviceLoading" :loading="adviceLoading"
:ai-score="aiVarietyScore"
@refresh="refreshAdvice" @refresh="refreshAdvice"
/> />
</template> </template>
@ -111,6 +112,7 @@ const {
positionAnomaly, positionAnomaly,
positionAnomalyLoading, positionAnomalyLoading,
positionAnomalyError, positionAnomalyError,
aiVarietyScore,
} = storeToRefs(useQuotaStore()) } = storeToRefs(useQuotaStore())
const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews() const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews()
const { data: positions } = usePositions() const { data: positions } = usePositions()

127
src/api/cache/localStorageCache.ts vendored Normal file
View File

@ -0,0 +1,127 @@
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import { getPositionQueryDate } from '../../utils/tradingDate'
const CACHE_PREFIX = 'api-cache:v1:'
type CacheEntry = {
data: unknown
savedAt: number
/** 与持仓查询日对齐,换日自动失效 */
dayKey: string
}
function stableSerialize(value: unknown): string {
if (value == null) return ''
if (typeof value !== 'object') return String(value)
if (Array.isArray(value)) {
return `[${value.map((v) => stableSerialize(v)).join(',')}]`
}
const obj = value as Record<string, unknown>
const keys = Object.keys(obj).sort()
return `{${keys.map((k) => `${k}:${stableSerialize(obj[k])}`).join(',')}}`
}
/**
* axios 在发 POST 前会把 data 转成 JSON 字符串;
* 请求拦截器看到的是 object,响应拦截器看到的常是 string,
* 必须归一化后再算缓存键,否则 position_profit_rank 等 POST 永远 miss。
*/
function normalizeBody(data: unknown): unknown {
if (typeof data !== 'string') return data
const trimmed = data.trim()
if (!trimmed) return ''
try {
return JSON.parse(trimmed) as unknown
} catch {
return data
}
}
/**
* 缓存键:baseURL + path + 查询参数 + 请求体(POST)。
* 例:/jqka|/futgwapi/api/.../variety_list|params:{}|body:
*/
export function buildApiCacheKey(config: InternalAxiosRequestConfig): string {
const base = (config.baseURL || '').replace(/\/$/, '')
const url = (config.url || '').split('?')[0]
const method = (config.method || 'get').toLowerCase()
const params = stableSerialize(config.params)
const body =
method === 'get' || method === 'head'
? ''
: stableSerialize(normalizeBody(config.data))
return `${CACHE_PREFIX}${method}|${base}|${url}|params:${params}|body:${body}`
}
function readCache(key: string): unknown | null {
try {
const raw = localStorage.getItem(key)
if (!raw) return null
const entry = JSON.parse(raw) as CacheEntry
if (!entry || entry.dayKey !== getPositionQueryDate()) {
localStorage.removeItem(key)
return null
}
return entry.data ?? null
} catch {
return null
}
}
function writeCache(key: string, data: unknown) {
try {
const entry: CacheEntry = {
data,
savedAt: Date.now(),
dayKey: getPositionQueryDate(),
}
localStorage.setItem(key, JSON.stringify(entry))
} catch (e) {
// quota exceeded 等:忽略,不影响主流程
console.warn('[api-cache] 写入 localStorage 失败', e)
}
}
/**
* 为 axios 实例挂载 localStorage 缓存:
* - 请求前命中则直接返回缓存(自定义 adapter)
* - 成功响应后按 path + 参数写入
* - 按持仓查询日换日失效
*/
export function attachLocalStorageCache(http: AxiosInstance) {
http.interceptors.request.use((config) => {
// 允许单次请求跳过缓存:config.headers['X-Skip-Cache'] = '1'
const skip =
config.headers?.['X-Skip-Cache'] === '1' ||
config.headers?.['x-skip-cache'] === '1'
if (skip) return config
const key = buildApiCacheKey(config)
const cached = readCache(key)
if (cached == null) return config
config.adapter = async () => ({
data: cached,
status: 200,
statusText: 'OK (localStorage cache)',
headers: { 'x-cache': 'HIT' },
config,
request: {},
})
// 标记已走缓存,避免响应拦截器重复写入
;(config as InternalAxiosRequestConfig & { __fromCache?: boolean }).__fromCache =
true
return config
})
http.interceptors.response.use((response) => {
const cfg = response.config as InternalAxiosRequestConfig & {
__fromCache?: boolean
}
if (cfg.__fromCache) return response
const key = buildApiCacheKey(cfg)
writeCache(key, response.data)
return response
})
}

View File

@ -42,8 +42,9 @@ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
}> { }> {
const system = [ const system = [
payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。', payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。',
'请仅基于用户提供的行情、盘口、大单分析、新闻、机构持仓、主力分析(盈利席位榜)、主力追踪(变盘预警、主力趋势机会)与主力异动数据给出交易建议。', '请仅基于用户提供的行情、盘口、大单分析、新闻、机构持仓、主力分析(盈利席位榜)、主力追踪(变盘预警、主力趋势机会)、主力异动与同花顺 AI 品种评分数据给出交易建议。',
'主力分析为当前品种盈利席位榜;主力追踪与主力异动为全市场数据,请优先关注标注为「当前品种」的条目,并结合整体主力动向综合判断。', '主力分析为当前品种盈利席位榜;主力追踪与主力异动为全市场数据,请优先关注标注为「当前品种」的条目,并结合整体主力动向综合判断。',
'同花顺 AI 评分为外部量化/基本面因子评分,正分偏多、负分偏空;若标注「最佳推荐」则表示该品种在推荐列表中,权重可适当提高,但仍需结合盘面综合判断。',
'文案中的单位与页面一致(手、万手、万、亿、百分比);不要编造未提供的数据;信息不足时偏向观望并说明原因。', '文案中的单位与页面一致(手、万手、万、亿、百分比);不要编造未提供的数据;信息不足时偏向观望并说明原因。',
'必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。', '必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。',
`JSON 格式:${OUTPUT_SCHEMA}`, `JSON 格式:${OUTPUT_SCHEMA}`,
@ -60,6 +61,7 @@ export function buildAnalysisUserContent(payload: AiAnalysisPayload): string {
const { snapshot, largeOrderLots, variety } = payload const { snapshot, largeOrderLots, variety } = payload
const sections = [ const sections = [
formatContract(snapshot), formatContract(snapshot),
formatAiVarietyScore(snapshot),
formatQuoteStats(snapshot.quote), formatQuoteStats(snapshot.quote),
formatOrderBook(snapshot.quote), formatOrderBook(snapshot.quote),
formatLargeOrder(snapshot.quote?.trades ?? [], largeOrderLots), formatLargeOrder(snapshot.quote?.trades ?? [], largeOrderLots),
@ -98,6 +100,32 @@ function formatContract(snapshot: AnalysisSnapshot): string {
return lines.join('\n') return lines.join('\n')
} }
function formatAiVarietyScore(snapshot: AnalysisSnapshot): string {
const score = snapshot.aiVarietyScore
if (!score) return '【同花顺 AI 评分】\n暂无数据'
const lines = [
'【同花顺 AI 评分】',
`品种:${score.varietyName}(${score.variety})`,
`主力合约:${score.mainContract}`,
`综合评分:${fmtSigned(score.score)}${score.isBest ? '【最佳推荐】' : ''}`,
`涨跌幅:${fmtSigned(score.percent)}%`,
'影响因素:',
]
if (!score.factors.length) {
lines.push('(无)')
} else {
for (const f of score.factors) {
lines.push(
`- ${f.factor}|${f.judge}|评分 ${fmtSigned(f.score)}`,
)
}
}
return lines.join('\n')
}
function formatQuoteStats(quote: QuoteData | null): string { function formatQuoteStats(quote: QuoteData | null): string {
if (!quote) return '【行情统计】\n暂无数据' if (!quote) return '【行情统计】\n暂无数据'
return [ return [

View File

@ -4,7 +4,7 @@ import type { AnalysisSnapshot } from '../../stores/quota'
export interface AiAnalysisPayload { export interface AiAnalysisPayload {
/** 设置中的角色/分析关键词 */ /** 设置中的角色/分析关键词 */
keywords: string keywords: string
/** 行情 + 新闻 + 机构持仓(含主力分析)+ 主力追踪 + 主力异动等接口快照 */ /** 行情 + 新闻 + 机构持仓(含主力分析)+ 主力追踪 + 主力异动 + AI 评分等接口快照 */
snapshot: AnalysisSnapshot snapshot: AnalysisSnapshot
/** 大单手数阈值,与 UI 大单分析一致 */ /** 大单手数阈值,与 UI 大单分析一致 */
largeOrderLots: number largeOrderLots: number

View File

@ -1,4 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { attachLocalStorageCache } from './cache/localStorageCache'
/** 开发环境走 Vite 代理 /baidu → https://finance.pae.baidu.com */ /** 开发环境走 Vite 代理 /baidu → https://finance.pae.baidu.com */
export const baiduHttp = axios.create({ export const baiduHttp = axios.create({
@ -18,6 +19,10 @@ export const jqkaDqHttp = axios.create({
timeout: 15000, timeout: 15000,
}) })
/** /jqka、/dq 响应按 path + 参数缓存到 localStorage */
attachLocalStorageCache(jqkaHttp)
attachLocalStorageCache(jqkaDqHttp)
/** 开发环境走 Vite 代理 /deepseek → https://api.deepseek.com */ /** 开发环境走 Vite 代理 /deepseek → https://api.deepseek.com */
export const deepseekHttp = axios.create({ export const deepseekHttp = axios.create({
baseURL: '/deepseek', baseURL: '/deepseek',

84
src/api/jqka/aiExplain.ts Normal file
View File

@ -0,0 +1,84 @@
import { jqkaHttp } from '../http'
import { extractVariety } from '../../utils/tradingDate'
import type {
AiExplainVariety,
AiExplainVarietyListResponse,
AiVarietyScore,
} from './aiExplainTypes'
function mapVariety(raw: AiExplainVariety, isBest: boolean): AiVarietyScore {
return {
market: raw.market,
score: Number(raw.score) || 0,
percent: Number(raw.percent) || 0,
varietyName: raw.variety_name,
mainContract: raw.main_contract,
variety: extractVariety(raw.main_contract).toUpperCase(),
factors: (raw.factor_list || []).map((f) => ({
factor: f.factor,
judge: f.judge,
score: Number(f.score) || 0,
})),
isBest,
}
}
/**
* 同花顺 — 全品种 AI 评分
* GET /futgwapi/api/f10/ai_explain/v1/variety_list
*/
export async function getAiVarietyList(): Promise<AiExplainVariety[]> {
const { data } = await jqkaHttp.get<AiExplainVarietyListResponse>(
'/futgwapi/api/f10/ai_explain/v1/variety_list',
)
if (data.code !== 0) {
throw new Error(`AI 评分接口失败: code=${data.code}, msg=${data.msg}`)
}
return data.data ?? []
}
/**
* 同花顺 — 最佳/推荐品种 AI 评分
* GET /futgwapi/api/f10/ai_explain/v1/recommend_variety_list
*/
export async function getAiRecommendVarietyList(): Promise<AiExplainVariety[]> {
const { data } = await jqkaHttp.get<AiExplainVarietyListResponse>(
'/futgwapi/api/f10/ai_explain/v1/recommend_variety_list',
)
if (data.code !== 0) {
throw new Error(`AI 推荐评分接口失败: code=${data.code}, msg=${data.msg}`)
}
return data.data ?? []
}
/** 按品种代码匹配(忽略大小写) */
export function findVarietyScore(
list: AiVarietyScore[],
variety: string,
): AiVarietyScore | null {
const key = variety.trim().toUpperCase()
if (!key) return null
return list.find((item) => item.variety === key) ?? null
}
/**
* 拉取全品种评分 + 推荐列表,合并 isBest 标记。
*/
export async function fetchAiVarietyScores(): Promise<AiVarietyScore[]> {
const [all, recommend] = await Promise.all([
getAiVarietyList(),
getAiRecommendVarietyList(),
])
const bestKeys = new Set(
recommend.map((r) => extractVariety(r.main_contract).toUpperCase()),
)
return all.map((raw) =>
mapVariety(raw, bestKeys.has(extractVariety(raw.main_contract).toUpperCase())),
)
}

View File

@ -0,0 +1,44 @@
/** 同花顺 AI 解读 — 单条影响因素 */
export interface AiExplainFactor {
factor: string
/** 利多 | 利空 */
judge: string
score: string
}
/** 同花顺 AI 解读 — 品种评分 */
export interface AiExplainVariety {
market: string
/** 综合评分(字符串数字,可正可负) */
score: string
/** 涨跌幅等百分比字段 */
percent: string
variety_name: string
/** 主力合约,如 UR2609、y2609 */
main_contract: string
factor_list: AiExplainFactor[]
}
export interface AiExplainVarietyListResponse {
code: number
msg: string
data: AiExplainVariety[]
}
/** 映射到应用内使用的评分视图 */
export interface AiVarietyScore {
market: string
score: number
percent: number
varietyName: string
mainContract: string
/** 品种代码(从 main_contract 字母前缀推导,大写) */
variety: string
factors: Array<{
factor: string
judge: string
score: number
}>
/** 是否出现在 recommend_variety_list(最佳推荐) */
isBest: boolean
}

View File

@ -4,6 +4,15 @@
<div class="summary"> <div class="summary">
<span class="badge" :class="data.direction">AI</span> <span class="badge" :class="data.direction">AI</span>
<strong>{{ data.action }}</strong> <strong>{{ data.action }}</strong>
<span
v-if="aiScore"
class="score"
:class="scoreClass"
title="同花顺 AI 品种评分"
>
评分 {{ formatSigned(aiScore.score) }}
<span v-if="aiScore.isBest" class="best-tag">最佳</span>
</span>
<span class="text">{{ data.summary }}</span> <span class="text">{{ data.summary }}</span>
<span class="meta"> <span class="meta">
置信度 {{ data.confidence }}% 置信度 {{ data.confidence }}%
@ -27,7 +36,32 @@
</div> </div>
</div> </div>
<div v-show="expanded" class="body"> <div v-show="expanded" class="body">
<div class="hint">基于行情、盘口、新闻与持仓,由 DeepSeek 综合分析</div> <div class="hint">基于行情、盘口、新闻、持仓与同花顺 AI 评分,由 DeepSeek 综合分析</div>
<div v-if="aiScore" class="score-detail">
<div class="score-head">
<span
>{{ aiScore.varietyName }} AI 评分
<strong :class="scoreClass">{{
formatSigned(aiScore.score)
}}</strong></span
>
<el-tag v-if="aiScore.isBest" size="small" type="danger" effect="dark"
>最佳</el-tag
>
<span class="score-pct"
>涨跌幅 {{ formatSigned(aiScore.percent) }}%</span
>
</div>
<ul v-if="aiScore.factors.length" class="factors">
<li v-for="(f, i) in aiScore.factors" :key="i">
<span class="judge" :class="judgeClass(f.judge)">{{
f.judge
}}</span>
{{ f.factor }}
<span class="factor-score">{{ formatSigned(f.score) }}</span>
</li>
</ul>
</div>
<ul> <ul>
<li v-for="(r, i) in data.reasons" :key="i">{{ r }}</li> <li v-for="(r, i) in data.reasons" :key="i">{{ r }}</li>
</ul> </ul>
@ -36,15 +70,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { computed, ref } from "vue";
import type { AiAdvice } from "../../types"; import type { AiAdvice, AiVarietyScoreView } from "../../types";
withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
data: AiAdvice; data: AiAdvice;
loading?: boolean; loading?: boolean;
aiScore?: AiVarietyScoreView | null;
}>(), }>(),
{ loading: false }, { loading: false, aiScore: null },
); );
const emit = defineEmits<{ const emit = defineEmits<{
@ -52,6 +87,24 @@ const emit = defineEmits<{
}>(); }>();
const expanded = ref(false); const expanded = ref(false);
const scoreClass = computed(() => {
const s = props.aiScore?.score ?? 0;
if (s > 0) return "bull";
if (s < 0) return "bear";
return "flat";
});
function formatSigned(n: number): string {
if (!Number.isFinite(n)) return "—";
return `${n > 0 ? "+" : ""}${n}`;
}
function judgeClass(judge: string): string {
if (judge.includes("多")) return "bull";
if (judge.includes("空")) return "bear";
return "flat";
}
function onRefresh() { function onRefresh() {
emit("refresh"); emit("refresh");
} }
@ -100,6 +153,45 @@ function onRefresh() {
flex-shrink: 0; flex-shrink: 0;
} }
.score {
display: inline-flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
font-weight: 600;
font-variant-numeric: tabular-nums;
padding: 2px 8px;
border-radius: 4px;
background: var(--bg-elevated, #f5f7fa);
font-size: 12px;
}
.score.bull {
color: #cf1322;
background: #fff1f0;
}
.score.bear {
color: #389e0d;
background: #f6ffed;
}
.score.flat {
color: var(--text-secondary);
}
.best-tag {
display: inline-block;
margin-left: 2px;
padding: 0 4px;
border-radius: 3px;
background: #cf1322;
color: #fff;
font-size: 10px;
font-weight: 700;
line-height: 16px;
}
.text { .text {
color: var(--text-primary); color: var(--text-primary);
overflow: hidden; overflow: hidden;
@ -132,6 +224,64 @@ function onRefresh() {
margin: 10px 0 6px; margin: 10px 0 6px;
} }
.score-detail {
margin: 8px 0 12px;
padding: 10px 12px;
border-radius: 6px;
background: var(--bg-elevated, #f8f9fb);
border: 1px solid var(--border);
}
.score-head {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
margin-bottom: 6px;
}
.score-head .bull {
color: #cf1322;
}
.score-head .bear {
color: #389e0d;
}
.score-pct {
color: var(--text-secondary);
font-size: 12px;
margin-left: auto;
}
.factors {
margin: 0;
padding-left: 18px;
font-size: 12px;
line-height: 1.7;
color: #3d4450;
}
.judge {
display: inline-block;
min-width: 2em;
margin-right: 4px;
font-weight: 600;
}
.judge.bull {
color: #cf1322;
}
.judge.bear {
color: #389e0d;
}
.factor-score {
color: var(--text-secondary);
margin-left: 6px;
}
ul { ul {
margin: 0; margin: 0;
padding-left: 18px; padding-left: 18px;

View File

@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref, toRaw } from 'vue' import { computed, ref, toRaw } from 'vue'
import type { import type {
AiVarietyScoreView,
Candle, Candle,
MainForceTraceData, MainForceTraceData,
NewsItem, NewsItem,
@ -8,6 +9,11 @@ import type {
PositionsData, PositionsData,
QuoteData, QuoteData,
} from '../types' } from '../types'
import {
fetchAiVarietyScores,
findVarietyScore,
} from '../api/jqka/aiExplain'
import type { AiVarietyScore } from '../api/jqka/aiExplainTypes'
import { useContractStore } from './contract' import { useContractStore } from './contract'
import { getStockKline, getStockQuotation } from '../api/baidu/quotation' import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
import { getFuturesNews } from '../api/baidu/news' import { getFuturesNews } from '../api/baidu/news'
@ -65,6 +71,8 @@ export interface AnalysisSnapshot {
mainForceTrace: MainForceTraceData | null mainForceTrace: MainForceTraceData | null
/** 主力异动六类排行(全市场,各合约共用) */ /** 主力异动六类排行(全市场,各合约共用) */
positionAnomaly: PositionAnomalyData | null positionAnomaly: PositionAnomalyData | null
/** 当前合约对应的同花顺 AI 评分(含影响因素) */
aiVarietyScore: AiVarietyScoreView | null
fetchedAt: string fetchedAt: string
} }
@ -278,6 +286,13 @@ export const useQuotaStore = defineStore('quota', () => {
const positionAnomalyLoading = ref(false) const positionAnomalyLoading = ref(false)
const positionAnomalyError = ref<unknown>(null) const positionAnomalyError = ref<unknown>(null)
// ─── 同花顺 AI 品种评分(全品种列表,按当前合约匹配)─────
const aiVarietyScores = ref<AiVarietyScore[]>([])
const aiVarietyScore = ref<AiVarietyScoreView | null>(null)
const aiVarietyScoreLoading = ref(false)
const aiVarietyScoreError = ref<unknown>(null)
const aiVarietyScoresLoaded = ref(false)
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */ /** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0) const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
const hasNews = computed(() => news.value.length > 0) const hasNews = computed(() => news.value.length > 0)
@ -306,6 +321,52 @@ export const useQuotaStore = defineStore('quota', () => {
positionAnomaly.value.shortToLong.length > 0 || positionAnomaly.value.shortToLong.length > 0 ||
positionAnomaly.value.longToShort.length > 0, positionAnomaly.value.longToShort.length > 0,
) )
const hasAiVarietyScore = computed(() => aiVarietyScore.value != null)
function resolveCurrentAiScore(list: AiVarietyScore[]): AiVarietyScoreView | null {
const c = activeContract()
const variety = (c.variety || extractVariety(c.code)).toUpperCase()
const hit = findVarietyScore(list, variety)
if (!hit) return null
return {
score: hit.score,
percent: hit.percent,
varietyName: hit.varietyName,
mainContract: hit.mainContract,
variety: hit.variety,
factors: hit.factors,
isBest: hit.isBest,
}
}
/** 刷新当前合约对应的 AI 评分(全品种列表已缓存时只做匹配) */
function bindCurrentAiScore() {
aiVarietyScore.value = resolveCurrentAiScore(aiVarietyScores.value)
}
/**
* 拉取全品种 AI 评分 + 推荐列表,并绑定当前合约。
* 列表本身走 axios localStorage 缓存,切换合约时复用。
*/
async function fetchAiVarietyScore(force = false) {
if (aiVarietyScoresLoaded.value && !force) {
bindCurrentAiScore()
return
}
aiVarietyScoreLoading.value = true
aiVarietyScoreError.value = null
try {
const list = await fetchAiVarietyScores()
aiVarietyScores.value = list
aiVarietyScoresLoaded.value = true
bindCurrentAiScore()
} catch (e) {
aiVarietyScoreError.value = e
console.error('[quota] 拉取 AI 品种评分失败', e)
} finally {
aiVarietyScoreLoading.value = false
}
}
async function fetchQuote() { async function fetchQuote() {
const c = activeContract() const c = activeContract()
@ -467,13 +528,19 @@ export const useQuotaStore = defineStore('quota', () => {
} }
} }
/** 并行刷新行情 + 新闻 + 持仓(K 线仍按需) */ /** 并行刷新行情 + 新闻 + 持仓 + AI 评分(K 线仍按需) */
async function fetchAll() { async function fetchAll() {
await Promise.all([fetchQuote(), fetchNews(), fetchPositions()]) await Promise.all([
fetchQuote(),
fetchNews(),
fetchPositions(),
fetchAiVarietyScore(),
])
} }
/** /**
* 切换合约后:断开旧 WS、清空缓存、重新拉取并按需重连。 * 切换合约后:断开旧 WS、清空合约级缓存、重新拉取并按需重连。
* AI 全品种评分列表可复用,仅重新匹配当前合约。
*/ */
async function reloadForContract() { async function reloadForContract() {
quoteWs?.disconnect() quoteWs?.disconnect()
@ -485,6 +552,7 @@ export const useQuotaStore = defineStore('quota', () => {
positions.value = emptyPositions() positions.value = emptyPositions()
mainForceTrace.value = emptyMainForceTrace() mainForceTrace.value = emptyMainForceTrace()
positionAnomaly.value = emptyPositionAnomaly() positionAnomaly.value = emptyPositionAnomaly()
aiVarietyScore.value = null
loadedKlines.value = { day: false, week: false, month: false } loadedKlines.value = { day: false, week: false, month: false }
newsLoaded.value = false newsLoaded.value = false
positionsLoaded.value = false positionsLoaded.value = false
@ -495,6 +563,7 @@ export const useQuotaStore = defineStore('quota', () => {
positionsError.value = null positionsError.value = null
mainForceTraceError.value = null mainForceTraceError.value = null
positionAnomalyError.value = null positionAnomalyError.value = null
aiVarietyScoreError.value = null
await fetchAll() await fetchAll()
} }
@ -502,10 +571,10 @@ export const useQuotaStore = defineStore('quota', () => {
/** /**
* AI 分析前取数:行情每次重新拉取; * AI 分析前取数:行情每次重新拉取;
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。 * 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
* (主力追踪 / 主力异动随持仓一并拉取) * (主力追踪 / 主力异动随持仓一并拉取;AI 评分一并带上)
*/ */
async function fetchForAnalysis() { async function fetchForAnalysis() {
const tasks: Promise<void>[] = [fetchQuote()] const tasks: Promise<void>[] = [fetchQuote(), fetchAiVarietyScore()]
if (!newsLoaded.value) tasks.push(fetchNews()) if (!newsLoaded.value) tasks.push(fetchNews())
if ( if (
!positionsLoaded.value || !positionsLoaded.value ||
@ -546,6 +615,9 @@ export const useQuotaStore = defineStore('quota', () => {
positionAnomaly: hasPositionAnomaly.value positionAnomaly: hasPositionAnomaly.value
? clonePlain(positionAnomaly.value) ? clonePlain(positionAnomaly.value)
: null, : null,
aiVarietyScore: aiVarietyScore.value
? clonePlain(aiVarietyScore.value)
: null,
fetchedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(),
} }
} }
@ -569,6 +641,9 @@ export const useQuotaStore = defineStore('quota', () => {
positionAnomaly, positionAnomaly,
positionAnomalyLoading, positionAnomalyLoading,
positionAnomalyError, positionAnomalyError,
aiVarietyScore,
aiVarietyScoreLoading,
aiVarietyScoreError,
wsStatus, wsStatus,
wsInSession, wsInSession,
// computed // computed
@ -577,15 +652,18 @@ export const useQuotaStore = defineStore('quota', () => {
hasPositions, hasPositions,
hasMainForceTrace, hasMainForceTrace,
hasPositionAnomaly, hasPositionAnomaly,
hasAiVarietyScore,
newsLoaded, newsLoaded,
positionsLoaded, positionsLoaded,
mainForceTraceLoaded, mainForceTraceLoaded,
positionAnomalyLoaded, positionAnomalyLoaded,
aiVarietyScoresLoaded,
// actions // actions
fetchQuote, fetchQuote,
loadKline, loadKline,
fetchNews, fetchNews,
fetchPositions, fetchPositions,
fetchAiVarietyScore,
fetchAll, fetchAll,
fetchForAnalysis, fetchForAnalysis,
reloadForContract, reloadForContract,

View File

@ -279,6 +279,22 @@ export interface PositionAnomalySection {
export type AdviceDirection = 'long' | 'short' | 'neutral' export type AdviceDirection = 'long' | 'short' | 'neutral'
/** 同花顺 AI 品种评分(展示 / 分析用) */
export interface AiVarietyScoreView {
score: number
percent: number
varietyName: string
mainContract: string
variety: string
factors: Array<{
factor: string
judge: string
score: number
}>
/** 是否在推荐列表(最佳) */
isBest: boolean
}
export interface AiAdvice { export interface AiAdvice {
action: string action: string
direction: AdviceDirection direction: AdviceDirection