AI定时分析
This commit is contained in:
parent
8fe2373b2d
commit
27c957d3fb
@ -24,6 +24,25 @@
|
|||||||
placeholder="用于规定 AI 角色与分析增强,多个词用逗号或换行分隔"
|
placeholder="用于规定 AI 角色与分析增强,多个词用逗号或换行分隔"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
||||||
@ -35,7 +54,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
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(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -50,6 +73,8 @@ const emit = defineEmits<{
|
|||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const localKey = ref('')
|
const localKey = ref('')
|
||||||
const localKeywords = ref('')
|
const localKeywords = ref('')
|
||||||
|
const localScheduledEnabled = ref(false)
|
||||||
|
const localScheduledMinutes = ref(DEFAULT_SCHEDULE_MINUTES)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@ -57,6 +82,9 @@ watch(
|
|||||||
if (open) {
|
if (open) {
|
||||||
localKey.value = settings.apiKey
|
localKey.value = settings.apiKey
|
||||||
localKeywords.value = settings.keywords || DEFAULT_KEYWORDS
|
localKeywords.value = settings.keywords || DEFAULT_KEYWORDS
|
||||||
|
localScheduledEnabled.value = settings.scheduledAnalysisEnabled
|
||||||
|
localScheduledMinutes.value =
|
||||||
|
settings.scheduledAnalysisMinutes || DEFAULT_SCHEDULE_MINUTES
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -64,7 +92,34 @@ watch(
|
|||||||
function save() {
|
function save() {
|
||||||
settings.apiKey = localKey.value.trim()
|
settings.apiKey = localKey.value.trim()
|
||||||
settings.keywords = localKeywords.value.trim() || DEFAULT_KEYWORDS
|
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('设置已保存到本地')
|
ElMessage.success('设置已保存到本地')
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
</script>
|
</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 { ElMessage } from 'element-plus'
|
||||||
import type { AiAdvice } from '../types'
|
import type { AiAdvice } from '../types'
|
||||||
import { fetchDeepSeekAdvice } from '../api/deepseek/analyze'
|
import { fetchDeepSeekAdvice } from '../api/deepseek/analyze'
|
||||||
import type { AiAnalysisPayload } from '../api/deepseek/types'
|
import type { AiAnalysisPayload } from '../api/deepseek/types'
|
||||||
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
|
import { isWsTradingSession } from '../utils/tradingDate'
|
||||||
|
import { useTitleBlink } from './useTitleBlink'
|
||||||
|
|
||||||
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
|
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
|
||||||
|
|
||||||
@ -26,6 +28,10 @@ export function useAiAdvice() {
|
|||||||
const quota = useQuotaStore()
|
const quota = useQuotaStore()
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
|
useTitleBlink(data)
|
||||||
|
|
||||||
|
let scheduleTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
/** 尽量补全日/周/月 K 线,失败不阻断主流程 */
|
/** 尽量补全日/周/月 K 线,失败不阻断主流程 */
|
||||||
async function ensureKlines() {
|
async function ensureKlines() {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -41,13 +47,19 @@ export function useAiAdvice() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 一键获取建议:收集行情 / 新闻 / 持仓后提交 DeepSeek,返回真实 AI 建议。
|
* 一键获取建议:收集行情 / 新闻 / 持仓后提交 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()
|
const apiKey = settings.apiKey.trim()
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
ElMessage.warning('请先在设置中配置 DeepSeek API Key')
|
if (!silent) ElMessage.warning('请先在设置中配置 DeepSeek API Key')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
@ -64,17 +76,45 @@ 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)
|
||||||
ElMessage.success('AI 建议已更新')
|
if (!silent) ElMessage.success('AI 建议已更新')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e
|
error.value = e
|
||||||
const msg = formatError(e)
|
const msg = formatError(e)
|
||||||
console.error('[AI] 一键获取建议失败', e)
|
console.error('[AI] 一键获取建议失败', e)
|
||||||
ElMessage.error(msg)
|
if (!silent) ElMessage.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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 }
|
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_KEYWORDS =
|
||||||
'你是一名专业的国内期货交易员和分析员,精通机构操盘手法,能够根据持仓与交易数据分析当前走势。'
|
'你是一名专业的国内期货交易员和分析员,精通机构操盘手法,能够根据持仓与交易数据分析当前走势。'
|
||||||
|
|
||||||
|
/** 定时分析默认间隔(分钟) */
|
||||||
|
export const DEFAULT_SCHEDULE_MINUTES = 5
|
||||||
|
|
||||||
function load(): Partial<AppSettings> {
|
function load(): Partial<AppSettings> {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') as Partial<AppSettings>
|
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', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
const saved = load()
|
const saved = load()
|
||||||
const apiKey = ref(saved.apiKey || '')
|
const apiKey = ref(saved.apiKey || '')
|
||||||
const keywords = ref(saved.keywords || DEFAULT_KEYWORDS)
|
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 = {
|
const payload: AppSettings = {
|
||||||
apiKey: apiKey.value,
|
apiKey: apiKey.value,
|
||||||
keywords: keywords.value,
|
keywords: keywords.value,
|
||||||
|
scheduledAnalysisEnabled: scheduledAnalysisEnabled.value,
|
||||||
|
scheduledAnalysisMinutes: clampMinutes(scheduledAnalysisMinutes.value),
|
||||||
}
|
}
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload))
|
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 {
|
export interface AppSettings {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
keywords: string
|
keywords: string
|
||||||
|
/** 是否开启定时分析 */
|
||||||
|
scheduledAnalysisEnabled: boolean
|
||||||
|
/** 定时分析间隔(分钟) */
|
||||||
|
scheduledAnalysisMinutes: number
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user