AI定时分析
This commit is contained in:
parent
8fe2373b2d
commit
27c957d3fb
@ -24,6 +24,25 @@
|
||||
placeholder="用于规定 AI 角色与分析增强,多个词用逗号或换行分隔"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="定时分析">
|
||||
<div class="schedule-row">
|
||||
<el-switch v-model="localScheduledEnabled" active-text="开启" inactive-text="关闭" />
|
||||
<template v-if="localScheduledEnabled">
|
||||
<span class="schedule-label">每</span>
|
||||
<el-input-number
|
||||
v-model="localScheduledMinutes"
|
||||
:min="1"
|
||||
:max="1440"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="schedule-label">分钟分析一次</span>
|
||||
</template>
|
||||
</div>
|
||||
<p class="schedule-hint">
|
||||
开启后仅在开盘时段(09:00–11:30 / 13:30–15:30 / 21:00–23:00)按间隔自动分析;出现买入/卖出信号时浏览器标题会闪烁提醒
|
||||
</p>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
||||
@ -35,7 +54,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useSettingsStore, DEFAULT_KEYWORDS } from '../../stores/settings'
|
||||
import {
|
||||
useSettingsStore,
|
||||
DEFAULT_KEYWORDS,
|
||||
DEFAULT_SCHEDULE_MINUTES,
|
||||
} from '../../stores/settings'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -50,6 +73,8 @@ const emit = defineEmits<{
|
||||
const settings = useSettingsStore()
|
||||
const localKey = ref('')
|
||||
const localKeywords = ref('')
|
||||
const localScheduledEnabled = ref(false)
|
||||
const localScheduledMinutes = ref(DEFAULT_SCHEDULE_MINUTES)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@ -57,6 +82,9 @@ watch(
|
||||
if (open) {
|
||||
localKey.value = settings.apiKey
|
||||
localKeywords.value = settings.keywords || DEFAULT_KEYWORDS
|
||||
localScheduledEnabled.value = settings.scheduledAnalysisEnabled
|
||||
localScheduledMinutes.value =
|
||||
settings.scheduledAnalysisMinutes || DEFAULT_SCHEDULE_MINUTES
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -64,7 +92,34 @@ watch(
|
||||
function save() {
|
||||
settings.apiKey = localKey.value.trim()
|
||||
settings.keywords = localKeywords.value.trim() || DEFAULT_KEYWORDS
|
||||
settings.scheduledAnalysisEnabled = localScheduledEnabled.value
|
||||
settings.scheduledAnalysisMinutes = Math.max(
|
||||
1,
|
||||
Math.min(1440, Math.floor(localScheduledMinutes.value || DEFAULT_SCHEDULE_MINUTES)),
|
||||
)
|
||||
ElMessage.success('设置已保存到本地')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.schedule-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schedule-label {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.schedule-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { ref } from 'vue'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { AiAdvice } from '../types'
|
||||
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'
|
||||
import { isWsTradingSession } from '../utils/tradingDate'
|
||||
import { useTitleBlink } from './useTitleBlink'
|
||||
|
||||
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
|
||||
|
||||
@ -26,6 +28,10 @@ export function useAiAdvice() {
|
||||
const quota = useQuotaStore()
|
||||
const settings = useSettingsStore()
|
||||
|
||||
useTitleBlink(data)
|
||||
|
||||
let scheduleTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 尽量补全日/周/月 K 线,失败不阻断主流程 */
|
||||
async function ensureKlines() {
|
||||
await Promise.all(
|
||||
@ -41,13 +47,19 @@ export function useAiAdvice() {
|
||||
|
||||
/**
|
||||
* 一键获取建议:收集行情 / 新闻 / 持仓后提交 DeepSeek,返回真实 AI 建议。
|
||||
* @param opts.silent 定时任务时静默:无 Key / 非开盘 / 进行中则跳过,成功不弹 toast
|
||||
*/
|
||||
async function refresh() {
|
||||
async function refresh(opts?: { silent?: boolean }) {
|
||||
const silent = Boolean(opts?.silent)
|
||||
// 定时分析仅在开盘时段执行(与行情 WS 时段一致)
|
||||
if (silent && !isWsTradingSession()) return
|
||||
|
||||
const apiKey = settings.apiKey.trim()
|
||||
if (!apiKey) {
|
||||
ElMessage.warning('请先在设置中配置 DeepSeek API Key')
|
||||
if (!silent) ElMessage.warning('请先在设置中配置 DeepSeek API Key')
|
||||
return
|
||||
}
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
@ -64,17 +76,45 @@ export function useAiAdvice() {
|
||||
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
|
||||
|
||||
data.value = await fetchDeepSeekAdvice(apiKey, payload)
|
||||
ElMessage.success('AI 建议已更新')
|
||||
if (!silent) ElMessage.success('AI 建议已更新')
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
const msg = formatError(e)
|
||||
console.error('[AI] 一键获取建议失败', e)
|
||||
ElMessage.error(msg)
|
||||
if (!silent) ElMessage.error(msg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSchedule() {
|
||||
if (scheduleTimer) {
|
||||
clearInterval(scheduleTimer)
|
||||
scheduleTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function setupSchedule() {
|
||||
clearSchedule()
|
||||
if (!settings.scheduledAnalysisEnabled) return
|
||||
const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5)
|
||||
scheduleTimer = setInterval(
|
||||
() => {
|
||||
void refresh({ silent: true })
|
||||
},
|
||||
minutes * 60_000,
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() =>
|
||||
[settings.scheduledAnalysisEnabled, settings.scheduledAnalysisMinutes] as const,
|
||||
() => setupSchedule(),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(clearSchedule)
|
||||
|
||||
return { data, loading, error, refresh }
|
||||
}
|
||||
|
||||
|
||||
89
src/composables/useTitleBlink.ts
Normal file
89
src/composables/useTitleBlink.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { onUnmounted, watch, type Ref } from 'vue'
|
||||
import type { AiAdvice } from '../types'
|
||||
|
||||
const DEFAULT_TITLE = 'AI 实时交易辅助系统'
|
||||
const BLINK_MS = 800
|
||||
|
||||
function hasTradeSignal(advice: AiAdvice): boolean {
|
||||
return advice.direction === 'long' || advice.direction === 'short'
|
||||
}
|
||||
|
||||
function signalTitle(advice: AiAdvice): string {
|
||||
const tag = advice.direction === 'long' ? '买入' : '卖出'
|
||||
return `【${tag}信号】${advice.action} · ${DEFAULT_TITLE}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 有多空交易信号时闪烁 document.title;页面获得焦点后停止,直至下一次新信号。
|
||||
*/
|
||||
export function useTitleBlink(advice: Ref<AiAdvice>) {
|
||||
const baseTitle = document.title || DEFAULT_TITLE
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
let showAlert = false
|
||||
/** 用户已看过当前信号,不再闪烁,直到信号内容变化 */
|
||||
let dismissedKey = ''
|
||||
let activeKey = ''
|
||||
|
||||
function stopBlink() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
showAlert = false
|
||||
activeKey = ''
|
||||
document.title = baseTitle
|
||||
}
|
||||
|
||||
function startBlink(alertTitle: string, key: string) {
|
||||
stopBlink()
|
||||
activeKey = key
|
||||
showAlert = true
|
||||
document.title = alertTitle
|
||||
timer = setInterval(() => {
|
||||
showAlert = !showAlert
|
||||
document.title = showAlert ? alertTitle : baseTitle
|
||||
}, BLINK_MS)
|
||||
}
|
||||
|
||||
function signalKey(a: AiAdvice): string {
|
||||
return `${a.direction}|${a.action}|${a.updatedAt}`
|
||||
}
|
||||
|
||||
function sync() {
|
||||
const a = advice.value
|
||||
if (!hasTradeSignal(a)) {
|
||||
stopBlink()
|
||||
dismissedKey = ''
|
||||
return
|
||||
}
|
||||
const key = signalKey(a)
|
||||
if (key === dismissedKey) {
|
||||
stopBlink()
|
||||
return
|
||||
}
|
||||
if (timer && activeKey === key) return
|
||||
startBlink(signalTitle(a), key)
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
const a = advice.value
|
||||
if (hasTradeSignal(a) && timer) {
|
||||
dismissedKey = signalKey(a)
|
||||
}
|
||||
stopBlink()
|
||||
}
|
||||
|
||||
function onVisibility() {
|
||||
if (document.visibilityState === 'visible') onFocus()
|
||||
}
|
||||
|
||||
watch(advice, sync, { deep: true, immediate: true })
|
||||
window.addEventListener('focus', onFocus)
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
|
||||
onUnmounted(() => {
|
||||
stopBlink()
|
||||
window.removeEventListener('focus', onFocus)
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
})
|
||||
}
|
||||
@ -8,6 +8,9 @@ const STORAGE_KEY = 'ai-trade-settings'
|
||||
export const DEFAULT_KEYWORDS =
|
||||
'你是一名专业的国内期货交易员和分析员,精通机构操盘手法,能够根据持仓与交易数据分析当前走势。'
|
||||
|
||||
/** 定时分析默认间隔(分钟) */
|
||||
export const DEFAULT_SCHEDULE_MINUTES = 5
|
||||
|
||||
function load(): Partial<AppSettings> {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') as Partial<AppSettings>
|
||||
@ -16,18 +19,37 @@ function load(): Partial<AppSettings> {
|
||||
}
|
||||
}
|
||||
|
||||
function clampMinutes(n: unknown): number {
|
||||
const v = typeof n === 'number' ? n : Number(n)
|
||||
if (!Number.isFinite(v) || v < 1) return DEFAULT_SCHEDULE_MINUTES
|
||||
return Math.min(1440, Math.floor(v))
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const saved = load()
|
||||
const apiKey = ref(saved.apiKey || '')
|
||||
const keywords = ref(saved.keywords || DEFAULT_KEYWORDS)
|
||||
const scheduledAnalysisEnabled = ref(Boolean(saved.scheduledAnalysisEnabled))
|
||||
const scheduledAnalysisMinutes = ref(clampMinutes(saved.scheduledAnalysisMinutes))
|
||||
|
||||
watch([apiKey, keywords], () => {
|
||||
watch(
|
||||
[apiKey, keywords, scheduledAnalysisEnabled, scheduledAnalysisMinutes],
|
||||
() => {
|
||||
const payload: AppSettings = {
|
||||
apiKey: apiKey.value,
|
||||
keywords: keywords.value,
|
||||
scheduledAnalysisEnabled: scheduledAnalysisEnabled.value,
|
||||
scheduledAnalysisMinutes: clampMinutes(scheduledAnalysisMinutes.value),
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload))
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return { apiKey, keywords }
|
||||
return {
|
||||
apiKey,
|
||||
keywords,
|
||||
scheduledAnalysisEnabled,
|
||||
scheduledAnalysisMinutes,
|
||||
}
|
||||
})
|
||||
|
||||
@ -137,4 +137,8 @@ export interface AiAdvice {
|
||||
export interface AppSettings {
|
||||
apiKey: string
|
||||
keywords: string
|
||||
/** 是否开启定时分析 */
|
||||
scheduledAnalysisEnabled: boolean
|
||||
/** 定时分析间隔(分钟) */
|
||||
scheduledAnalysisMinutes: number
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user