This commit is contained in:
dongzp 2026-07-22 11:19:10 +08:00
parent 27c957d3fb
commit 8d1162a2af
7 changed files with 83 additions and 9 deletions

View File

@ -22,6 +22,22 @@ function parseAmountYi(value: string | undefined, rawAmount?: string): number {
return toNum(value) 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[] { function parseIntraday(result: BaiduQuotationResult): IntradayPoint[] {
const days = result.newMarketData?.marketData ?? [] const days = result.newMarketData?.marketData ?? []
const points: IntradayPoint[] = [] const points: IntradayPoint[] = []
@ -99,6 +115,10 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat
result.pankouinfos?.list?.find((i) => i.ename === 'amount')?.value, result.pankouinfos?.list?.find((i) => i.ename === 'amount')?.value,
op?.amount, op?.amount,
) )
const amountDelta = parseAmountDelta(
result.pankouinfos?.list?.find((i) => i.ename === 'amountDelta')?.value,
op?.amountDelta,
)
return { return {
name: basic?.name || contractConfig.name, name: basic?.name || contractConfig.name,
@ -118,6 +138,7 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat
volume: toNum(op?.volume), volume: toNum(op?.volume),
amount: amountYi, amount: amountYi,
openInterest: toNum(op?.holdingAmount), openInterest: toNum(op?.holdingAmount),
amountDelta,
amplitude: toNum(op?.amplitudeRatio), amplitude: toNum(op?.amplitudeRatio),
outerVol: outside, outerVol: outside,
innerVol: inside, innerVol: inside,

View File

@ -28,6 +28,15 @@ function parseAmountYi(value: string | undefined, originValue?: number): number
return toNum(value) 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」. */ /** Extract HH:mm from 「07-15 10:59」 or 「10:59」. */
function hhmmFromPointTime(time: string): string { function hhmmFromPointTime(time: string): string {
if (time.includes(' ')) return time.split(' ')[1]!.slice(0, 5) 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) { if (by.amount) {
next.amount = parseAmountYi(by.amount.value, by.amount.originValue) 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) { if (data.askinfos || data.buyinfos) {

View File

@ -5,7 +5,10 @@
<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 class="text">{{ data.summary }}</span> <span class="text">{{ data.summary }}</span>
<span class="meta">置信度 {{ data.confidence }}% · {{ data.updatedAt }}</span> <span class="meta">
置信度 {{ data.confidence }}%
<template v-if="data.updatedAt"> · 执行 {{ data.updatedAt }}</template>
</span>
</div> </div>
<div class="btns"> <div class="btns">
<el-button size="small" :loading="loading" @click="onRefresh">一键获取建议</el-button> <el-button size="small" :loading="loading" @click="onRefresh">一键获取建议</el-button>

View File

@ -23,9 +23,16 @@ const props = defineProps<{
quote: QuoteData 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 columns = computed((): StatCell[][] => {
const q = props.quote const q = props.quote
const chgClass = q.change >= 0 ? 'price-up' : 'price-down' const chgClass = q.change >= 0 ? 'price-up' : 'price-down'
const deltaClass = q.amountDelta >= 0 ? 'price-up' : 'price-down'
return [ return [
[ [
{ label: '开盘', value: q.open.toFixed(2) }, { 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.low.toFixed(2), className: 'price-down' },
{ label: '昨结', value: q.prevSettlement.toFixed(2) }, { label: '昨结', value: q.prevSettlement.toFixed(2) },
{ label: '日增', value: formatWanSigned(q.amountDelta), className: deltaClass },
], ],
] ]
}) })

View File

@ -10,6 +10,9 @@ import { useTitleBlink } from './useTitleBlink'
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month'] const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
/** 调度轮询间隔:检查距上次执行是否已超过设定分钟 */
const SCHEDULE_TICK_MS = 15_000
function emptyAdvice(): AiAdvice { function emptyAdvice(): AiAdvice {
return { return {
action: '—', action: '—',
@ -25,6 +28,8 @@ export function useAiAdvice() {
const data = ref<AiAdvice>(emptyAdvice()) const data = ref<AiAdvice>(emptyAdvice())
const loading = ref(false) const loading = ref(false)
const error = ref<unknown>(null) const error = ref<unknown>(null)
/** 上次成功执行分析的时间戳(ms) */
const lastExecutedAt = ref<number | null>(null)
const quota = useQuotaStore() const quota = useQuotaStore()
const settings = useSettingsStore() const settings = useSettingsStore()
@ -76,6 +81,7 @@ export function useAiAdvice() {
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot) console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
data.value = await fetchDeepSeekAdvice(apiKey, payload) data.value = await fetchDeepSeekAdvice(apiKey, payload)
lastExecutedAt.value = Date.now()
if (!silent) ElMessage.success('AI 建议已更新') if (!silent) ElMessage.success('AI 建议已更新')
} catch (e) { } catch (e) {
error.value = 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() { function clearSchedule() {
if (scheduleTimer) { if (scheduleTimer) {
clearInterval(scheduleTimer) clearInterval(scheduleTimer)
@ -97,13 +122,9 @@ export function useAiAdvice() {
function setupSchedule() { function setupSchedule() {
clearSchedule() clearSchedule()
if (!settings.scheduledAnalysisEnabled) return if (!settings.scheduledAnalysisEnabled) return
const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5) // 开启后立即检查一次,之后按短周期轮询「是否已超过间隔」
scheduleTimer = setInterval( tickSchedule()
() => { scheduleTimer = setInterval(tickSchedule, SCHEDULE_TICK_MS)
void refresh({ silent: true })
},
minutes * 60_000,
)
} }
watch( watch(
@ -115,7 +136,7 @@ export function useAiAdvice() {
onUnmounted(clearSchedule) onUnmounted(clearSchedule)
return { data, loading, error, refresh } return { data, loading, error, lastExecutedAt, refresh }
} }
function formatError(e: unknown): string { function formatError(e: unknown): string {

View File

@ -70,6 +70,7 @@ function emptyQuote(): QuoteData {
volume: 0, volume: 0,
amount: 0, amount: 0,
openInterest: 0, openInterest: 0,
amountDelta: 0,
amplitude: 0, amplitude: 0,
outerVol: 0, outerVol: 0,
innerVol: 0, innerVol: 0,

View File

@ -45,6 +45,8 @@ export interface QuoteData {
volume: number volume: number
amount: number amount: number
openInterest: number openInterest: number
/** 日增仓(手),对应盘口 amountDelta / 日增 */
amountDelta: number
amplitude: number amplitude: number
outerVol: number outerVol: number
innerVol: number innerVol: number