import { shallowRef, reactive, computed, toRefs, onUnmounted } from 'vue' import { v4 } from 'uuid' import { timerTask } from '@/utils/timer' import { handlePriceColor } from '@/filters' import { queryMemberGoodsLimitConfig } from '@/services/api/common' import { getTodayAccountConfigInfo } from '@/services/api/account' import { queryErmcpGoods, queryQuoteDay } from '@/services/api/goods' import { wordArrayToUint8Array } from '@/services/websocket/package/crypto' import { decodeProto } from '@/services/websocket/package/package50/proto' import { defineStore } from '../store' import { useAccountStore } from './account' import CryptoJS from 'crypto-js' import eventBus from '@/services/bus' import moment from 'moment' /** * 期货存储对象 * @returns */ export const useFuturesStore = defineStore(() => { // 行情监听集合 const quoteWatchMap = new Map) => void; }>() // 当前市场ID const marketIds = shallowRef([]) // 请求商品列表成功后的回调集合 // 由于异步请求原因,页面发起行情订阅时商品数据可能还不存在 // 将回调事件收集到合集中,待请求成功后再执行 const dataCompletedCallback = new Set<() => void>() const state = reactive({ loading: false, goodsList: <(Model.GoodsRsp & Partial)[]>[], // 商品列表 quotationList: [], // 行情列表 selectedGoodsId: undefined, // 当前选中的商品ID }) // 指定市场ID的商品列表 const marketGoodsList = computed(() => { const list = state.quotationList.filter((e) => marketIds.value.includes(e.marketid)) return list.sort((a, b) => a.goodscode.localeCompare(b.goodscode)) // 根据 goodscode 字母升序排序 }) // 当前选中的商品 const selectedGoods = computed(() => state.quotationList.find((e) => e.goodsid === state.selectedGoodsId)) // 设置当前市场ID const setMarketId = (...values: number[]) => { marketIds.value = values const list = marketGoodsList.value if (list.length) { // 查找当前选中的商品ID if (!list.every((e) => e.goodsid === state.selectedGoodsId)) { state.selectedGoodsId = list[0].goodsid } } else { state.selectedGoodsId = undefined } } const getGoodsListByMarketId = (...marketId: number[]) => { const list = state.quotationList.filter((e) => marketId.includes(e.marketid)) return list.sort((a, b) => a.goodscode.localeCompare(b.goodscode)) // 根据 goodscode 字母升序排序 } const getGoodsListByTradeMode = (...tradeMode: number[]) => { const list = state.quotationList.filter((e) => tradeMode.includes(e.trademode)) return list.sort((a, b) => a.goodscode.localeCompare(b.goodscode)) // 根据 goodscode 字母升序排序 } // 获取商品列表 const getGoodsList = async () => { state.loading = true state.goodsList = [] state.quotationList = [] timerTask.clearTimeout('quoteDay') const accountStore = useAccountStore() // 任务 #5511 const { data: accountConfig } = await getTodayAccountConfigInfo() // 任务 #5197 const { data: limitConfig } = await queryMemberGoodsLimitConfig({ data: { roletype: 7 } }) const { data: goodsList } = await queryErmcpGoods() for (let i = 0; i < goodsList.length; i++) { const item = goodsList[i] const limit = limitConfig.find((e) => e.goodsid === item.goodsid) // 跳过不显示的商品 if (limit?.isnodisplay) { continue } // 更新商品配置 const findAccountConfig = accountConfig.todayAccountMargins.find((e) => e.accountid === accountStore.currentAccountId && e.goodsid === item.goodsid) const config = findAccountConfig ?? accountConfig.todayAccountMargins.find((e) => e.accountid === 0 && e.goodsid === item.goodsid) if (config) { const wordArray = CryptoJS.enc.Base64.parse(config.infocontent) // 解析base64 const uint8Array = wordArrayToUint8Array(wordArray) const res = await decodeProto('MarginInfoStruct', uint8Array) // proto数据解析 item.marketmarginalgorithm = res.MarginAlgorithm item.marketmarginvalue = res.MarketMarginValue } // 组合商品属性 state.goodsList.push({ ...item, iscannotbuy: limit?.iscannotbuy ?? 0, iscannotsell: limit?.iscannotsell ?? 0 }) } // 待优化 getQuoteDay().then(() => { dataCompletedCallback.forEach((callback) => callback()) dataCompletedCallback.clear() }).finally(() => { state.loading = false if (!state.selectedGoodsId) { state.selectedGoodsId = marketGoodsList.value[0]?.goodsid } // 每5分钟获取一次盘面 timerTask.setTimeout(() => getQuoteDay(), 5 * 60 * 1000, 'quoteDay') }) } // 获取商品盘面信息(待优化) const getQuoteDay = async () => { state.loading = true const values = state.goodsList.map((e) => e.goodscode.toString()) const res = await queryQuoteDay({ data: { goodsCodes: values.join(',') } }) // 只遍历有盘面的商品 // res.data.forEach((item) => { // updateQuotation(item) // }) // 遍历所有商品 values.forEach((goodscode) => { const item = res.data.find((e_1) => e_1.goodscode === goodscode) updateQuotation(item ?? { goodscode }) }) state.loading = false } // 盘面数据加载完成后触发 const onDataCompleted = (callback: () => void) => { if (state.quotationList.length) { setTimeout(() => callback(), 0) } else { dataCompletedCallback.add(callback) onUnmounted(() => dataCompletedCallback.delete(callback)) } } // 通过 goodscode 获取实时盘面 const getGoodsQuote = (code: string | number) => { return computed(() => state.quotationList.find((e) => e.goodscode === code || e.goodsid === code)) } // 通过 goodscode 获取实时行情报价 const getQuotePrice = (goodsCode: string) => { return computed(() => { const quote = getGoodsQuote(goodsCode) const { last = 0, presettle = 0, preclose = 0 } = quote.value ?? {} if (last != 0.0) { return last } else if (presettle != 0.0) { return presettle } else if (preclose != 0.0) { return preclose } else { return 0.0 } }) } // 获取商品名称 const getGoodsName = (code: string | number) => { const goods = getGoods(code) return goods?.goodsname ?? '' } // 获取对应的商品 const getGoods = (code: string | number) => { const goods = state.goodsList.find((e) => e.goodscode === code || e.goodsid === code) return goods } // 获取商品市场ID const getGoodsMarket = (code: string | number) => { const quote = state.goodsList.find((e) => e.goodscode === code || e.goodsid === code) return quote?.marketid ?? 0 } // 监听行情推送 const quoteWatch = (goodsCodes: string | string[], callback: (value: Partial) => void) => { const uuid = v4() quoteWatchMap.set(uuid, { keys: Array.isArray(goodsCodes) ? goodsCodes : [goodsCodes], callback }) const append = (...goodsCodes: string[]) => { const value = quoteWatchMap.get(uuid) value?.keys.push(...goodsCodes) } const stop = () => { quoteWatchMap.delete(uuid) } // 页面离开时停止监听事件,防止事件重复触发 onUnmounted(() => stop()) return { uuid, append, stop } } // 更新行情数据 const updateQuotation = (quote: Partial) => { // 查找对应的商品行情 const item: Model.GoodsQuote = state.quotationList.find((e) => e.goodscode === quote.goodscode) ?? { goodsid: 0, goodscode: quote.goodscode ?? '', goodsname: '', goodsgroupid: 0, goodunitid: 0, marketid: 0, trademode: 0, agreeunit: 0, decimalplace: 0, decimalvalue: 0, quoteminunit: 0, quotegear: 0, last: quote.last ?? 0, lasttime: quote.lasttime ?? '', bid: quote.bid ?? 0, bid2: quote.bid2 ?? 0, bid3: quote.bid3 ?? 0, bid4: quote.bid4 ?? 0, bid5: quote.bid5 ?? 0, bidvolume: quote.bidvolume ?? 0, bidvolume2: quote.bidvolume2 ?? 0, bidvolume3: quote.bidvolume3 ?? 0, bidvolume4: quote.bidvolume4 ?? 0, bidvolume5: quote.bidvolume5 ?? 0, ask: quote.ask ?? 0, ask2: quote.ask2 ?? 0, ask3: quote.ask3 ?? 0, ask4: quote.ask4 ?? 0, ask5: quote.ask5 ?? 0, askvolume: quote.askvolume ?? 0, askvolume2: quote.askvolume2 ?? 0, askvolume3: quote.askvolume3 ?? 0, askvolume4: quote.askvolume4 ?? 0, askvolume5: quote.askvolume5 ?? 0, lastvolume: quote.lastvolume ?? 0, lastturnover: quote.Lastturnover ?? 0, presettle: quote.presettle ?? 0, preclose: quote.preclose ?? 0, opened: quote.opened ?? 0, highest: quote.highest ?? 0, lowest: quote.lowest ?? 0, limitup: quote.limitup ?? 0, limitdown: quote.limitdown ?? 0, averageprice: quote.averageprice ?? 0, totalvolume: quote.totalvolume ?? 0, totalturnover: quote.totalturnover ?? 0, holdvolume: quote.holdvolume ?? 0, provideraccountid: 0, currencyid: 1, provideruserid: 0, marketmarginalgorithm: 0, marketmarginvalue: 0, goodstradetype: 0, maxspread: 0, minspread: 0, goodsorder: '', tradeproperty: 0, rise: 0, change: 0, amplitude: 0, iscannotbuy: 0, iscannotsell: 0, bidColor: '', bid2Color: '', bid3Color: '', bid4Color: '', bid5Color: '', askColor: '', ask2Color: '', ask3Color: '', ask4Color: '', ask5Color: '', lastColor: '', averagepriceColor: '', openedColor: '', highestColor: '', lowestColor: '', } if (item.goodsid) { // 更新对象属性 Object.entries(quote).forEach(([key, value]) => { if (value !== undefined) { type TKey = keyof Model.GoodsQuote ((prop: K, value: Model.GoodsQuote[K]) => { item[prop] = value })(key as TKey, value) } }) } else { const goods = state.goodsList.find((e) => e.goodscode === quote.goodscode) if (goods) { ({ goodsid: item.goodsid, goodsname: item.goodsname, goodsgroupid: item.goodsgroupid, goodunitid: item.goodunitid, marketid: item.marketid, trademode: item.trademode, agreeunit: item.agreeunit, decimalplace: item.decimalplace, quoteminunit: item.quoteminunit, quotegear: item.quotegear, marketmarginalgorithm: item.marketmarginalgorithm, marketmarginvalue: item.marketmarginvalue, maxspread: item.maxspread, minspread: item.minspread, goodsorder: item.goodsorder, tradeproperty: item.tradeproperty, provideraccountid: item.provideraccountid, provideruserid: item.provideruserid, goodstradetype: item.goodstradetype, currencyid: item.currencyid } = goods) item.iscannotbuy = goods.iscannotbuy ?? 0 item.iscannotsell = goods.iscannotsell ?? 0 // 向列表添加新数据 state.quotationList.push(item) } } const price = item.trademode === 52 ? Math.max(item.last, item.ask) : item.last // 任务 #5677 item.opened = item.opened || item.last // 没有开盘价默认取最新价 item.highest = item.highest || price // 没有最高价默认取最新价 item.lowest = item.lowest || item.last // 没有最低价价默认取最新价 // 处理最高最低价 if (item.last) { if (price > item.highest) { item.highest = price } if (item.last < item.lowest) { item.lowest = item.last } } item.averageprice = item.totalvolume ? item.totalturnover / (item.totalvolume * item.agreeunit) : 0 // 计算均价 item.rise = item.last ? item.last - item.presettle : 0 // 涨跌额/涨跌: 最新价 - 昨结价 item.change = item.presettle ? item.rise / item.presettle : 0 // 涨跌幅/幅度: (最新价 - 昨结价) / 昨结价 item.amplitude = item.presettle ? (item.highest - item.lowest) / item.presettle : 0 // 振幅: (最高价 - 最低价 ) / 最新价 // 计算小数单位值 item.decimalvalue = item.quoteminunit * Math.pow(10, item.decimalplace * -1) // 处理行情价格颜色 const handleColor = (value: number | string) => handlePriceColor(Number(value), item.presettle) item.bidColor = handleColor(item.bid) item.bid2Color = handleColor(item.bid2) item.bid3Color = handleColor(item.bid3) item.bid4Color = handleColor(item.bid4) item.bid5Color = handleColor(item.bid5) item.askColor = handleColor(item.ask) item.ask2Color = handleColor(item.ask2) item.ask3Color = handleColor(item.ask3) item.ask4Color = handleColor(item.ask4) item.ask5Color = handleColor(item.ask5) item.lastColor = handleColor(item.last) item.averagepriceColor = handleColor(item.averageprice.toFixed(item.decimalplace)) item.openedColor = handleColor(item.opened) item.highestColor = handleColor(item.highest) item.lowestColor = handleColor(item.lowest) } // 处理行情数据 const handleQuotation = (quote: Proto.Quote): Partial => { const goods = state.goodsList.find((e) => e.goodscode.toUpperCase() === quote.goodscode?.toUpperCase()) // 处理报价小数位 const handleDeimalplace = (value?: number) => { if (goods && value) { const decimal = Math.pow(10, goods.decimalplace) return value / decimal } return value } return { goodscode: quote.goodscode, last: handleDeimalplace(quote.last), lasttime: (quote.date && quote.time) ? moment(quote.date + quote.time, 'YYYYMMDDHHmmss').toISOString(true) : undefined, ask: handleDeimalplace(quote.ask), ask2: handleDeimalplace(quote.ask2), ask3: handleDeimalplace(quote.ask3), ask4: handleDeimalplace(quote.ask4), ask5: handleDeimalplace(quote.ask5), askvolume: quote.askvolume, askvolume2: quote.askvolume2, askvolume3: quote.askvolume3, askvolume4: quote.askvolume4, askvolume5: quote.askvolume5, bid: handleDeimalplace(quote.bid), bid2: handleDeimalplace(quote.bid2), bid3: handleDeimalplace(quote.bid3), bid4: handleDeimalplace(quote.bid4), bid5: handleDeimalplace(quote.bid5), bidvolume: quote.bidvolume, bidvolume2: quote.bidvolume2, bidvolume3: quote.bidvolume3, bidvolume4: quote.bidvolume4, bidvolume5: quote.bidvolume5, calloptionpremiums: quote.calloptionpremiums, calloptionpremiums2: quote.calloptionpremiums2, calloptionpremiums3: quote.calloptionpremiums3, calloptionpremiums4: quote.calloptionpremiums4, calloptionpremiums5: quote.calloptionpremiums5, exchangecode: quote.exchangecode, exchangedate: quote.exchangedate, highest: handleDeimalplace(quote.highest), holdvolume: quote.holdvolume, inventory: quote.inventory, lastvolume: quote.lastvolume, Lastturnover: handleDeimalplace(quote.lastturnover), limitdown: handleDeimalplace(quote.limitlow), limitup: handleDeimalplace(quote.limithigh), lowest: handleDeimalplace(quote.lowest), opened: handleDeimalplace(quote.opened), preclose: handleDeimalplace(quote.preclose), preholdvolume: quote.preholdvolume, presettle: handleDeimalplace(quote.presettle), putoptionpremiums: handleDeimalplace(quote.putoptionpremiums), putoptionpremiums2: handleDeimalplace(quote.putoptionpremiums2), putoptionpremiums3: handleDeimalplace(quote.putoptionpremiums3), putoptionpremiums4: handleDeimalplace(quote.putoptionpremiums4), putoptionpremiums5: handleDeimalplace(quote.putoptionpremiums5), settle: handleDeimalplace(quote.settle), totalturnover: handleDeimalplace(quote.totalturnover), totalvolume: quote.totalvolume, } } // 接收行情推送通知 const quotePushNotify = eventBus.$on('QuotePushNotify', (res) => { const data = res as Proto.Quote[] data.forEach((item) => { const quote = handleQuotation(item) if (!state.loading) { updateQuotation(quote) } // 触发行情监听事件 for (const e of quoteWatchMap.values()) { if (e.keys.includes(quote.goodscode ?? '')) { e.callback(quote) } } }) }) return { ...toRefs(state), marketGoodsList, selectedGoods, onDataCompleted, setMarketId, getQuoteDay, getQuotePrice, getGoodsList, getGoodsQuote, getGoodsName, getGoodsMarket, getGoodsListByMarketId, getGoodsListByTradeMode, updateQuotation, quoteWatch, quotePushNotify, getGoods } })