行情数据做状态管理
This commit is contained in:
parent
671a6355da
commit
6a81305bad
@ -24,7 +24,7 @@ import {
|
|||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
import type { Candle, QuoteData } from '../../types'
|
import type { Candle, QuoteData } from '../../types'
|
||||||
import type { KlinePeriod } from '../../composables/useQuote'
|
import type { KlinePeriod } from '../../stores/quota'
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
|
|||||||
@ -1,32 +1,22 @@
|
|||||||
import { onMounted, ref } from 'vue'
|
import { storeToRefs } from 'pinia'
|
||||||
import type { NewsItem } from '../types'
|
import { onMounted } from 'vue'
|
||||||
import { newsMock } from '../mocks/news'
|
import { useQuotaStore } from '../stores/quota'
|
||||||
import { contractConfig } from '../config/contract'
|
|
||||||
import { getFuturesNews } from '../api/baidu/news'
|
|
||||||
import { mapBaiduNewsToItems } from '../api/baidu/mapNews'
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新闻 composable:状态统一落在 quota store。
|
||||||
|
*/
|
||||||
export function useNews() {
|
export function useNews() {
|
||||||
const data = ref<NewsItem[]>(structuredClone(newsMock))
|
const store = useQuotaStore()
|
||||||
const loading = ref(false)
|
const { news: data, newsLoading: loading, newsError: error } = storeToRefs(store)
|
||||||
const error = ref<unknown>(null)
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
try {
|
|
||||||
const list = await getFuturesNews({ code: contractConfig.code })
|
|
||||||
data.value = mapBaiduNewsToItems(list)
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e
|
|
||||||
console.error('[useNews] 拉取新闻失败', e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void refresh()
|
void store.fetchNews()
|
||||||
})
|
})
|
||||||
|
|
||||||
return { data, loading, error, refresh }
|
return {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh: () => store.fetchNews(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,21 @@
|
|||||||
import { ref } from 'vue'
|
import { storeToRefs } from 'pinia'
|
||||||
import type { PositionsData } from '../types'
|
import { useQuotaStore } from '../stores/quota'
|
||||||
import { positionsMock } from '../mocks/positions'
|
|
||||||
|
|
||||||
function delay(ms = 300): Promise<void> {
|
|
||||||
return new Promise((r) => setTimeout(r, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构持仓 composable:状态统一落在 quota store(暂 mock)。
|
||||||
|
*/
|
||||||
export function usePositions() {
|
export function usePositions() {
|
||||||
const data = ref<PositionsData>(structuredClone(positionsMock))
|
const store = useQuotaStore()
|
||||||
const loading = ref(false)
|
const {
|
||||||
const error = ref<unknown>(null)
|
positions: data,
|
||||||
|
positionsLoading: loading,
|
||||||
|
positionsError: error,
|
||||||
|
} = storeToRefs(store)
|
||||||
|
|
||||||
async function refresh() {
|
return {
|
||||||
loading.value = true
|
data,
|
||||||
error.value = null
|
loading,
|
||||||
try {
|
error,
|
||||||
await delay()
|
refresh: () => store.fetchPositions(),
|
||||||
data.value = structuredClone(positionsMock)
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return { data, loading, error, refresh }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,81 +1,28 @@
|
|||||||
import { onMounted, ref } from 'vue'
|
import { storeToRefs } from 'pinia'
|
||||||
import type { Candle, QuoteData } from '../types'
|
import { onMounted } from 'vue'
|
||||||
import { quoteMock } from '../mocks/quote'
|
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
||||||
import { contractConfig } from '../config/contract'
|
|
||||||
import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
|
|
||||||
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
|
||||||
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
|
|
||||||
import type { BaiduKlineType } from '../api/baidu/types'
|
|
||||||
|
|
||||||
export type KlinePeriod = 'day' | 'week' | 'month'
|
export type { KlinePeriod }
|
||||||
|
|
||||||
const KTYPE_MAP: Record<KlinePeriod, BaiduKlineType> = {
|
|
||||||
day: 1,
|
|
||||||
week: 2,
|
|
||||||
month: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行情 composable:状态统一落在 quota store,便于 AI 分析读取。
|
||||||
|
* 页面打开时只拉分时/盘口;K 线点击 Tab 再拉。
|
||||||
|
*/
|
||||||
export function useQuote() {
|
export function useQuote() {
|
||||||
const data = ref<QuoteData>(structuredClone(quoteMock))
|
const store = useQuotaStore()
|
||||||
const loading = ref(false)
|
const { quote: data, quoteLoading: loading, klineLoading, quoteError: error } =
|
||||||
const klineLoading = ref(false)
|
storeToRefs(store)
|
||||||
const error = ref<unknown>(null)
|
|
||||||
|
|
||||||
/** 已加载过的周期,避免重复请求 */
|
|
||||||
const loadedKlines = ref<Record<KlinePeriod, boolean>>({
|
|
||||||
day: false,
|
|
||||||
week: false,
|
|
||||||
month: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
try {
|
|
||||||
const result = await getStockQuotation({ code: contractConfig.code })
|
|
||||||
const quote = mapBaiduQuotationToQuote(result)
|
|
||||||
// 分时刷新时清空 K 线缓存,点击 Tab 再拉
|
|
||||||
quote.candles = { day: [], week: [], month: [] }
|
|
||||||
loadedKlines.value = { day: false, week: false, month: false }
|
|
||||||
data.value = quote
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e
|
|
||||||
console.error('[useQuote] 拉取行情失败', e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 按需加载日/周/月 K 线;已加载则跳过 */
|
|
||||||
async function loadKline(period: KlinePeriod): Promise<Candle[]> {
|
|
||||||
if (loadedKlines.value[period]) {
|
|
||||||
return data.value.candles[period]
|
|
||||||
}
|
|
||||||
|
|
||||||
klineLoading.value = true
|
|
||||||
error.value = null
|
|
||||||
try {
|
|
||||||
const result = await getStockKline({
|
|
||||||
code: contractConfig.code,
|
|
||||||
ktype: KTYPE_MAP[period],
|
|
||||||
})
|
|
||||||
const candles = mapBaiduKlineToCandles(result)
|
|
||||||
data.value.candles[period] = candles
|
|
||||||
loadedKlines.value[period] = true
|
|
||||||
return candles
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e
|
|
||||||
console.error(`[useQuote] 拉取${period}K线失败`, e)
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
klineLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面打开时只请求分时/盘口;K 线点击 Tab 再拉
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void refresh()
|
void store.fetchQuote()
|
||||||
})
|
})
|
||||||
|
|
||||||
return { data, loading, klineLoading, error, refresh, loadKline }
|
return {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
klineLoading,
|
||||||
|
error,
|
||||||
|
refresh: () => store.fetchQuote(),
|
||||||
|
loadKline: (period: KlinePeriod) => store.loadKline(period),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
186
src/stores/quota.ts
Normal file
186
src/stores/quota.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import type { Candle, NewsItem, PositionsData, QuoteData } from '../types'
|
||||||
|
import { quoteMock } from '../mocks/quote'
|
||||||
|
import { newsMock } from '../mocks/news'
|
||||||
|
import { positionsMock } from '../mocks/positions'
|
||||||
|
import { contractConfig } from '../config/contract'
|
||||||
|
import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
|
||||||
|
import { getFuturesNews } from '../api/baidu/news'
|
||||||
|
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
||||||
|
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
|
||||||
|
import { mapBaiduNewsToItems } from '../api/baidu/mapNews'
|
||||||
|
import type { BaiduKlineType } from '../api/baidu/types'
|
||||||
|
|
||||||
|
/** K 线周期(日/周/月) */
|
||||||
|
export type KlinePeriod = 'day' | 'week' | 'month'
|
||||||
|
|
||||||
|
const KTYPE_MAP: Record<KlinePeriod, BaiduKlineType> = {
|
||||||
|
day: 1,
|
||||||
|
week: 2,
|
||||||
|
month: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供 AI 分析消费的数据快照(可序列化) */
|
||||||
|
export interface AnalysisSnapshot {
|
||||||
|
contract: {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
exchange: string
|
||||||
|
}
|
||||||
|
quote: QuoteData | null
|
||||||
|
news: NewsItem[]
|
||||||
|
positions: PositionsData | null
|
||||||
|
fetchedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyCandles(): QuoteData['candles'] {
|
||||||
|
return { day: [], week: [], month: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useQuotaStore = defineStore('quota', () => {
|
||||||
|
// ─── 行情 ───────────────────────────────────────────────
|
||||||
|
const quote = ref<QuoteData>(structuredClone(quoteMock))
|
||||||
|
const quoteLoading = ref(false)
|
||||||
|
const klineLoading = ref(false)
|
||||||
|
const quoteError = ref<unknown>(null)
|
||||||
|
const loadedKlines = ref<Record<KlinePeriod, boolean>>({
|
||||||
|
day: false,
|
||||||
|
week: false,
|
||||||
|
month: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── 新闻 ───────────────────────────────────────────────
|
||||||
|
const news = ref<NewsItem[]>(structuredClone(newsMock))
|
||||||
|
const newsLoading = ref(false)
|
||||||
|
const newsError = ref<unknown>(null)
|
||||||
|
|
||||||
|
// ─── 机构持仓(暂 mock,后续接 API)────────────────────
|
||||||
|
const positions = ref<PositionsData>(structuredClone(positionsMock))
|
||||||
|
const positionsLoading = ref(false)
|
||||||
|
const positionsError = ref<unknown>(null)
|
||||||
|
|
||||||
|
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
|
||||||
|
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
|
||||||
|
const hasNews = computed(() => news.value.length > 0)
|
||||||
|
|
||||||
|
async function fetchQuote() {
|
||||||
|
quoteLoading.value = true
|
||||||
|
quoteError.value = null
|
||||||
|
try {
|
||||||
|
const result = await getStockQuotation({ code: contractConfig.code })
|
||||||
|
const mapped = mapBaiduQuotationToQuote(result)
|
||||||
|
mapped.candles = emptyCandles()
|
||||||
|
loadedKlines.value = { day: false, week: false, month: false }
|
||||||
|
quote.value = mapped
|
||||||
|
} catch (e) {
|
||||||
|
quoteError.value = e
|
||||||
|
console.error('[quota] 拉取行情失败', e)
|
||||||
|
} finally {
|
||||||
|
quoteLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按需加载日/周/月 K 线;已加载则直接返回缓存 */
|
||||||
|
async function loadKline(period: KlinePeriod): Promise<Candle[]> {
|
||||||
|
if (loadedKlines.value[period]) {
|
||||||
|
return quote.value.candles[period]
|
||||||
|
}
|
||||||
|
|
||||||
|
klineLoading.value = true
|
||||||
|
quoteError.value = null
|
||||||
|
try {
|
||||||
|
const result = await getStockKline({
|
||||||
|
code: contractConfig.code,
|
||||||
|
ktype: KTYPE_MAP[period],
|
||||||
|
})
|
||||||
|
const candles = mapBaiduKlineToCandles(result)
|
||||||
|
quote.value.candles[period] = candles
|
||||||
|
loadedKlines.value[period] = true
|
||||||
|
return candles
|
||||||
|
} catch (e) {
|
||||||
|
quoteError.value = e
|
||||||
|
console.error(`[quota] 拉取${period}K线失败`, e)
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
klineLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNews() {
|
||||||
|
newsLoading.value = true
|
||||||
|
newsError.value = null
|
||||||
|
try {
|
||||||
|
const list = await getFuturesNews({ code: contractConfig.code })
|
||||||
|
news.value = mapBaiduNewsToItems(list)
|
||||||
|
} catch (e) {
|
||||||
|
newsError.value = e
|
||||||
|
console.error('[quota] 拉取新闻失败', e)
|
||||||
|
} finally {
|
||||||
|
newsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPositions() {
|
||||||
|
positionsLoading.value = true
|
||||||
|
positionsError.value = null
|
||||||
|
try {
|
||||||
|
// TODO: 接入真实持仓 API 后替换
|
||||||
|
await new Promise((r) => setTimeout(r, 300))
|
||||||
|
positions.value = structuredClone(positionsMock)
|
||||||
|
} catch (e) {
|
||||||
|
positionsError.value = e
|
||||||
|
console.error('[quota] 拉取持仓失败', e)
|
||||||
|
} finally {
|
||||||
|
positionsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 并行刷新行情 + 新闻 + 持仓(K 线仍按需) */
|
||||||
|
async function fetchAll() {
|
||||||
|
await Promise.all([fetchQuote(), fetchNews(), fetchPositions()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出当前全部接口数据快照,供下一步 AI 分析拼接 Prompt。
|
||||||
|
* 返回深拷贝,避免分析过程中被行情刷新污染。
|
||||||
|
*/
|
||||||
|
function getAnalysisSnapshot(): AnalysisSnapshot {
|
||||||
|
return {
|
||||||
|
contract: {
|
||||||
|
code: quote.value.code || contractConfig.code,
|
||||||
|
name: quote.value.name || contractConfig.name,
|
||||||
|
exchange: quote.value.exchange || contractConfig.exchange,
|
||||||
|
},
|
||||||
|
quote: quote.value ? structuredClone(quote.value) : null,
|
||||||
|
news: structuredClone(news.value),
|
||||||
|
positions: positions.value ? structuredClone(positions.value) : null,
|
||||||
|
fetchedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// state
|
||||||
|
quote,
|
||||||
|
quoteLoading,
|
||||||
|
klineLoading,
|
||||||
|
quoteError,
|
||||||
|
loadedKlines,
|
||||||
|
news,
|
||||||
|
newsLoading,
|
||||||
|
newsError,
|
||||||
|
positions,
|
||||||
|
positionsLoading,
|
||||||
|
positionsError,
|
||||||
|
// computed
|
||||||
|
hasQuote,
|
||||||
|
hasNews,
|
||||||
|
// actions
|
||||||
|
fetchQuote,
|
||||||
|
loadKline,
|
||||||
|
fetchNews,
|
||||||
|
fetchPositions,
|
||||||
|
fetchAll,
|
||||||
|
getAnalysisSnapshot,
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user