import { shallowRef, reactive, computed, toRefs, onUnmounted } from 'vue' import { v4 } from 'uuid' import { isMatch } from 'lodash' import { timerTask } from '@/utils/timer' import { handlePriceColor } from '@/filters' import { BuyOrSell } from '@/constants/order' import { queryMemberGoodsLimitConfig } from '@/services/api/common' 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' import { i18n } from './language' /** * 期货存储对象 * @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 buyOrSell: 0, }) // 指定市场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 字母升序排序 } // 根据商品 ID 查找对应的商品信息 const getGoodsListByGoodsID = (goodsid: number[]) => { const list = state.quotationList.filter((e) => goodsid.includes(e.goodsid)) 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 { currentAccountId, currentAccountConfig } = useAccountStore() const { todayAccountMargins = [], todayAccountTradeRules = [], todayAccountTradefees = [] } = currentAccountConfig ?? {} // 任务 #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 findMargin = todayAccountMargins.find((e) => e.accountid === currentAccountId && e.goodsid === item.goodsid) const margin = findMargin ?? todayAccountMargins.find((e) => e.accountid === 0 && e.goodsid === item.goodsid) if (margin) { const marginWordArray = CryptoJS.enc.Base64.parse(margin.infocontent) // 解析base64 const marginUint8Array = wordArrayToUint8Array(marginWordArray) const marginData = await decodeProto('MarginInfoStruct', marginUint8Array) // proto数据解析 item.marketmarginalgorithm = marginData.MarginAlgorithm item.marketmarginvalue = marginData.MarketMarginValue } const findRule = todayAccountTradeRules.find((e) => e.accountid === currentAccountId && e.goodsid === item.goodsid) const rule = findRule ?? todayAccountTradeRules.find((e) => e.accountid === 0 && e.goodsid === item.goodsid) if (rule) { const ruleWordArray = CryptoJS.enc.Base64.parse(rule.infocontent) // 解析base64 const ruleUint8Array = wordArrayToUint8Array(ruleWordArray) const ruleData = await decodeProto('TradeRuleInfoStruct', ruleUint8Array) // proto数据解析 item.traderules = ruleData.TradeRules } else { item.traderules = [] } // 交易费用 const findfee = todayAccountTradefees.find((e) => e.accountid === currentAccountId && e.goodsid === item.goodsid) const fee = findfee ?? todayAccountTradefees.find((e) => e.accountid === 0 && e.goodsid === item.goodsid && e.tradetype === 1) if (fee) { const ruleWordArray = CryptoJS.enc.Base64.parse(fee.infocontent) // 解析base64 const ruleUint8Array = wordArrayToUint8Array(ruleWordArray) const ruleData = await decodeProto('TradeFeeInfoStruct', ruleUint8Array) // proto数据解析 item.tradefees = ruleData.TradeFees } else { item.tradefees = [] } // 组合商品属性 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)) } const getQuoteItem = (query: Partial) => { return state.quotationList.find(item => isMatch(item, query)) } // 获取费用值 const getFeeValue = (goodsItem?: Model.GoodsQuote, feeId = 0) => { const { tradefees = [] } = goodsItem ?? {} const { FeeAlgorithm = 0, ExchangeValue = 0, MemberDefaultValue = 0 } = tradefees.find((e) => e.FeeID === feeId) ?? {} return { FeeAlgorithm, feeValue: ExchangeValue + MemberDefaultValue } } // 通过 goodscode 获取实时行情报价 const getQuotePrice = (goodscode: string) => { const { last = 0, presettle = 0, preclose = 0 } = getQuoteItem({ goodscode }) ?? {} 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 } // 获取商品国际化名称 const getI18nGoodsName = (code: string | number) => { const item = getGoods(code) switch (i18n.global.locale) { case 'zh-CN': return item?.goodsname ?? '' case 'en-US': return item?.goodsnameen ?? '' case 'zh-TW': return item?.goodsnametw ?? '' case 'vi': return item?.goodsnamevi ?? '' default: return item?.goodsnameth ?? '' } } // 获取商品市场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, goodsgroupname: '', 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, goodscurrencyid: 0, currencyid: 0, 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: '', pictureurl: '', thumurls: '', tpslflag: 0, tpslforceflag: 0, slratiodefault: 0, slratiodown: 0, slratioup: 1, tpratiodefault: 0, tpratiodown: 0, tpratioup: 1, basecurrencycode: '', quotecurrencycode: '', buyslpoint: 0, buytppoint: 0, sellslpoint: 0, selltppoint: 0, riskcontrolmode: 1, basedecimalplace: 0, quotedecimalplace: 0, traderules: [], tradefees: [] } 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, goodsgroupname: item.goodsgroupname, 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, goodscurrencyid: item.goodscurrencyid, currencyid: item.currencyid, pictureurl: item.pictureurl, traderules: item.traderules, tradefees: item.tradefees, thumurls: item.thumurls, tpslflag: item.tpslflag, tpslforceflag: item.tpslforceflag, slratiodefault: item.slratiodefault, slratiodown: item.slratiodown, slratioup: item.slratioup, tpratiodefault: item.tpratiodefault, tpratiodown: item.tpratiodown, tpratioup: item.tpratioup, basecurrencycode: item.basecurrencycode, quotecurrencycode: item.quotecurrencycode, buyslpoint: item.buyslpoint, buytppoint: item.buytppoint, sellslpoint: item.sellslpoint, selltppoint: item.selltppoint, riskcontrolmode: item.riskcontrolmode, basedecimalplace: item.basedecimalplace, quotedecimalplace: item.quotedecimalplace } = goods) item.iscannotbuy = goods.iscannotbuy ?? 0 item.iscannotsell = goods.iscannotsell ?? 0 // 向列表添加新数据 state.quotationList.push(item) } } // 任务 #5677 任务 #6268 const hprice = (item.trademode === 52 || item.trademode === 10) ? item.ask : item.last const lprice = (item.trademode === 52 || item.trademode === 10) ? item.bid : item.last item.opened = item.opened || item.last // 没有开盘价默认取最新价 item.highest = item.highest || hprice // 没有最高价默认取最新价 item.lowest = item.lowest || lprice // 没有最低价价默认取最新价 // 处理最高最低价 if (item.last) { if (hprice > item.highest) { item.highest = hprice } if (lprice < item.lowest) { item.lowest = lprice } } 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) if (item?.trademode === 80 || item?.trademode === 81) { // 处理数量小数位 const handleDecimalPlace = (value: number) => { if (value) { const decimal = Math.pow(10, item.basedecimalplace) return value / decimal } return value } item.askvolume = handleDecimalPlace(item.askvolume) item.askvolume2 = handleDecimalPlace(item.askvolume2) item.askvolume3 = handleDecimalPlace(item.askvolume3) item.askvolume4 = handleDecimalPlace(item.askvolume4) item.askvolume5 = handleDecimalPlace(item.askvolume5) item.bidvolume = handleDecimalPlace(item.bidvolume) item.bidvolume2 = handleDecimalPlace(item.bidvolume2) item.bidvolume3 = handleDecimalPlace(item.bidvolume3) item.bidvolume4 = handleDecimalPlace(item.bidvolume4) item.bidvolume5 = handleDecimalPlace(item.bidvolume5) } } // 处理行情数据 const handleQuotation = (quote: Proto.Quote): Partial => { const goods = state.goodsList.find((e) => e.goodscode.toUpperCase() === quote.goodscode?.toUpperCase()) // 处理报价小数位 const handleDecimalPlace = (value?: number) => { if (goods && value) { const decimal = Math.pow(10, goods.decimalplace) return value / decimal } return value } return { goodscode: quote.goodscode, last: handleDecimalPlace(quote.last), lasttime: (quote.date && quote.time) ? moment(quote.date + quote.time, 'YYYYMMDDHHmmss').toISOString(true) : undefined, ask: handleDecimalPlace(quote.ask), ask2: handleDecimalPlace(quote.ask2), ask3: handleDecimalPlace(quote.ask3), ask4: handleDecimalPlace(quote.ask4), ask5: handleDecimalPlace(quote.ask5), askvolume: quote.askvolume, askvolume2: quote.askvolume2, askvolume3: quote.askvolume3, askvolume4: quote.askvolume4, askvolume5: quote.askvolume5, bid: handleDecimalPlace(quote.bid), bid2: handleDecimalPlace(quote.bid2), bid3: handleDecimalPlace(quote.bid3), bid4: handleDecimalPlace(quote.bid4), bid5: handleDecimalPlace(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: handleDecimalPlace(quote.highest), holdvolume: quote.holdvolume, inventory: quote.inventory, lastvolume: quote.lastvolume, Lastturnover: handleDecimalPlace(quote.lastturnover), limitdown: handleDecimalPlace(quote.limitlow), limitup: handleDecimalPlace(quote.limithigh), lowest: handleDecimalPlace(quote.lowest), opened: handleDecimalPlace(quote.opened), preclose: handleDecimalPlace(quote.preclose), preholdvolume: quote.preholdvolume, presettle: handleDecimalPlace(quote.presettle), putoptionpremiums: handleDecimalPlace(quote.putoptionpremiums), putoptionpremiums2: handleDecimalPlace(quote.putoptionpremiums2), putoptionpremiums3: handleDecimalPlace(quote.putoptionpremiums3), putoptionpremiums4: handleDecimalPlace(quote.putoptionpremiums4), putoptionpremiums5: handleDecimalPlace(quote.putoptionpremiums5), settle: handleDecimalPlace(quote.settle), totalturnover: handleDecimalPlace(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) } } }) }) // 商品显示 const goodsdisplay = (code: string | number) => { const item = getGoods(code) if (item) { return item.trademode != 10 ? item.goodscode + '/' + item.goodsname : item.goodscode } return '--' } // 计算浮动盈亏 const calcFloatingPL = (quote: Model.GoodsQuote | undefined, buyorsell: number, quantity: number, amount: number, exchangerate = 1) => { const { trademode = 0, last = 0, presettle = 0, ask = 0, bid = 0, agreeunit = 1 } = quote ?? {} const isSupportedMode = [10, 52, 80].includes(trademode) const lastPrice = (isSupportedMode && (buyorsell === BuyOrSell.Buy ? bid : ask)) || last || presettle const result = { lastPrice, // 最新价 marketValue: 0, // 市值 profitLoss: 0, // 浮动盈亏 lastPriceClass: '', // 最新价样式 profitLossClass: '' // 浮动盈亏样式 } if (lastPrice) { // 计算市值 = 现价 * 数量 * 合约单位 * 汇率 result.marketValue = lastPrice * quantity * agreeunit * exchangerate // 计算浮动盈亏 任务 #5600 #6013 result.profitLoss = (result.marketValue - amount) * (buyorsell === BuyOrSell.Buy ? 1 : -1) result.lastPriceClass = handlePriceColor(lastPrice, presettle) result.profitLossClass = handlePriceColor(result.profitLoss) } return result } return { ...toRefs(state), marketGoodsList, selectedGoods, onDataCompleted, setMarketId, getQuoteDay, getQuotePrice, getGoodsList, getGoodsQuote, getGoodsName, getGoodsMarket, getGoodsListByMarketId, getGoodsListByTradeMode, getGoodsListByGoodsID, updateQuotation, quoteWatch, quotePushNotify, getGoods, goodsdisplay, getQuoteItem, getFeeValue, calcFloatingPL, getI18nGoodsName } })