futures.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import { shallowRef, reactive, computed, toRefs, onUnmounted } from 'vue'
  2. import { v4 } from 'uuid'
  3. import { timerTask } from '@/utils/timer'
  4. import { handlePriceColor } from '@/filters'
  5. import { queryMemberGoodsLimitConfig } from '@/services/api/common'
  6. import { getTodayAccountConfigInfo } from '@/services/api/account'
  7. import { queryErmcpGoods, queryQuoteDay } from '@/services/api/goods'
  8. import { wordArrayToUint8Array } from '@/services/websocket/package/crypto'
  9. import { decodeProto } from '@/services/websocket/package/package50/proto'
  10. import { defineStore } from '../store'
  11. import { useAccountStore } from './account'
  12. import CryptoJS from 'crypto-js'
  13. import eventBus from '@/services/bus'
  14. import moment from 'moment'
  15. /**
  16. * 期货存储对象
  17. * @returns
  18. */
  19. export const useFuturesStore = defineStore(() => {
  20. // 行情监听集合
  21. const quoteWatchMap = new Map<string, { keys: string[]; callback: (value: Partial<Model.QuoteDayRsp>) => void; }>()
  22. // 当前市场ID
  23. const marketIds = shallowRef<number[]>([])
  24. // 请求商品列表成功后的回调集合
  25. // 由于异步请求原因,页面发起行情订阅时商品数据可能还不存在
  26. // 将回调事件收集到合集中,待请求成功后再执行
  27. const dataCompletedCallback = new Set<() => void>()
  28. const state = reactive({
  29. loading: false,
  30. goodsList: <(Model.GoodsRsp & Partial<Model.MemberGoodsLimitConfigRsp>)[]>[], // 商品列表
  31. quotationList: <Model.GoodsQuote[]>[], // 行情列表
  32. selectedGoodsId: <number | undefined>undefined, // 当前选中的商品ID
  33. })
  34. // 指定市场ID的商品列表
  35. const marketGoodsList = computed(() => {
  36. const list = state.quotationList.filter((e) => marketIds.value.includes(e.marketid))
  37. return list.sort((a, b) => a.goodscode.localeCompare(b.goodscode)) // 根据 goodscode 字母升序排序
  38. })
  39. // 当前选中的商品
  40. const selectedGoods = computed(() => state.quotationList.find((e) => e.goodsid === state.selectedGoodsId))
  41. // 设置当前市场ID
  42. const setMarketId = (...values: number[]) => {
  43. marketIds.value = values
  44. const list = marketGoodsList.value
  45. if (list.length) {
  46. // 查找当前选中的商品ID
  47. if (!list.every((e) => e.goodsid === state.selectedGoodsId)) {
  48. state.selectedGoodsId = list[0].goodsid
  49. }
  50. } else {
  51. state.selectedGoodsId = undefined
  52. }
  53. }
  54. const getGoodsListByMarketId = (...marketId: number[]) => {
  55. const list = state.quotationList.filter((e) => marketId.includes(e.marketid))
  56. return list.sort((a, b) => a.goodscode.localeCompare(b.goodscode)) // 根据 goodscode 字母升序排序
  57. }
  58. const getGoodsListByTradeMode = (...tradeMode: number[]) => {
  59. const list = state.quotationList.filter((e) => tradeMode.includes(e.trademode))
  60. return list.sort((a, b) => a.goodscode.localeCompare(b.goodscode)) // 根据 goodscode 字母升序排序
  61. }
  62. // 获取商品列表
  63. const getGoodsList = async () => {
  64. state.loading = true
  65. state.goodsList = []
  66. state.quotationList = []
  67. timerTask.clearTimeout('quoteDay')
  68. const accountStore = useAccountStore()
  69. // 任务 #5511
  70. const { data: accountConfig } = await getTodayAccountConfigInfo()
  71. // 任务 #5197
  72. const { data: limitConfig } = await queryMemberGoodsLimitConfig({
  73. data: {
  74. roletype: 7
  75. }
  76. })
  77. const { data: goodsList } = await queryErmcpGoods()
  78. for (let i = 0; i < goodsList.length; i++) {
  79. const item = goodsList[i]
  80. const limit = limitConfig.find((e) => e.goodsid === item.goodsid)
  81. // 跳过不显示的商品
  82. if (limit?.isnodisplay) {
  83. continue
  84. }
  85. // 更新商品配置
  86. const findAccountConfig = accountConfig.todayAccountMargins.find((e) => e.accountid === accountStore.currentAccountId && e.goodsid === item.goodsid)
  87. const config = findAccountConfig ?? accountConfig.todayAccountMargins.find((e) => e.accountid === 0 && e.goodsid === item.goodsid)
  88. if (config) {
  89. const wordArray = CryptoJS.enc.Base64.parse(config.infocontent) // 解析base64
  90. const uint8Array = wordArrayToUint8Array(wordArray)
  91. const res = await decodeProto<Proto.MarginInfoStruct>('MarginInfoStruct', uint8Array) // proto数据解析
  92. item.marketmarginalgorithm = res.MarginAlgorithm
  93. item.marketmarginvalue = res.MarketMarginValue
  94. }
  95. // 组合商品属性
  96. state.goodsList.push({
  97. ...item,
  98. iscannotbuy: limit?.iscannotbuy ?? 0,
  99. iscannotsell: limit?.iscannotsell ?? 0
  100. })
  101. }
  102. // 待优化
  103. getQuoteDay().then(() => {
  104. dataCompletedCallback.forEach((callback) => callback())
  105. dataCompletedCallback.clear()
  106. }).finally(() => {
  107. state.loading = false
  108. if (!state.selectedGoodsId) {
  109. state.selectedGoodsId = marketGoodsList.value[0]?.goodsid
  110. }
  111. // 每5分钟获取一次盘面
  112. timerTask.setTimeout(() => getQuoteDay(), 5 * 60 * 1000, 'quoteDay')
  113. })
  114. }
  115. // 获取商品盘面信息(待优化)
  116. const getQuoteDay = async () => {
  117. state.loading = true
  118. const values = state.goodsList.map((e) => e.goodscode.toString())
  119. const res = await queryQuoteDay({
  120. data: {
  121. goodsCodes: values.join(',')
  122. }
  123. })
  124. // 只遍历有盘面的商品
  125. // res.data.forEach((item) => {
  126. // updateQuotation(item)
  127. // })
  128. // 遍历所有商品
  129. values.forEach((goodscode) => {
  130. const item = res.data.find((e_1) => e_1.goodscode === goodscode)
  131. updateQuotation(item ?? { goodscode })
  132. })
  133. state.loading = false
  134. }
  135. // 盘面数据加载完成后触发
  136. const onDataCompleted = (callback: () => void) => {
  137. if (state.quotationList.length) {
  138. setTimeout(() => callback(), 0)
  139. } else {
  140. dataCompletedCallback.add(callback)
  141. onUnmounted(() => dataCompletedCallback.delete(callback))
  142. }
  143. }
  144. // 通过 goodscode 获取实时盘面
  145. const getGoodsQuote = (code: string | number) => {
  146. return computed(() => state.quotationList.find((e) => e.goodscode === code || e.goodsid === code))
  147. }
  148. // 通过 goodscode 获取实时行情报价
  149. const getQuotePrice = (goodsCode: string) => {
  150. return computed(() => {
  151. const quote = getGoodsQuote(goodsCode)
  152. const { last = 0, presettle = 0, preclose = 0 } = quote.value ?? {}
  153. if (last != 0.0) {
  154. return last
  155. } else if (presettle != 0.0) {
  156. return presettle
  157. } else if (preclose != 0.0) {
  158. return preclose
  159. } else {
  160. return 0.0
  161. }
  162. })
  163. }
  164. // 获取商品名称
  165. const getGoodsName = (code: string | number) => {
  166. const goods = getGoods(code)
  167. return goods?.goodsname ?? ''
  168. }
  169. // 获取对应的商品
  170. const getGoods = (code: string | number) => {
  171. const goods = state.goodsList.find((e) => e.goodscode === code || e.goodsid === code)
  172. return goods
  173. }
  174. // 获取商品市场ID
  175. const getGoodsMarket = (code: string | number) => {
  176. const quote = state.goodsList.find((e) => e.goodscode === code || e.goodsid === code)
  177. return quote?.marketid ?? 0
  178. }
  179. // 监听行情推送
  180. const quoteWatch = (goodsCodes: string | string[], callback: (value: Partial<Model.QuoteDayRsp>) => void) => {
  181. const uuid = v4()
  182. quoteWatchMap.set(uuid, {
  183. keys: Array.isArray(goodsCodes) ? goodsCodes : [goodsCodes],
  184. callback
  185. })
  186. const append = (...goodsCodes: string[]) => {
  187. const value = quoteWatchMap.get(uuid)
  188. value?.keys.push(...goodsCodes)
  189. }
  190. const stop = () => {
  191. quoteWatchMap.delete(uuid)
  192. }
  193. // 页面离开时停止监听事件,防止事件重复触发
  194. onUnmounted(() => stop())
  195. return {
  196. uuid,
  197. append,
  198. stop
  199. }
  200. }
  201. // 更新行情数据
  202. const updateQuotation = (quote: Partial<Model.QuoteDayRsp>) => {
  203. // 查找对应的商品行情
  204. const item: Model.GoodsQuote = state.quotationList.find((e) => e.goodscode === quote.goodscode) ?? {
  205. goodsid: 0,
  206. goodscode: quote.goodscode ?? '',
  207. goodsname: '',
  208. goodsgroupid: 0,
  209. goodunitid: 0,
  210. marketid: 0,
  211. trademode: 0,
  212. agreeunit: 0,
  213. decimalplace: 0,
  214. decimalvalue: 0,
  215. quoteminunit: 0,
  216. quotegear: 0,
  217. last: quote.last ?? 0,
  218. lasttime: quote.lasttime ?? '',
  219. bid: quote.bid ?? 0,
  220. bid2: quote.bid2 ?? 0,
  221. bid3: quote.bid3 ?? 0,
  222. bid4: quote.bid4 ?? 0,
  223. bid5: quote.bid5 ?? 0,
  224. bidvolume: quote.bidvolume ?? 0,
  225. bidvolume2: quote.bidvolume2 ?? 0,
  226. bidvolume3: quote.bidvolume3 ?? 0,
  227. bidvolume4: quote.bidvolume4 ?? 0,
  228. bidvolume5: quote.bidvolume5 ?? 0,
  229. ask: quote.ask ?? 0,
  230. ask2: quote.ask2 ?? 0,
  231. ask3: quote.ask3 ?? 0,
  232. ask4: quote.ask4 ?? 0,
  233. ask5: quote.ask5 ?? 0,
  234. askvolume: quote.askvolume ?? 0,
  235. askvolume2: quote.askvolume2 ?? 0,
  236. askvolume3: quote.askvolume3 ?? 0,
  237. askvolume4: quote.askvolume4 ?? 0,
  238. askvolume5: quote.askvolume5 ?? 0,
  239. lastvolume: quote.lastvolume ?? 0,
  240. lastturnover: quote.Lastturnover ?? 0,
  241. presettle: quote.presettle ?? 0,
  242. preclose: quote.preclose ?? 0,
  243. opened: quote.opened ?? 0,
  244. highest: quote.highest ?? 0,
  245. lowest: quote.lowest ?? 0,
  246. limitup: quote.limitup ?? 0,
  247. limitdown: quote.limitdown ?? 0,
  248. averageprice: quote.averageprice ?? 0,
  249. totalvolume: quote.totalvolume ?? 0,
  250. totalturnover: quote.totalturnover ?? 0,
  251. holdvolume: quote.holdvolume ?? 0,
  252. provideraccountid: 0,
  253. currencyid: 1,
  254. provideruserid: 0,
  255. marketmarginalgorithm: 0,
  256. marketmarginvalue: 0,
  257. goodstradetype: 0,
  258. maxspread: 0,
  259. minspread: 0,
  260. goodsorder: '',
  261. tradeproperty: 0,
  262. rise: 0,
  263. change: 0,
  264. amplitude: 0,
  265. iscannotbuy: 0,
  266. iscannotsell: 0,
  267. bidColor: '',
  268. bid2Color: '',
  269. bid3Color: '',
  270. bid4Color: '',
  271. bid5Color: '',
  272. askColor: '',
  273. ask2Color: '',
  274. ask3Color: '',
  275. ask4Color: '',
  276. ask5Color: '',
  277. lastColor: '',
  278. averagepriceColor: '',
  279. openedColor: '',
  280. highestColor: '',
  281. lowestColor: '',
  282. }
  283. if (item.goodsid) {
  284. // 更新对象属性
  285. Object.entries(quote).forEach(([key, value]) => {
  286. if (value !== undefined) {
  287. type TKey = keyof Model.GoodsQuote
  288. (<K extends TKey>(prop: K, value: Model.GoodsQuote[K]) => {
  289. item[prop] = value
  290. })(key as TKey, value)
  291. }
  292. })
  293. } else {
  294. const goods = state.goodsList.find((e) => e.goodscode === quote.goodscode)
  295. if (goods) {
  296. ({
  297. goodsid: item.goodsid,
  298. goodsname: item.goodsname,
  299. goodsgroupid: item.goodsgroupid,
  300. goodunitid: item.goodunitid,
  301. marketid: item.marketid,
  302. trademode: item.trademode,
  303. agreeunit: item.agreeunit,
  304. decimalplace: item.decimalplace,
  305. quoteminunit: item.quoteminunit,
  306. quotegear: item.quotegear,
  307. marketmarginalgorithm: item.marketmarginalgorithm,
  308. marketmarginvalue: item.marketmarginvalue,
  309. maxspread: item.maxspread,
  310. minspread: item.minspread,
  311. goodsorder: item.goodsorder,
  312. tradeproperty: item.tradeproperty,
  313. provideraccountid: item.provideraccountid,
  314. provideruserid: item.provideruserid,
  315. goodstradetype: item.goodstradetype,
  316. currencyid: item.currencyid
  317. } = goods)
  318. item.iscannotbuy = goods.iscannotbuy ?? 0
  319. item.iscannotsell = goods.iscannotsell ?? 0
  320. // 向列表添加新数据
  321. state.quotationList.push(item)
  322. }
  323. }
  324. const price = item.trademode === 52 ? Math.max(item.last, item.ask) : item.last // 任务 #5677
  325. item.opened = item.opened || item.last // 没有开盘价默认取最新价
  326. item.highest = item.highest || price // 没有最高价默认取最新价
  327. item.lowest = item.lowest || item.last // 没有最低价价默认取最新价
  328. // 处理最高最低价
  329. if (item.last) {
  330. if (price > item.highest) {
  331. item.highest = price
  332. }
  333. if (item.last < item.lowest) {
  334. item.lowest = item.last
  335. }
  336. }
  337. item.averageprice = item.totalvolume ? item.totalturnover / (item.totalvolume * item.agreeunit) : 0 // 计算均价
  338. item.rise = item.last ? item.last - item.presettle : 0 // 涨跌额/涨跌: 最新价 - 昨结价
  339. item.change = item.presettle ? item.rise / item.presettle : 0 // 涨跌幅/幅度: (最新价 - 昨结价) / 昨结价
  340. item.amplitude = item.presettle ? (item.highest - item.lowest) / item.presettle : 0 // 振幅: (最高价 - 最低价 ) / 最新价
  341. // 计算小数单位值
  342. item.decimalvalue = item.quoteminunit * Math.pow(10, item.decimalplace * -1)
  343. // 处理行情价格颜色
  344. const handleColor = (value: number | string) => handlePriceColor(Number(value), item.presettle)
  345. item.bidColor = handleColor(item.bid)
  346. item.bid2Color = handleColor(item.bid2)
  347. item.bid3Color = handleColor(item.bid3)
  348. item.bid4Color = handleColor(item.bid4)
  349. item.bid5Color = handleColor(item.bid5)
  350. item.askColor = handleColor(item.ask)
  351. item.ask2Color = handleColor(item.ask2)
  352. item.ask3Color = handleColor(item.ask3)
  353. item.ask4Color = handleColor(item.ask4)
  354. item.ask5Color = handleColor(item.ask5)
  355. item.lastColor = handleColor(item.last)
  356. item.averagepriceColor = handleColor(item.averageprice.toFixed(item.decimalplace))
  357. item.openedColor = handleColor(item.opened)
  358. item.highestColor = handleColor(item.highest)
  359. item.lowestColor = handleColor(item.lowest)
  360. }
  361. // 处理行情数据
  362. const handleQuotation = (quote: Proto.Quote): Partial<Model.QuoteDayRsp> => {
  363. const goods = state.goodsList.find((e) => e.goodscode.toUpperCase() === quote.goodscode?.toUpperCase())
  364. // 处理报价小数位
  365. const handleDeimalplace = (value?: number) => {
  366. if (goods && value) {
  367. const decimal = Math.pow(10, goods.decimalplace)
  368. return value / decimal
  369. }
  370. return value
  371. }
  372. return {
  373. goodscode: quote.goodscode,
  374. last: handleDeimalplace(quote.last),
  375. lasttime: (quote.date && quote.time) ? moment(quote.date + quote.time, 'YYYYMMDDHHmmss').toISOString(true) : undefined,
  376. ask: handleDeimalplace(quote.ask),
  377. ask2: handleDeimalplace(quote.ask2),
  378. ask3: handleDeimalplace(quote.ask3),
  379. ask4: handleDeimalplace(quote.ask4),
  380. ask5: handleDeimalplace(quote.ask5),
  381. askvolume: quote.askvolume,
  382. askvolume2: quote.askvolume2,
  383. askvolume3: quote.askvolume3,
  384. askvolume4: quote.askvolume4,
  385. askvolume5: quote.askvolume5,
  386. bid: handleDeimalplace(quote.bid),
  387. bid2: handleDeimalplace(quote.bid2),
  388. bid3: handleDeimalplace(quote.bid3),
  389. bid4: handleDeimalplace(quote.bid4),
  390. bid5: handleDeimalplace(quote.bid5),
  391. bidvolume: quote.bidvolume,
  392. bidvolume2: quote.bidvolume2,
  393. bidvolume3: quote.bidvolume3,
  394. bidvolume4: quote.bidvolume4,
  395. bidvolume5: quote.bidvolume5,
  396. calloptionpremiums: quote.calloptionpremiums,
  397. calloptionpremiums2: quote.calloptionpremiums2,
  398. calloptionpremiums3: quote.calloptionpremiums3,
  399. calloptionpremiums4: quote.calloptionpremiums4,
  400. calloptionpremiums5: quote.calloptionpremiums5,
  401. exchangecode: quote.exchangecode,
  402. exchangedate: quote.exchangedate,
  403. highest: handleDeimalplace(quote.highest),
  404. holdvolume: quote.holdvolume,
  405. inventory: quote.inventory,
  406. lastvolume: quote.lastvolume,
  407. Lastturnover: handleDeimalplace(quote.lastturnover),
  408. limitdown: handleDeimalplace(quote.limitlow),
  409. limitup: handleDeimalplace(quote.limithigh),
  410. lowest: handleDeimalplace(quote.lowest),
  411. opened: handleDeimalplace(quote.opened),
  412. preclose: handleDeimalplace(quote.preclose),
  413. preholdvolume: quote.preholdvolume,
  414. presettle: handleDeimalplace(quote.presettle),
  415. putoptionpremiums: handleDeimalplace(quote.putoptionpremiums),
  416. putoptionpremiums2: handleDeimalplace(quote.putoptionpremiums2),
  417. putoptionpremiums3: handleDeimalplace(quote.putoptionpremiums3),
  418. putoptionpremiums4: handleDeimalplace(quote.putoptionpremiums4),
  419. putoptionpremiums5: handleDeimalplace(quote.putoptionpremiums5),
  420. settle: handleDeimalplace(quote.settle),
  421. totalturnover: handleDeimalplace(quote.totalturnover),
  422. totalvolume: quote.totalvolume,
  423. }
  424. }
  425. // 接收行情推送通知
  426. const quotePushNotify = eventBus.$on('QuotePushNotify', (res) => {
  427. const data = res as Proto.Quote[]
  428. data.forEach((item) => {
  429. const quote = handleQuotation(item)
  430. if (!state.loading) {
  431. updateQuotation(quote)
  432. }
  433. // 触发行情监听事件
  434. for (const e of quoteWatchMap.values()) {
  435. if (e.keys.includes(quote.goodscode ?? '')) {
  436. e.callback(quote)
  437. }
  438. }
  439. })
  440. })
  441. return {
  442. ...toRefs(state),
  443. marketGoodsList,
  444. selectedGoods,
  445. onDataCompleted,
  446. setMarketId,
  447. getQuoteDay,
  448. getQuotePrice,
  449. getGoodsList,
  450. getGoodsQuote,
  451. getGoodsName,
  452. getGoodsMarket,
  453. getGoodsListByMarketId,
  454. getGoodsListByTradeMode,
  455. updateQuotation,
  456. quoteWatch,
  457. quotePushNotify,
  458. getGoods
  459. }
  460. })