history.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  1. package quote
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "mtp2_if/global/app"
  7. "mtp2_if/global/e"
  8. "mtp2_if/logger"
  9. "mtp2_if/models"
  10. "mtp2_if/mtpcache"
  11. "mtp2_if/utils"
  12. "net/http"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/gin-gonic/gin"
  18. "github.com/shopspring/decimal"
  19. )
  20. // HistoryData 历史数据
  21. type HistoryData struct {
  22. Opened float64 `json:"o"` // 开盘价
  23. Highest float64 `json:"h"` // 最高价
  24. Lowest float64 `json:"l"` // 最低价
  25. Closed float64 `json:"c"` // 收盘价
  26. TotleVolume float64 `json:"tv"` // 总量(成交量)
  27. TotleTurnover float64 `json:"tt"` // 总金额
  28. HoldVolume float64 `json:"hv"` // 持仓量
  29. Settle float64 `json:"s"` // 结算价,日线周期(包括)以上才有
  30. TimeStamp time.Time `json:"ts"` // 时间
  31. IsFill bool `json:"f"` // 是否补充数据
  32. }
  33. // QueryHistoryDatasReq 查询行情历史数据请求参数
  34. type QueryHistoryDatasReq struct {
  35. CycleType int `form:"cycleType" binding:"required"`
  36. GoodsCode string `form:"goodsCode" binding:"required"`
  37. StartTime string `form:"startTime"`
  38. EndTime string `form:"endTime"`
  39. Count int `form:"count"`
  40. IsAsc bool `form:"isAsc"`
  41. }
  42. // QueryHistoryDatas 查询行情历史数据
  43. // @Summary 查询行情历史数据
  44. // @Produce json
  45. // @Security ApiKeyAuth
  46. // @Param cycleType query int true "周期类型, 0-秒 1: 1分钟 2: 5分钟 3: 30分钟 4: 60分钟 120: 2小时 240: 4小时 11: 日线 12:周线 13:月线 14:年线"
  47. // @Param goodsCode query string true "商品代码"
  48. // @Param startTime query string false "开始时间,格式:yyyy-MM-dd HH:mm:ss"
  49. // @Param endTime query string false "结束时间,格式:yyyy-MM-dd HH:mm:ss"
  50. // @Param count query int false "条数"
  51. // @Param isAsc query bool false "是否按时间顺序排序(默认为时间倒序排序)"
  52. // @Success 200 {object} HistoryData
  53. // @Failure 500 {object} app.Response
  54. // @Router /Quote/QueryHistoryDatas [get]
  55. // @Tags 行情服务
  56. func QueryHistoryDatas(c *gin.Context) {
  57. appG := app.Gin{C: c}
  58. // 获取请求参数
  59. var req QueryHistoryDatasReq
  60. if err := appG.C.ShouldBindQuery(&req); err != nil {
  61. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  62. appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
  63. return
  64. }
  65. // 转换时间
  66. timeFormat := "2006-01-02 15:04:05" // go中的时间格式化必须是这个时间
  67. var startTime *time.Time
  68. if len(req.StartTime) > 0 {
  69. st, err := time.ParseInLocation(timeFormat, req.StartTime, time.Local)
  70. if err != nil {
  71. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  72. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil)
  73. return
  74. }
  75. startTime = &st
  76. }
  77. var endTime *time.Time
  78. if len(req.EndTime) > 0 {
  79. et, err := time.ParseInLocation(timeFormat, req.EndTime, time.Local)
  80. if err != nil {
  81. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  82. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil)
  83. return
  84. }
  85. endTime = &et
  86. }
  87. // 查询数据
  88. cycleDatas, err := models.GetHistoryCycleDatas(models.CycleType(req.CycleType), req.GoodsCode, startTime, endTime, req.Count, req.IsAsc)
  89. if err != nil {
  90. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  91. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  92. return
  93. }
  94. // 获取目标商品报价小数位
  95. var dcplace int = 0
  96. if len(cycleDatas) > 0 || req.CycleType >= 11 {
  97. dcplace = mtpcache.GetGoodsDecimalplace(req.GoodsCode)
  98. }
  99. var tPlace int = 0
  100. var jPlace int32 = 2
  101. if goods, has := mtpcache.GetGoods(req.GoodsCode); has {
  102. s := goods.MARKETID[0:2]
  103. if s == "80" || s == "81" {
  104. if enumitems, err := models.GetEnumDicItem("digitalcurrency", int(goods.GOODSCURRENCYID)); err == nil && enumitems != nil {
  105. if len(enumitems) > 0 {
  106. tPlace, _ = strconv.Atoi(enumitems[0].Param1)
  107. }
  108. }
  109. if enumitems, err := models.GetEnumDicItem("digitalcurrency", int(goods.CURRENCYID)); err == nil && enumitems != nil {
  110. if len(enumitems) > 0 {
  111. j, _ := strconv.Atoi(enumitems[0].Param1)
  112. jPlace = int32(j)
  113. }
  114. }
  115. }
  116. }
  117. // 计算最终价格
  118. rst := make([]HistoryData, 0)
  119. for _, v := range cycleDatas {
  120. tt, _ := decimal.NewFromFloat(utils.IntToFloat64(v.TT, dcplace)).Round(jPlace).Float64() // 按行情服务要求,金额类需要先除以商品报价小数位再按2位小数四舍五入
  121. historyData := HistoryData{
  122. Opened: utils.IntToFloat64(v.Open, dcplace),
  123. Highest: utils.IntToFloat64(v.High, dcplace),
  124. Lowest: utils.IntToFloat64(v.Low, dcplace),
  125. Closed: utils.IntToFloat64(v.Close, dcplace),
  126. TotleVolume: utils.IntToFloat64(v.TV, tPlace),
  127. TotleTurnover: tt,
  128. HoldVolume: utils.IntToFloat64(v.HV, tPlace),
  129. Settle: utils.IntToFloat64(v.SP, dcplace),
  130. TimeStamp: time.Unix(int64(v.ST), 0),
  131. }
  132. rst = append(rst, historyData)
  133. }
  134. // 包括日线以上的周期,如果没有带当前周期则通过盘面数据生成当前周期数据
  135. if req.CycleType >= 11 {
  136. logger.GetLogger().Debugf("[checkTiks]%v---------开始确定是否要补K线-----------", req.GoodsCode)
  137. // 获取商品信息
  138. goods, err := models.GetGoodsByGoodsCode(req.GoodsCode)
  139. if goods == nil {
  140. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  141. appG.Response(http.StatusBadRequest, e.ERROR_GET_GOODS_FAILED, nil)
  142. return
  143. }
  144. // 获取市场
  145. market, err := models.GetMarketByGoodsCode(req.GoodsCode)
  146. if err != nil {
  147. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  148. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  149. return
  150. }
  151. if market == nil {
  152. logger.GetLogger().Error("QueryHistoryDatas failed: not found goodscode")
  153. appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKET_FAILED, nil)
  154. return
  155. }
  156. logger.GetLogger().Debugf("[checkTiks] goodscode[%v] trademode[%v]", req.GoodsCode, market.TRADEMODE)
  157. // 获取目标品种交易日
  158. // FIXME: - 由于mtp2.0目前未同步外部交易所品种的当前交易日,
  159. // 故通道交易的品种目前只能在交易系统的外部市场中获
  160. // 取统一的交易日,后期应要求服务端同步外部数据
  161. // 2021.06.30 业务更新:15、97和99市场的行情交易日要从商品盘面中获取
  162. quoteTradeDate := ""
  163. if market.TRADEMODE == 15 || market.TRADEMODE == 97 || market.TRADEMODE == 99 || market.TRADEMODE == 80 || market.TRADEMODE == 81 {
  164. if quoteDays, err := models.GetQuoteDays(req.GoodsCode); err == nil {
  165. if len(quoteDays) > 0 {
  166. quoteTradeDate = time.Unix(quoteDays[0].Exchangedate, 0).Format("20060102")
  167. logger.GetLogger().Debugf("[checkTiks] get quoteTradeDate[%v]", quoteTradeDate)
  168. }
  169. }
  170. } else {
  171. if marketRun, err := models.GetMarketRun(int(market.MARKETID)); err == nil || marketRun == nil {
  172. quoteTradeDate = marketRun.Tradedate2
  173. logger.GetLogger().Debugf("[checkTiks] get quoteTradeDate[%v] = marketRun.Tradedate2", quoteTradeDate)
  174. }
  175. }
  176. if quoteTradeDate != "" {
  177. if tradeDate, err := time.ParseInLocation("20060102", quoteTradeDate, time.Local); err == nil {
  178. var fisrtDate time.Time
  179. switch req.CycleType {
  180. case 12: // 周
  181. fisrtDate = getFirstDateOfWeek(tradeDate)
  182. case 13: // 月
  183. fisrtDate = getFirstDateOfMonth(tradeDate)
  184. case 14: // 年
  185. fisrtDate = getFirstDateOfYear(tradeDate)
  186. default: // 日
  187. fisrtDate = tradeDate
  188. }
  189. logger.GetLogger().Debugf("[checkTiks] get firstDate[%v] CycleType[%v]", fisrtDate, req.CycleType)
  190. // 获取盘面数据
  191. if quoteDays, err := models.GetQuoteDays(req.GoodsCode); err == nil && len(quoteDays) > 0 {
  192. v := quoteDays[0]
  193. if len(rst) == 0 {
  194. logger.GetLogger().Debugf("[checkTiks] no tiks, add a tik:%v", v)
  195. tt, _ := decimal.NewFromFloat(utils.IntToFloat64(int(v.Totalturnover), dcplace)).Round(jPlace).Float64() // 按行情服务要求,金额类需要先除以商品报价小数位再按2位小数四舍五入
  196. // 历史数据当前没数据则直接加当前盘面数据
  197. historyData := HistoryData{
  198. Opened: utils.IntToFloat64(int(v.Opened), dcplace),
  199. Highest: utils.IntToFloat64(int(v.Highest), dcplace),
  200. Lowest: utils.IntToFloat64(int(v.Lowest), dcplace),
  201. Closed: utils.IntToFloat64(int(v.Last), dcplace),
  202. TotleVolume: utils.IntToFloat64(int(v.Totalvolume), tPlace),
  203. TotleTurnover: tt,
  204. HoldVolume: utils.IntToFloat64(int(v.Holdvolume), tPlace),
  205. Settle: utils.IntToFloat64(int(v.Settle), dcplace),
  206. TimeStamp: fisrtDate,
  207. }
  208. if req.IsAsc {
  209. rst = append(rst, historyData)
  210. } else {
  211. // 插入第一条
  212. rear := append([]HistoryData{}, rst[0:]...)
  213. rst = append(append(rst[:0], historyData), rear...)
  214. }
  215. } else {
  216. // 判断最后周期是否已经存在盘面周期
  217. a, b := rst[0], rst[len(rst)-1]
  218. logger.GetLogger().Debugf("[checkTiks] firstTik TimeStamp[%v] lastTik TimeStamp[%v] IsAsc[%v]",
  219. a.TimeStamp, b.TimeStamp, req.IsAsc)
  220. hd := a
  221. if a.TimeStamp.Before(b.TimeStamp) {
  222. // 取最大日期 b在a之后,取b进行比较
  223. hd = b
  224. }
  225. logger.GetLogger().Debugf("[checkTiks] compare hd.TimeStamp[%v] > firstDate[%v]? ",
  226. hd.TimeStamp, fisrtDate)
  227. if hd.TimeStamp.Before(fisrtDate) {
  228. // #3424 当日未开市显示了K线
  229. // 修改:这里增加判断盘面是否有开盘价
  230. if v.Opened > 0 && v.Last > 0 {
  231. tt, _ := decimal.NewFromFloat(utils.IntToFloat64(int(v.Totalturnover), dcplace)).Round(jPlace).Float64() // 按行情服务要求,金额类需要先除以商品报价小数位再按2位小数四舍五入
  232. historyData := HistoryData{
  233. Opened: utils.IntToFloat64(int(v.Opened), dcplace),
  234. Highest: utils.IntToFloat64(int(v.Highest), dcplace),
  235. Lowest: utils.IntToFloat64(int(v.Lowest), dcplace),
  236. Closed: utils.IntToFloat64(int(v.Last), dcplace),
  237. TotleVolume: utils.IntToFloat64(int(v.Totalvolume), tPlace),
  238. TotleTurnover: tt,
  239. HoldVolume: utils.IntToFloat64(int(v.Holdvolume), tPlace),
  240. Settle: utils.IntToFloat64(int(v.Settle), dcplace),
  241. TimeStamp: fisrtDate,
  242. }
  243. logger.GetLogger().Debugf("[checkTiks] 补K线:\n%v", historyData)
  244. if req.IsAsc {
  245. //升序 尾部插入
  246. rst = append(rst, historyData)
  247. } else {
  248. //降序 头部插入
  249. rst = append([]HistoryData{historyData}, rst...)
  250. }
  251. }
  252. }
  253. }
  254. }
  255. }
  256. }
  257. }
  258. // 查询成功
  259. logger.GetLogger().Debugln("QueryHistoryDatas successed, rows:", len(rst))
  260. appG.Response(http.StatusOK, e.SUCCESS, rst)
  261. }
  262. // HistoryTikData Tik数据
  263. type HistoryTikData struct {
  264. TimeStamp time.Time `json:"TS"` // 行情时间文本
  265. PE float64 `json:"PE"` // 现价
  266. Vol int `json:"Vol"` // 现量
  267. TT float64 `json:"TT"` // 现金额
  268. Bid float64 `json:"Bid"` // 买价
  269. BV int `json:"BV"` // 买量
  270. Ask float64 `json:"Ask"` // 卖价
  271. AV int `json:"AV"` // 卖量
  272. HV int `json:"HV"` // 持仓量
  273. HI int `json:"HI"` // 单笔持仓
  274. TDR int `json:"TDR"` // 交易方向,0:买 1:卖
  275. TK int `json:"TK"` // 交易类型
  276. }
  277. // QueryHistoryTikDatasReq 查询行情Tik数据请求参数
  278. type QueryHistoryTikDatasReq struct {
  279. GoodsCode string `form:"goodsCode" binding:"required"`
  280. StartTime string `form:"startTime"`
  281. EndTime string `form:"endTime"`
  282. Count int `form:"count"`
  283. IsAsc bool `form:"isAsc"`
  284. }
  285. // QueryHistoryTikDatas 查询行情Tik数据
  286. // @Summary 查询行情Tik数据
  287. // @Produce json
  288. // @Security ApiKeyAuth
  289. // @Param goodsCode query string true "商品代码"
  290. // @Param startTime query string false "开始时间,格式:yyyy-MM-dd HH:mm:ss"
  291. // @Param endTime query string false "结束时间,格式:yyyy-MM-dd HH:mm:ss"
  292. // @Param count query int false "条数"
  293. // @Param isAsc query bool false "是否按时间顺序排序(默认为时间倒序排序)"
  294. // @Success 200 {object} HistoryTikData
  295. // @Failure 500 {object} app.Response
  296. // @Router /Quote/QueryHistoryTikDatas [get]
  297. // @Tags 行情服务
  298. func QueryHistoryTikDatas(c *gin.Context) {
  299. appG := app.Gin{C: c}
  300. // 获取请求参数
  301. var req QueryHistoryTikDatasReq
  302. if err := appG.C.ShouldBindQuery(&req); err != nil {
  303. logger.GetLogger().Errorf("QueryHistoryTikDatas failed: %s", err.Error())
  304. appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
  305. return
  306. }
  307. // 转换时间
  308. timeFormat := "2006-01-02 15:04:05" // go中的时间格式化必须是这个时间
  309. var startTime *time.Time
  310. if len(req.StartTime) > 0 {
  311. st, err := time.ParseInLocation(timeFormat, req.StartTime, time.Local)
  312. if err != nil {
  313. logger.GetLogger().Errorf("QueryHistoryTikDatas failed: %s", err.Error())
  314. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil)
  315. return
  316. }
  317. startTime = &st
  318. }
  319. var endTime *time.Time
  320. if len(req.EndTime) > 0 {
  321. et, err := time.ParseInLocation(timeFormat, req.EndTime, time.Local)
  322. if err != nil {
  323. logger.GetLogger().Errorf("QueryHistoryTikDatas failed: %s", err.Error())
  324. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil)
  325. return
  326. }
  327. endTime = &et
  328. }
  329. // 查询数据
  330. tikDatas, err := models.GetHistoryTikDatas(req.GoodsCode, startTime, endTime, req.Count, req.IsAsc)
  331. if err != nil {
  332. logger.GetLogger().Errorf("QueryHistoryTikDatas failed: %s", err.Error())
  333. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  334. return
  335. }
  336. // 获取目标商品报价小数位
  337. var dcplace int = 0
  338. if len(tikDatas) > 0 {
  339. dcplace = mtpcache.GetGoodsDecimalplace(req.GoodsCode)
  340. }
  341. // 计算最终价格
  342. rst := make([]HistoryTikData, 0)
  343. for _, v := range tikDatas {
  344. // 获取方向
  345. buyOrSell := 0
  346. if v.TDR == 83 { // (Ascii) B-66 S-83
  347. buyOrSell = 1
  348. }
  349. tt, _ := decimal.NewFromFloat(utils.IntToFloat64(v.TT, dcplace)).Round(2).Float64() // 按行情服务要求,金额类需要先除以商品报价小数位再按2位小数四舍五入
  350. rst = append(rst, HistoryTikData{
  351. TimeStamp: time.Unix(int64(v.AT), 0),
  352. PE: utils.IntToFloat64(v.PE, dcplace),
  353. Vol: v.Vol,
  354. TT: tt,
  355. Bid: utils.IntToFloat64(v.Bid, dcplace),
  356. BV: v.BV,
  357. Ask: utils.IntToFloat64(v.Ask, dcplace),
  358. AV: v.AV,
  359. HV: v.HV,
  360. HI: v.HI,
  361. TDR: buyOrSell,
  362. TK: v.TK,
  363. })
  364. }
  365. // 查询成功
  366. logger.GetLogger().Debugln("QueryHistoryTikDatas successed, rows:", len(rst))
  367. appG.Response(http.StatusOK, e.SUCCESS, rst)
  368. }
  369. // QueryTSDataReq 分时图数据查询请求参数
  370. type QueryTSDataReq struct {
  371. GoodsCode string `form:"goodsCode" binding:"required"` // 商品代码
  372. MarketId int `form:"makretId"` // 关联市场ID
  373. }
  374. // QueryTSDataRsp 分时图数据查询返回模型
  375. type QueryTSDataRsp struct {
  376. GoodsCode string `json:"goodsCode"` // 商品代码
  377. OutGoodsCode string `json:"outGoodsCode"` // 外部商品代码
  378. DecimalPlace int `json:"decimalPlace"` // 小数位
  379. TradeDate string `json:"tradeDate"` // 交易日
  380. StartTime time.Time `json:"startTime"` // 开始时间
  381. EndTime time.Time `json:"endTime"` // 结束时间
  382. PreSettle float64 `json:"preSettle"` // 昨结价
  383. Count int `json:"Count"` // 交易日应有数据个数
  384. HistoryDatas []HistoryData `json:"historyDatas"` // 历史数据
  385. RunSteps []TSDataRunStep `json:"runSteps"` // 交易日开休市计划
  386. }
  387. // TSDataRunStep 分时图交易日开休市计划
  388. type TSDataRunStep struct {
  389. Groupid int32 `json:"groupid"` // 分组ID
  390. Tradeweekday int32 `json:"tradeweekday"` // 交易日归属 - 0:星期天、1:星期一、2:星期二、3:星期三、4:星期四、5:星期五、6:星期六
  391. Sectionid int32 `json:"sectionid"` // 从 1 开始 往下编 [0为系统清盘、结算时间] SectionId = 0时,开始时间=清盘时间 开始周几= 清盘周几, 结束时间=结算时间 结束周几=结算周几
  392. Runstep int32 `json:"runstep"` // 运行阶段 - 2:连续交易
  393. Startweekday int32 `json:"startweekday"` // 起始周几
  394. Starttime string `json:"starttime"` // 起始时间(HH:mm)
  395. Endweekday int32 `json:"endweekday"` // 结束周几
  396. Endtime string `json:"endtime"` // 结束时间(HH:mm)
  397. Startflag int32 `json:"startflag"` // 开始日标识 - (-1:上日 0:当日 1:次日 )
  398. Endflag int32 `json:"endflag"` // 结束日标识 - (-1:上日 0:当日 1:次日 )
  399. Start time.Time `json:"start"` // 真实开始时间
  400. End time.Time `json:"end"` // 真实结束时间
  401. }
  402. // QueryTSData 分时图数据查询
  403. // @Summary 分时图数据查询
  404. // @Produce json
  405. // @Security ApiKeyAuth
  406. // @Param goodsCode query string true "商品代码"
  407. // @Param makretId query int false "关联市场ID"
  408. // @Success 200 {object} QueryTSDataRsp
  409. // @Failure 500 {object} app.Response
  410. // @Router /Quote/QueryTSData [get]
  411. // @Tags 行情服务
  412. func QueryTSData(c *gin.Context) {
  413. appG := app.Gin{C: c}
  414. // 获取请求参数
  415. var req QueryTSDataReq
  416. if err := appG.C.ShouldBindQuery(&req); err != nil {
  417. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  418. appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
  419. return
  420. }
  421. // FIXME: - 一些不常变化的数据(如市场信息、商品信息等)应缓存到Redis中, 或缓存到服务内存
  422. // 获取商品信息
  423. goods, err := models.GetGoodsByGoodsCode(req.GoodsCode)
  424. if goods == nil {
  425. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  426. appG.Response(http.StatusBadRequest, e.ERROR_GET_GOODS_FAILED, nil)
  427. return
  428. }
  429. // 获取市场
  430. var market *models.Market
  431. if req.MarketId == 0 {
  432. market, err = models.GetMarketByGoodsCode(req.GoodsCode)
  433. if err != nil {
  434. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  435. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  436. return
  437. }
  438. } else {
  439. market, err = models.GetMarket(req.MarketId)
  440. if err != nil {
  441. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  442. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  443. return
  444. }
  445. }
  446. if market == nil {
  447. logger.GetLogger().Error("QueryTSData failed: not found goodscode")
  448. appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKET_FAILED, nil)
  449. return
  450. }
  451. // 获取目标品种交易日
  452. // FIXME: - 由于mtp2.0目前未同步外部交易所品种的当前交易日,
  453. // 故通道交易的品种目前只能在交易系统的外部市场中获
  454. // 取统一的交易日,后期应要求服务端同步外部数据
  455. // 2021.06.30 业务更新:15、97和99市场的行情交易日要从商品盘面中获取
  456. quoteTradeDate := ""
  457. if market.TRADEMODE == 15 || market.TRADEMODE == 97 || market.TRADEMODE == 99 || market.TRADEMODE == 80 || market.TRADEMODE == 81 {
  458. if quoteDays, err := models.GetQuoteDays(req.GoodsCode); err == nil {
  459. if len(quoteDays) > 0 {
  460. quoteTradeDate = time.Unix(quoteDays[0].Exchangedate, 0).Format("20060102")
  461. }
  462. }
  463. } else {
  464. if marketRun, err := models.GetMarketRun(int(market.MARKETID)); err == nil {
  465. quoteTradeDate = marketRun.Tradedate2
  466. }
  467. }
  468. if len(quoteTradeDate) == 0 {
  469. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  470. appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKETRUN_FAILED, nil)
  471. return
  472. }
  473. // 获取目标品种的开休市计划
  474. var runSteps []map[string]interface{}
  475. // 通道交易外部市场开休市计划表 - QuoteSourceGroupRunStep; 其它市场的 - MarketRunStepDetail
  476. if market.TRADEMODE == 15 || market.TRADEMODE == 97 || market.TRADEMODE == 99 {
  477. // 外部市场
  478. sourceRunSteps, err := models.FindQuoteSourceGroupRunSteps(*goods)
  479. if err != nil {
  480. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  481. appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil)
  482. return
  483. }
  484. for _, v := range sourceRunSteps {
  485. // struct -> json
  486. if jsonBytes, err := json.Marshal(v); err == nil {
  487. // json -> struct
  488. var runStepMap map[string]interface{}
  489. json.Unmarshal(jsonBytes, &runStepMap)
  490. runSteps = append(runSteps, runStepMap)
  491. }
  492. }
  493. }
  494. // 非外部市场或外部市场没有配置QuoteSourceGroupRunStep表数据的情况下,都从MarketRunStepDetail中获取数据
  495. if len(runSteps) == 0 {
  496. sourceRunSteps, err := models.FindMarketRunStepDetails(int(market.MARKETID))
  497. if err != nil {
  498. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  499. appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil)
  500. return
  501. }
  502. for _, v := range sourceRunSteps {
  503. // struct -> json
  504. if jsonBytes, err := json.Marshal(v); err == nil {
  505. // json -> struct
  506. var runStepMap map[string]interface{}
  507. json.Unmarshal(jsonBytes, &runStepMap)
  508. runSteps = append(runSteps, runStepMap)
  509. }
  510. }
  511. }
  512. // 获取目标商品的盘面信息
  513. // quoteDays, err := models.GetQuoteDays("'" + strings.ToUpper(goods.Goodscode) + "'")
  514. quoteDays, err := models.GetQuoteDays(strings.ToUpper(goods.Goodscode))
  515. if err != nil {
  516. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  517. appG.Response(http.StatusBadRequest, e.ERROR_GET_GOODS_FAILED, nil)
  518. return
  519. }
  520. var preSettle float64
  521. var preSettleInt int
  522. if len(quoteDays) > 0 {
  523. if quoteDays[0].Presettle != 0 {
  524. preSettleInt = int(quoteDays[0].Presettle)
  525. preSettle = utils.IntToFloat64(preSettleInt, int(goods.Decimalplace))
  526. }
  527. if preSettle == 0 && quoteDays[0].Preclose != 0 {
  528. preSettleInt = int(quoteDays[0].Preclose)
  529. preSettle = utils.IntToFloat64(preSettleInt, int(goods.Decimalplace))
  530. }
  531. }
  532. // 构建返回数据
  533. // 注意:这里使用的是 marketRun.Tradedate2 行情交易日
  534. queryTSDataRsp := QueryTSDataRsp{
  535. GoodsCode: goods.Goodscode,
  536. OutGoodsCode: goods.Outgoodscode,
  537. TradeDate: quoteTradeDate,
  538. DecimalPlace: int(goods.Decimalplace),
  539. PreSettle: preSettle,
  540. }
  541. // 构建分时图可直接使用的开休市数据
  542. // 这里有一个知识点:TRADEWEEKDAY 与 STARTWEEKDAY,以及 TRADEWEEKDAY 与 ENDWEEKDAY 之间相差最多一天(管理端限制),
  543. // 所以目前并不支持正真的周五夜盘模式。我们在实现时不用做得太复杂。
  544. // 当前交易日(周几)对应的开休市计划
  545. tradeDate, err := time.ParseInLocation("20060102", quoteTradeDate, time.Local)
  546. if err != nil {
  547. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  548. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  549. return
  550. }
  551. // !!!开休市计划明细
  552. curWeekRunSteps := make([]map[string]interface{}, 0)
  553. for _, v := range runSteps {
  554. tradeWeekDay := int(v["tradeweekday"].(float64))
  555. if tradeWeekDay == int(tradeDate.Weekday()) {
  556. curWeekRunSteps = append(curWeekRunSteps, v)
  557. }
  558. }
  559. // 获取不到可用的开休市计划
  560. if len(curWeekRunSteps) == 0 {
  561. err = errors.New("not found runstep")
  562. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  563. appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil)
  564. return
  565. }
  566. // 按 SECTIONID 顺序排序
  567. sort.Slice(curWeekRunSteps, func(i int, j int) bool {
  568. return curWeekRunSteps[i]["sectionid"].(float64) < curWeekRunSteps[j]["sectionid"].(float64)
  569. })
  570. // 把各开休市时间段转化为真实的时间
  571. // 关于开休市计划的时间顺序:管理端会按时间顺序添加开休市计划,所以交易日开始时间为第一条记录的开始时间,结束时间为最后一条记录的结束时间
  572. // 关于目标商品的交易日问题:目前只能从商品所属市场获取当前交易日,这样有两个问题,一是不能按常规显示最后一个有历史数据的交易日;二是目前所有外部商品的开休市计划都是一样的。
  573. timeFormat := "20060102 15:04"
  574. // 开始时间
  575. startInterval := GetTradeDay(int(curWeekRunSteps[0]["tradeweekday"].(float64)), int(curWeekRunSteps[0]["startweekday"].(float64)))
  576. queryTSDataRsp.StartTime, _ = time.ParseInLocation(timeFormat, fmt.Sprintf("%s %s", quoteTradeDate, curWeekRunSteps[0]["starttime"].(string)), time.Local)
  577. if startInterval != 0 {
  578. duration, _ := time.ParseDuration(fmt.Sprintf("%dh", startInterval*24))
  579. queryTSDataRsp.StartTime = queryTSDataRsp.StartTime.Add(duration)
  580. }
  581. // 结束时间
  582. index := len(curWeekRunSteps) - 1
  583. endInterval := GetTradeDay(int(curWeekRunSteps[index]["tradeweekday"].(float64)), int(curWeekRunSteps[index]["endweekday"].(float64)))
  584. queryTSDataRsp.EndTime, _ = time.ParseInLocation(timeFormat, fmt.Sprintf("%s %s", quoteTradeDate, curWeekRunSteps[index]["endtime"].(string)), time.Local)
  585. if endInterval != 0 {
  586. duration, _ := time.ParseDuration(fmt.Sprintf("%dh", endInterval*24))
  587. queryTSDataRsp.EndTime = queryTSDataRsp.EndTime.Add(duration)
  588. }
  589. // fmt.Printf("开始时间:%s 结束时间:%s\n", queryTSDataRsp.StartTime.Format(timeFormat), queryTSDataRsp.EndTime.Format(timeFormat))
  590. // 获取目标时间段的历史数据(1分钟周期)
  591. // 这里要注意:由于交易库和行情库由于GoodsCode大小写不一定对得上,所以在使用交易库的商品查询行情数据时间,都要使用OutGoodsCode字段
  592. // 20210702: 按行情服务要求,改用大写的GoodsCode
  593. cycleDatas, err := models.GetHistoryCycleDatas(models.CycleTypeMinutes1, strings.ToUpper(queryTSDataRsp.GoodsCode), &queryTSDataRsp.StartTime, &queryTSDataRsp.EndTime, 0, true)
  594. if err != nil {
  595. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  596. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  597. return
  598. }
  599. // ==================== 补数据(补休市数据和缺少的数据)====================
  600. if len(cycleDatas) > 0 {
  601. // 补数据第一步:如果第一数据不是开始时间的,需要补数据
  602. sources := time.Unix(int64(cycleDatas[0].ST), 0)
  603. diff := sources.Sub(queryTSDataRsp.StartTime)
  604. if diff.Minutes() > 0 {
  605. minute := int(diff.Minutes())
  606. for i := 1; i <= minute; i++ {
  607. st := cycleDatas[0].ST - i*60
  608. stt := time.Unix(int64(st), 0).Format("2006-01-02 15:04:05")
  609. // 前面的数据使用昨结来补
  610. cycleDatas = append(cycleDatas, models.CycleData{
  611. GC: cycleDatas[0].GC,
  612. Open: preSettleInt,
  613. High: preSettleInt,
  614. Low: preSettleInt,
  615. Close: preSettleInt,
  616. TV: 0,
  617. TT: 0,
  618. HV: 0,
  619. SP: 0,
  620. ST: st,
  621. SST: stt,
  622. FI: true,
  623. })
  624. }
  625. }
  626. // 接时间戳排序
  627. sort.Slice(cycleDatas, func(i int, j int) bool {
  628. return cycleDatas[i].ST < cycleDatas[j].ST
  629. })
  630. // 补数据第二步:按尾部的时间(当前服务器时间或最后休市时间)进行全补
  631. // 获取服务器时间
  632. s, _ := models.GetServerTime2()
  633. endTime, err := time.ParseInLocation("2006-01-02T15:04:05Z", *s, time.Local)
  634. if err != nil {
  635. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  636. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  637. return
  638. }
  639. if endTime.After(queryTSDataRsp.EndTime) {
  640. endTime = queryTSDataRsp.EndTime
  641. }
  642. index := len(cycleDatas) - 1
  643. sources = time.Unix(int64(cycleDatas[index].ST), 0)
  644. diff = endTime.Sub(sources)
  645. if diff.Minutes() > 0 {
  646. minute := int(diff.Minutes())
  647. for i := 1; i <= minute; i++ {
  648. st := cycleDatas[index].ST + i*60
  649. stt := time.Unix(int64(st), 0).Format("2006-01-02 15:04:05")
  650. cycleDatas = append(cycleDatas, models.CycleData{
  651. GC: cycleDatas[index].GC,
  652. Open: cycleDatas[index].Close,
  653. High: cycleDatas[index].Close,
  654. Low: cycleDatas[index].Close,
  655. Close: cycleDatas[index].Close,
  656. TV: 0,
  657. TT: 0,
  658. HV: 0,
  659. SP: 0,
  660. ST: st,
  661. SST: stt,
  662. FI: true,
  663. })
  664. }
  665. }
  666. // 接时间戳排序
  667. sort.Slice(cycleDatas, func(i int, j int) bool {
  668. return cycleDatas[i].ST < cycleDatas[j].ST
  669. })
  670. // 补数据第三步:补中间数据
  671. fillDatas := make([]models.CycleData, 0)
  672. for i := range cycleDatas {
  673. // 第一条记录跳过
  674. if i == 0 {
  675. continue
  676. }
  677. current := time.Unix(int64(cycleDatas[i].ST), 0)
  678. prev := time.Unix(int64(cycleDatas[i-1].ST), 0)
  679. diff := current.Sub(prev)
  680. if diff.Minutes() > 1 {
  681. minute := int(diff.Minutes())
  682. // 判断是否需要补数据,与上一条数据的间距不是一分钟
  683. if minute > 1 {
  684. // 注意补中间数据时,是不用补最后一条数据的,所以是 < minute
  685. for j := 1; j < minute; j++ {
  686. st := cycleDatas[i-1].ST + j*60
  687. stt := time.Unix(int64(st), 0).Format("2006-01-02 15:04:05")
  688. fillDatas = append(fillDatas, models.CycleData{
  689. GC: cycleDatas[i-1].GC,
  690. Open: cycleDatas[i-1].Close,
  691. High: cycleDatas[i-1].Close,
  692. Low: cycleDatas[i-1].Close,
  693. Close: cycleDatas[i-1].Close,
  694. TV: 0,
  695. TT: 0,
  696. HV: 0,
  697. SP: 0,
  698. ST: st,
  699. SST: stt,
  700. FI: true,
  701. })
  702. }
  703. }
  704. }
  705. }
  706. // 加入到源数据
  707. cycleDatas = append(cycleDatas, fillDatas...)
  708. // 接时间戳排序
  709. sort.Slice(cycleDatas, func(i int, j int) bool {
  710. return cycleDatas[i].ST < cycleDatas[j].ST
  711. })
  712. } else {
  713. // TODO: - 下面这块操作需求确认
  714. // 如果查询结果是空数据,则使用昨结价补到服务器时间(或最后休市时间)
  715. // 获取服务器时间
  716. s, _ := models.GetServerTime2()
  717. endTime, err := time.ParseInLocation("2006-01-02T15:04:05Z", *s, time.Local)
  718. if err != nil {
  719. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  720. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  721. return
  722. }
  723. if endTime.After(queryTSDataRsp.EndTime) {
  724. endTime = queryTSDataRsp.EndTime
  725. }
  726. diff := endTime.Sub(queryTSDataRsp.StartTime)
  727. minute := int(diff.Minutes())
  728. for i := 0; i < minute; i++ {
  729. st := int(queryTSDataRsp.StartTime.Unix()) + i*60
  730. stt := time.Unix(int64(st), 0).Format("2006-01-02 15:04:05")
  731. cycleDatas = append(cycleDatas, models.CycleData{
  732. GC: queryTSDataRsp.GoodsCode,
  733. Open: preSettleInt,
  734. High: preSettleInt,
  735. Low: preSettleInt,
  736. Close: preSettleInt,
  737. TV: 0,
  738. TT: 0,
  739. HV: 0,
  740. SP: 0,
  741. ST: st,
  742. SST: stt,
  743. FI: true,
  744. })
  745. }
  746. // 接时间戳排序
  747. sort.Slice(cycleDatas, func(i int, j int) bool {
  748. return cycleDatas[i].ST < cycleDatas[j].ST
  749. })
  750. }
  751. // 补数据第四步:清除掉开市计划外的数据
  752. // 先计算出每条计划明细的真正开始与结束时间
  753. queryTSDataRsp.RunSteps = make([]TSDataRunStep, 0)
  754. for _, v := range curWeekRunSteps {
  755. // 开始时间
  756. startInterval := GetTradeDay(int(v["tradeweekday"].(float64)), int(v["startweekday"].(float64)))
  757. v["start"], _ = time.ParseInLocation(timeFormat, fmt.Sprintf("%s %s", quoteTradeDate, v["starttime"].(string)), time.Local)
  758. if startInterval != 0 {
  759. duration, _ := time.ParseDuration(fmt.Sprintf("%dh", startInterval*24))
  760. v["start"] = v["start"].(time.Time).Add(duration)
  761. }
  762. // 结束时间
  763. endInterval := GetTradeDay(int(v["tradeweekday"].(float64)), int(v["endweekday"].(float64)))
  764. v["end"], _ = time.ParseInLocation(timeFormat, fmt.Sprintf("%s %s", quoteTradeDate, v["endtime"].(string)), time.Local)
  765. if endInterval != 0 {
  766. duration, _ := time.ParseDuration(fmt.Sprintf("%dh", endInterval*24))
  767. v["end"] = v["end"].(time.Time).Add(duration)
  768. }
  769. // map -> struct
  770. var tsDataRunStep TSDataRunStep
  771. jsonbody, err := json.Marshal(v)
  772. if err != nil {
  773. continue
  774. }
  775. if err := json.Unmarshal(jsonbody, &tsDataRunStep); err != nil {
  776. continue
  777. }
  778. queryTSDataRsp.RunSteps = append(queryTSDataRsp.RunSteps, tsDataRunStep)
  779. }
  780. // 获取目标商品报价小数位
  781. var tPlace int = 0
  782. var jPlace int32 = 2
  783. if goods, has := mtpcache.GetGoods(req.GoodsCode); has {
  784. s := goods.MARKETID[0:2]
  785. if s == "80" || s == "81" {
  786. if enumitems, err := models.GetEnumDicItem("digitalcurrency", int(goods.GOODSCURRENCYID)); err == nil && enumitems != nil {
  787. if len(enumitems) > 0 {
  788. tPlace, _ = strconv.Atoi(enumitems[0].Param1)
  789. }
  790. }
  791. if enumitems, err := models.GetEnumDicItem("digitalcurrency", int(goods.CURRENCYID)); err == nil && enumitems != nil {
  792. if len(enumitems) > 0 {
  793. j, _ := strconv.Atoi(enumitems[0].Param1)
  794. jPlace = int32(j)
  795. }
  796. }
  797. }
  798. }
  799. // 最终返回的历史数据
  800. historyDatas := make([]HistoryData, 0)
  801. for _, cycleData := range cycleDatas {
  802. needAdd := false
  803. for _, runStep := range curWeekRunSteps {
  804. // 判断是否在开市计划内
  805. if cycleData.ST >= int(runStep["start"].(time.Time).Unix()) && cycleData.ST <= int(runStep["end"].(time.Time).Unix()) {
  806. needAdd = true
  807. break
  808. }
  809. }
  810. if needAdd {
  811. tt, _ := decimal.NewFromFloat(utils.IntToFloat64(cycleData.TT, int(goods.Decimalplace))).Round(jPlace).Float64() // 按行情服务要求,金额类需要先除以商品报价小数位再按2位小数四舍五入
  812. historyDatas = append(historyDatas, HistoryData{
  813. Opened: utils.IntToFloat64(cycleData.Open, int(goods.Decimalplace)),
  814. Highest: utils.IntToFloat64(cycleData.High, int(goods.Decimalplace)),
  815. Lowest: utils.IntToFloat64(cycleData.Low, int(goods.Decimalplace)),
  816. Closed: utils.IntToFloat64(cycleData.Close, int(goods.Decimalplace)),
  817. TotleVolume: utils.IntToFloat64(cycleData.TV, tPlace),
  818. TotleTurnover: tt,
  819. HoldVolume: utils.IntToFloat64(cycleData.HV, tPlace),
  820. Settle: utils.IntToFloat64(cycleData.SP, int(goods.Decimalplace)),
  821. TimeStamp: time.Unix(int64(cycleData.ST), 0),
  822. IsFill: cycleData.FI,
  823. })
  824. }
  825. }
  826. queryTSDataRsp.HistoryDatas = historyDatas
  827. // 计算当前交易日应该有多少个历史数据,主要用于iOS
  828. for _, v := range queryTSDataRsp.RunSteps {
  829. diff := v.End.Sub(v.Start)
  830. queryTSDataRsp.Count += int(diff.Minutes())
  831. }
  832. // 查询成功
  833. logger.GetLogger().Debugln("QueryTSData successed.")
  834. appG.Response(http.StatusOK, e.SUCCESS, queryTSDataRsp)
  835. }
  836. // GetTradeDay 获取结算计划中天数间隔的方法
  837. // - tradeDay: 交易日周几
  838. // - weekDay: 开始或结束周几
  839. // - Returns: 天数间隔
  840. func GetTradeDay(tradeDay, weekDay int) int {
  841. if tradeDay == weekDay {
  842. return 0
  843. }
  844. if weekDay == 0 {
  845. weekDay = 7
  846. }
  847. betWeekDay := weekDay - tradeDay
  848. if betWeekDay < 0 {
  849. betWeekDay = betWeekDay + 7
  850. }
  851. if betWeekDay >= 4 {
  852. betWeekDay = betWeekDay - 7
  853. }
  854. return betWeekDay
  855. }
  856. /*
  857. *
  858. 获取某日所在周周一的日期
  859. */
  860. func getFirstDateOfWeek(d time.Time) time.Time {
  861. offset := int(time.Monday - d.Weekday())
  862. if offset > 0 {
  863. offset = -6
  864. }
  865. weekStartDate := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, offset)
  866. return weekStartDate
  867. }
  868. // 获取传入的时间所在月份的第一天,即某月第一天的0点。如传入time.Now(), 返回当前月份的第一天0点时间。
  869. func getFirstDateOfMonth(d time.Time) time.Time {
  870. d = d.AddDate(0, 0, -d.Day()+1)
  871. return getZeroTime(d)
  872. }
  873. // 获取传入的时间所在年的第一天,即某年第一天的0点。如传入time.Now(), 返回当前年的第一天0点时间。
  874. func getFirstDateOfYear(d time.Time) time.Time {
  875. d = d.AddDate(0, -d.Year()+1, 0)
  876. return getZeroTime(d)
  877. }
  878. // 获取传入的时间所在月份的最后一天,即某月最后一天的0点。如传入time.Now(), 返回当前月份的最后一天0点时间。
  879. func getLastDateOfMonth(d time.Time) time.Time {
  880. return getFirstDateOfMonth(d).AddDate(0, 1, -1)
  881. }
  882. // 获取某一天的0点时间
  883. func getZeroTime(d time.Time) time.Time {
  884. return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location())
  885. }