AI分析
This commit is contained in:
parent
27c957d3fb
commit
8d1162a2af
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
<span class="badge" :class="data.direction">AI</span>
|
||||
<strong>{{ data.action }}</strong>
|
||||
<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 class="btns">
|
||||
<el-button size="small" :loading="loading" @click="onRefresh">一键获取建议</el-button>
|
||||
|
||||
@ -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 },
|
||||
],
|
||||
]
|
||||
})
|
||||
|
||||
@ -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<AiAdvice>(emptyAdvice())
|
||||
const loading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
/** 上次成功执行分析的时间戳(ms) */
|
||||
const lastExecutedAt = ref<number | null>(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 {
|
||||
|
||||
@ -70,6 +70,7 @@ function emptyQuote(): QuoteData {
|
||||
volume: 0,
|
||||
amount: 0,
|
||||
openInterest: 0,
|
||||
amountDelta: 0,
|
||||
amplitude: 0,
|
||||
outerVol: 0,
|
||||
innerVol: 0,
|
||||
|
||||
@ -45,6 +45,8 @@ export interface QuoteData {
|
||||
volume: number
|
||||
amount: number
|
||||
openInterest: number
|
||||
/** 日增仓(手),对应盘口 amountDelta / 日增 */
|
||||
amountDelta: number
|
||||
amplitude: number
|
||||
outerVol: number
|
||||
innerVol: number
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user