From 8d1162a2afbd700a3f253c310c174347bec6a2df Mon Sep 17 00:00:00 2001 From: dongzp <975303544@qq.com> Date: Wed, 22 Jul 2026 11:19:10 +0800 Subject: [PATCH] =?UTF-8?q?AI=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/baidu/mapQuote.ts | 21 ++++++++++++++++ src/api/baidu/mapWsQuote.ts | 18 ++++++++++++++ src/components/ai/AiAdviceDrawer.vue | 5 +++- src/components/quote/QuoteStats.vue | 8 ++++++ src/composables/useAiAdvice.ts | 37 ++++++++++++++++++++++------ src/stores/quota.ts | 1 + src/types/index.ts | 2 ++ 7 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/api/baidu/mapQuote.ts b/src/api/baidu/mapQuote.ts index ad3ee1b..3a22e0b 100644 --- a/src/api/baidu/mapQuote.ts +++ b/src/api/baidu/mapQuote.ts @@ -22,6 +22,22 @@ function parseAmountYi(value: string | undefined, rawAmount?: string): number { return toNum(value) } +/** 将「-4.20万」「+8.29万手」或原始手数转为手数 */ +function parseDeltaHands(text: string): number { + const cleaned = text.replace(/手/g, '') + if (cleaned.includes('万')) return Math.round(toNum(cleaned.replace('万', '')) * 10000) + return toNum(cleaned) +} + +function parseAmountDelta(value: string | undefined, raw?: string): number { + if (raw != null && raw !== '' && raw !== '--' && !String(raw).includes('万')) { + return toNum(raw) + } + if (value && value !== '--') return parseDeltaHands(value) + if (raw && String(raw).includes('万')) return parseDeltaHands(String(raw)) + return toNum(raw) +} + function parseIntraday(result: BaiduQuotationResult): IntradayPoint[] { const days = result.newMarketData?.marketData ?? [] const points: IntradayPoint[] = [] @@ -99,6 +115,10 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat result.pankouinfos?.list?.find((i) => i.ename === 'amount')?.value, op?.amount, ) + const amountDelta = parseAmountDelta( + result.pankouinfos?.list?.find((i) => i.ename === 'amountDelta')?.value, + op?.amountDelta, + ) return { name: basic?.name || contractConfig.name, @@ -118,6 +138,7 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat volume: toNum(op?.volume), amount: amountYi, openInterest: toNum(op?.holdingAmount), + amountDelta, amplitude: toNum(op?.amplitudeRatio), outerVol: outside, innerVol: inside, diff --git a/src/api/baidu/mapWsQuote.ts b/src/api/baidu/mapWsQuote.ts index e64fea5..9e8ba46 100644 --- a/src/api/baidu/mapWsQuote.ts +++ b/src/api/baidu/mapWsQuote.ts @@ -28,6 +28,15 @@ function parseAmountYi(value: string | undefined, originValue?: number): number return toNum(value) } +/** Parse 日增「+8.29万手」; prefer originValue (hands). */ +function parseAmountDelta(value: string | undefined, originValue?: number): number { + if (originValue != null && Number.isFinite(originValue)) return originValue + if (!value || value === '--') return 0 + const cleaned = value.replace(/手/g, '') + if (cleaned.includes('万')) return Math.round(toNum(cleaned.replace('万', '')) * 10000) + return toNum(cleaned) +} + /** Extract HH:mm from 「07-15 10:59」 or 「10:59」. */ function hhmmFromPointTime(time: string): string { if (time.includes(' ')) return time.split(' ')[1]!.slice(0, 5) @@ -126,6 +135,15 @@ export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): Qu if (by.amount) { next.amount = parseAmountYi(by.amount.value, by.amount.originValue) } + if (by.amountDelta) { + const raw = by.amountDelta.originValue + const display = by.amountDelta.value + const unavailable = + (raw == null || !Number.isFinite(raw)) && (!display || display === '--') + if (!unavailable) { + next.amountDelta = parseAmountDelta(display, raw) + } + } } if (data.askinfos || data.buyinfos) { diff --git a/src/components/ai/AiAdviceDrawer.vue b/src/components/ai/AiAdviceDrawer.vue index e70500f..47f9742 100644 --- a/src/components/ai/AiAdviceDrawer.vue +++ b/src/components/ai/AiAdviceDrawer.vue @@ -5,7 +5,10 @@ AI {{ data.action }} {{ data.summary }} - 置信度 {{ data.confidence }}% · {{ data.updatedAt }} + + 置信度 {{ data.confidence }}% + +
一键获取建议 diff --git a/src/components/quote/QuoteStats.vue b/src/components/quote/QuoteStats.vue index 3a4687e..9f2718f 100644 --- a/src/components/quote/QuoteStats.vue +++ b/src/components/quote/QuoteStats.vue @@ -23,9 +23,16 @@ const props = defineProps<{ quote: QuoteData }>() +function formatWanSigned(hands: number): string { + const wan = hands / 10000 + const sign = wan > 0 ? '+' : '' + return `${sign}${wan.toFixed(2)}万手` +} + const columns = computed((): StatCell[][] => { const q = props.quote const chgClass = q.change >= 0 ? 'price-up' : 'price-down' + const deltaClass = q.amountDelta >= 0 ? 'price-up' : 'price-down' return [ [ { label: '开盘', value: q.open.toFixed(2) }, @@ -50,6 +57,7 @@ const columns = computed((): StatCell[][] => { [ { label: '最低', value: q.low.toFixed(2), className: 'price-down' }, { label: '昨结', value: q.prevSettlement.toFixed(2) }, + { label: '日增', value: formatWanSigned(q.amountDelta), className: deltaClass }, ], ] }) diff --git a/src/composables/useAiAdvice.ts b/src/composables/useAiAdvice.ts index aea60dc..c67bf63 100644 --- a/src/composables/useAiAdvice.ts +++ b/src/composables/useAiAdvice.ts @@ -10,6 +10,9 @@ import { useTitleBlink } from './useTitleBlink' const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month'] +/** 调度轮询间隔:检查距上次执行是否已超过设定分钟 */ +const SCHEDULE_TICK_MS = 15_000 + function emptyAdvice(): AiAdvice { return { action: '—', @@ -25,6 +28,8 @@ export function useAiAdvice() { const data = ref(emptyAdvice()) const loading = ref(false) const error = ref(null) + /** 上次成功执行分析的时间戳(ms) */ + const lastExecutedAt = ref(null) const quota = useQuotaStore() const settings = useSettingsStore() @@ -76,6 +81,7 @@ export function useAiAdvice() { console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot) data.value = await fetchDeepSeekAdvice(apiKey, payload) + lastExecutedAt.value = Date.now() if (!silent) ElMessage.success('AI 建议已更新') } catch (e) { error.value = e @@ -87,6 +93,25 @@ export function useAiAdvice() { } } + /** 距上次成功执行是否已满设定间隔(从未执行则视为应执行) */ + function shouldRunScheduled(): boolean { + if (!settings.scheduledAnalysisEnabled) return false + if (!isWsTradingSession()) return false + if (loading.value) return false + if (!settings.apiKey.trim()) return false + + const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5) + const last = lastExecutedAt.value + if (last == null) return true + return Date.now() - last >= minutes * 60_000 + } + + function tickSchedule() { + if (shouldRunScheduled()) { + void refresh({ silent: true }) + } + } + function clearSchedule() { if (scheduleTimer) { clearInterval(scheduleTimer) @@ -97,13 +122,9 @@ export function useAiAdvice() { function setupSchedule() { clearSchedule() if (!settings.scheduledAnalysisEnabled) return - const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5) - scheduleTimer = setInterval( - () => { - void refresh({ silent: true }) - }, - minutes * 60_000, - ) + // 开启后立即检查一次,之后按短周期轮询「是否已超过间隔」 + tickSchedule() + scheduleTimer = setInterval(tickSchedule, SCHEDULE_TICK_MS) } watch( @@ -115,7 +136,7 @@ export function useAiAdvice() { onUnmounted(clearSchedule) - return { data, loading, error, refresh } + return { data, loading, error, lastExecutedAt, refresh } } function formatError(e: unknown): string { diff --git a/src/stores/quota.ts b/src/stores/quota.ts index 9939147..ab02c68 100644 --- a/src/stores/quota.ts +++ b/src/stores/quota.ts @@ -70,6 +70,7 @@ function emptyQuote(): QuoteData { volume: 0, amount: 0, openInterest: 0, + amountDelta: 0, amplitude: 0, outerVol: 0, innerVol: 0, diff --git a/src/types/index.ts b/src/types/index.ts index 506bf6b..5fa2d74 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -45,6 +45,8 @@ export interface QuoteData { volume: number amount: number openInterest: number + /** 日增仓(手),对应盘口 amountDelta / 日增 */ + amountDelta: number amplitude: number outerVol: number innerVol: number