history.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. package quote
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "mtp2_if/global/app"
  6. "mtp2_if/global/e"
  7. "mtp2_if/logger"
  8. "mtp2_if/models"
  9. "mtp2_if/utils"
  10. "net/http"
  11. "sort"
  12. "time"
  13. "github.com/gin-gonic/gin"
  14. )
  15. // HistoryData 历史数据
  16. type HistoryData struct {
  17. Opened float64 `json:"o"` // 开盘价
  18. Highest float64 `json:"h"` // 最高价
  19. Lowest float64 `json:"l"` // 最低价
  20. Closed float64 `json:"c"` // 收盘价
  21. TotleVolume int `json:"tv"` // 总量
  22. TotleTurnover float64 `json:"tt"` // 总金额
  23. HoldVolume int `json:"hv"` // 持仓量
  24. Settle float64 `json:"s"` // 结算价,日线周期(包括)以上才有
  25. TimeStamp time.Time `json:"ts"` // 时间
  26. IsFill bool `json:"f"` // 是否补充数据
  27. }
  28. // QueryHistoryDatasReq 查询行情历史数据请求参数
  29. type QueryHistoryDatasReq struct {
  30. CycleType int `form:"cycleType" binding:"required"`
  31. GoodsCode string `form:"goodsCode" binding:"required"`
  32. StartTime string `form:"startTime"`
  33. EndTime string `form:"endTime"`
  34. Count int `form:"count"`
  35. IsAsc bool `form:"isAsc"`
  36. }
  37. // QueryHistoryDatas 查询行情历史数据
  38. // @Summary 查询行情历史数据
  39. // @Produce json
  40. // @Security ApiKeyAuth
  41. // @Param cycleType query int true "周期类型, 0-秒 1: 1分钟 2: 5分钟 3: 30分钟 4: 60分钟 120: 2小时 240: 4小时 11: 日线 10: Tik"
  42. // @Param goodsCode query string true "商品代码"
  43. // @Param startTime query string false "开始时间,格式:yyyy-MM-dd HH:mm:ss"
  44. // @Param endTime query string false "结束时间,格式:yyyy-MM-dd HH:mm:ss"
  45. // @Param count query int false "条数"
  46. // @Param isAsc query bool false "是否按时间顺序排序(默认为时间倒序排序)"
  47. // @Success 200 {object} HistoryData
  48. // @Failure 500 {object} app.Response
  49. // @Router /Quote/QueryHistoryDatas [get]
  50. // @Tags 行情服务
  51. func QueryHistoryDatas(c *gin.Context) {
  52. appG := app.Gin{C: c}
  53. // 获取请求参数
  54. var req QueryHistoryDatasReq
  55. if err := appG.C.ShouldBindQuery(&req); err != nil {
  56. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  57. appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
  58. return
  59. }
  60. // 转换时间
  61. timeFormat := "2006-01-02 15:04:05" // go中的时间格式化必须是这个时间
  62. var startTime *time.Time
  63. if len(req.StartTime) > 0 {
  64. st, err := time.ParseInLocation(timeFormat, req.StartTime, time.Local)
  65. if err != nil {
  66. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  67. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil)
  68. return
  69. }
  70. startTime = &st
  71. }
  72. var endTime *time.Time
  73. if len(req.EndTime) > 0 {
  74. et, err := time.ParseInLocation(timeFormat, req.EndTime, time.Local)
  75. if err != nil {
  76. logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error())
  77. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil)
  78. return
  79. }
  80. endTime = &et
  81. }
  82. // 查询数据
  83. cycleDatas, err := models.GetHistoryCycleDatas(models.CycleType(req.CycleType), req.GoodsCode, startTime, endTime, req.Count, req.IsAsc)
  84. if err != nil {
  85. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  86. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  87. return
  88. }
  89. // 获取目标商品信息
  90. goods, err := models.GetGoodsByGoodsCode(req.GoodsCode)
  91. if err != nil {
  92. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  93. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  94. return
  95. }
  96. // 计算最终价格
  97. rst := make([]HistoryData, 0)
  98. for _, v := range cycleDatas {
  99. historyData := HistoryData{
  100. Opened: utils.IntToFloat64(v.Open, int(goods.Decimalplace)),
  101. Highest: utils.IntToFloat64(v.High, int(goods.Decimalplace)),
  102. Lowest: utils.IntToFloat64(v.Low, int(goods.Decimalplace)),
  103. Closed: utils.IntToFloat64(v.Close, int(goods.Decimalplace)),
  104. TotleVolume: v.TV,
  105. TotleTurnover: float64(v.TT),
  106. HoldVolume: v.HV,
  107. Settle: utils.IntToFloat64(v.SP, int(goods.Decimalplace)),
  108. TimeStamp: time.Unix(int64(v.ST), 0),
  109. }
  110. rst = append(rst, historyData)
  111. }
  112. // 查询成功
  113. logger.GetLogger().Debugln("QueryHistoryDatas successed: %v", rst)
  114. appG.Response(http.StatusOK, e.SUCCESS, rst)
  115. }
  116. // QueryTSDataReq 分时图数据查询请求参数
  117. type QueryTSDataReq struct {
  118. GoodsCode string `form:"goodsCode" binding:"required"` // 商品代码
  119. }
  120. // QueryTSDataRsp 分时图数据查询返回模型
  121. type QueryTSDataRsp struct {
  122. GoodsCode string `json:"goodsCode"` // 商品代码
  123. OutGoodsCode string `json:"outGoodsCode"` // 外部商品代码
  124. DecimalPlace int `json:"decimalPlace"` // 小数位
  125. TradeDate string `json:"tradeDate"` // 交易日
  126. StartTime time.Time `json:"startTime"` // 开始时间
  127. EndTime time.Time `json:"endTime"` // 结束时间
  128. PreSettle float32 `json:"preSettle"` // 昨结
  129. HistoryDatas []HistoryData `json:"historyDatas"` // 历史数据
  130. }
  131. // QueryTSData 分时图数据查询
  132. // @Summary 分时图数据查询
  133. // @Produce json
  134. // @Param goodsCode query string true "商品代码"
  135. // @Success 200 {object} QueryTSDataRsp
  136. // @Failure 500 {object} app.Response
  137. // @Router /Quote/QueryTSData [get]
  138. // @Tags 行情服务
  139. func QueryTSData(c *gin.Context) {
  140. appG := app.Gin{C: c}
  141. // 获取请求参数
  142. var req QueryTSDataReq
  143. if err := appG.C.ShouldBindQuery(&req); err != nil {
  144. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  145. appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
  146. return
  147. }
  148. // FIXME: - 一些不常变化的数据(如市场信息、商品信息等)应缓存到Redis中, 或缓存到服务内存
  149. market, err := models.GetMarketByGoodsCode(req.GoodsCode)
  150. if err != nil {
  151. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  152. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  153. return
  154. }
  155. if market == nil {
  156. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  157. appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKET_FAILED, nil)
  158. return
  159. }
  160. // 获取目标品种交易日
  161. // FIXME: - 由于mtp2.0目前未同步外部交易所品种的当前交易日,
  162. // 故通道交易的品种目前只能在交易系统的外部市场中获
  163. // 取统一的交易日,后期应要求服务端同步外部数据
  164. marketRun, err := models.GetMarketRun(int(market.Marketid))
  165. if marketRun == nil {
  166. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  167. appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKETRUN_FAILED, nil)
  168. return
  169. }
  170. // 获取目标品种的开休市计划
  171. var runSteps []map[string]interface{}
  172. // 通道交易外部市场开休市计划表 - QuoteSourceGroupRunStep; 其它市场的 - MarketRunStepDetail
  173. if market.Trademode == 15 {
  174. // 外部市场
  175. sourceRunSteps, err := models.FindQuoteSourceGroupRunStepsByMarket(*market)
  176. if err != nil {
  177. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  178. appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil)
  179. return
  180. }
  181. for v := range sourceRunSteps {
  182. // struct -> json
  183. if jsonBytes, err := json.Marshal(v); err == nil {
  184. // json -> struct
  185. var runStepMap map[string]interface{}
  186. json.Unmarshal(jsonBytes, &runStepMap)
  187. runSteps = append(runSteps, runStepMap)
  188. }
  189. }
  190. }
  191. // 非外部市场或外部市场没有配置QuoteSourceGroupRunStep表数据的情况下,都从MarketRunStepDetail中获取数据
  192. if len(runSteps) == 0 {
  193. sourceRunSteps, err := models.FindMarketRunStepDetails(int(market.Marketid))
  194. if err != nil {
  195. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  196. appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil)
  197. return
  198. }
  199. for _, v := range sourceRunSteps {
  200. // struct -> json
  201. if jsonBytes, err := json.Marshal(v); err == nil {
  202. // json -> struct
  203. var runStepMap map[string]interface{}
  204. json.Unmarshal(jsonBytes, &runStepMap)
  205. runSteps = append(runSteps, runStepMap)
  206. }
  207. }
  208. }
  209. // 获取商品信息
  210. goods, err := models.GetGoodsByGoodsCode(req.GoodsCode)
  211. if goods == nil {
  212. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  213. appG.Response(http.StatusBadRequest, e.ERROR_GET_GOODS_FAILED, nil)
  214. return
  215. }
  216. // 构建返回数据
  217. queryTSDataRsp := QueryTSDataRsp{
  218. GoodsCode: goods.Goodscode,
  219. OutGoodsCode: goods.Outgoodscode,
  220. TradeDate: marketRun.Tradedate,
  221. DecimalPlace: int(goods.Decimalplace),
  222. }
  223. // 构建分时图可直接使用的开休市数据
  224. // 这里有一个知识点:TRADEWEEKDAY 与 STARTWEEKDAY,以及 TRADEWEEKDAY 与 ENDWEEKDAY 之间相差最多一天(管理端限制),
  225. // 所以目前并不支持正真的周五夜盘模式。我们在实现时不用做得太复杂。
  226. // 当前交易日(周几)对应的开休市计划
  227. tradeDate, err := time.ParseInLocation("20060102", marketRun.Tradedate, time.Local)
  228. if err != nil {
  229. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  230. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  231. return
  232. }
  233. curWeekRunSteps := make([]map[string]interface{}, 0)
  234. for _, v := range runSteps {
  235. tradeWeekDay := int(v["tradeweekday"].(float64))
  236. if tradeWeekDay == int(tradeDate.Weekday()) {
  237. curWeekRunSteps = append(curWeekRunSteps, v)
  238. }
  239. }
  240. // 获取不到可用的开休市计划
  241. if len(curWeekRunSteps) == 0 {
  242. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  243. appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil)
  244. return
  245. }
  246. // 按 SECTIONID 顺序排序
  247. sort.Slice(curWeekRunSteps, func(i int, j int) bool {
  248. return curWeekRunSteps[i]["sectionid"].(int) < curWeekRunSteps[j]["sectionid"].(int)
  249. })
  250. // 把各开休市时间段转化为真实的时间
  251. // 关于开休市计划的时间顺序:管理端会按时间顺序添加开休市计划,所以交易日开始时间为第一条记录的开始时间,结束时间为最后一条记录的结束时间
  252. // 关于目标商品的交易日问题:目前只能从商品所属市场获取当前交易日,这样有两个问题,一是不能按常规显示最后一个有历史数据的交易日;二是目前所有外部商品的开休市计划都是一样的。
  253. timeFormat := "20060102 15:04"
  254. // 开始时间
  255. startInterval := getTradeDay(int(curWeekRunSteps[0]["tradeweekday"].(float64)), int(curWeekRunSteps[0]["startweekday"].(float64)))
  256. queryTSDataRsp.StartTime, _ = time.ParseInLocation(timeFormat, fmt.Sprintf("%s %s", marketRun.Tradedate, curWeekRunSteps[0]["starttime"].(string)), time.Local)
  257. if startInterval != 0 {
  258. duration, _ := time.ParseDuration(fmt.Sprintf("%dh", startInterval*24))
  259. queryTSDataRsp.StartTime = queryTSDataRsp.StartTime.Add(duration)
  260. }
  261. // 结束时间
  262. index := len(curWeekRunSteps) - 1
  263. endInterval := getTradeDay(int(curWeekRunSteps[index]["tradeweekday"].(float64)), int(curWeekRunSteps[index]["endweekday"].(float64)))
  264. queryTSDataRsp.EndTime, _ = time.ParseInLocation(timeFormat, fmt.Sprintf("%s %s", marketRun.Tradedate, curWeekRunSteps[index]["endtime"].(string)), time.Local)
  265. if endInterval != 0 {
  266. duration, _ := time.ParseDuration(fmt.Sprintf("%dh", endInterval*24))
  267. queryTSDataRsp.EndTime = queryTSDataRsp.EndTime.Add(duration)
  268. }
  269. fmt.Printf("开始时间:%s 结束时间:%s\n", queryTSDataRsp.StartTime.Format(timeFormat), queryTSDataRsp.EndTime.Format(timeFormat))
  270. // 获取目标时间段的历史数据
  271. // 这里要注意:由于交易库和行情库由于GoodsCode大小写不一定对得上,所以在使用交易库的商品查询行情数据时间,都要使用OutGoodsCode字段
  272. cycleDatas, err := models.GetHistoryCycleDatas(models.CycleTypeMinutes1, queryTSDataRsp.OutGoodsCode, &queryTSDataRsp.StartTime, &queryTSDataRsp.EndTime, 0, true)
  273. if err != nil {
  274. logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error())
  275. appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil)
  276. return
  277. }
  278. // ==================== 补数据(补休市数据和缺少的数据)====================
  279. // 补数据第一步:如果第一数据不是开始时间的,需要补数据
  280. if len(cycleDatas) > 0 {
  281. sources := time.Unix(int64(cycleDatas[0].ST), 0)
  282. diff := sources.Sub(queryTSDataRsp.StartTime)
  283. if diff.Minutes() > 0 {
  284. minute := int(diff.Minutes())
  285. for i := 0; i < minute; i++ {
  286. st := cycleDatas[0].ST - i*60
  287. stt := time.Unix(int64(st), 0).Format("2006-01-02 15:04:05")
  288. cycleDatas = append(cycleDatas, models.CycleData{
  289. GC: cycleDatas[0].GC,
  290. Open: cycleDatas[0].Open,
  291. High: cycleDatas[0].High,
  292. Low: cycleDatas[0].Low,
  293. Close: cycleDatas[0].Close,
  294. TV: cycleDatas[0].TV,
  295. TT: cycleDatas[0].TT,
  296. HV: cycleDatas[0].HV,
  297. SP: cycleDatas[0].SP,
  298. ST: st,
  299. SST: stt,
  300. })
  301. }
  302. }
  303. // 接时间戳排序
  304. sort.Slice(cycleDatas, func(i int, j int) bool {
  305. return cycleDatas[i].ST < cycleDatas[j].ST
  306. })
  307. }
  308. // 补数据第二步:按尾部的时间(当前服务器时间或最后休市时间)进行全补
  309. if len(cycleDatas) > 0 {
  310. s, _ := models.GetServerTime()
  311. endTime, _ := time.ParseInLocation("2006/01/02 15:04:05", *s, time.Local)
  312. if endTime.After(queryTSDataRsp.EndTime) {
  313. endTime = queryTSDataRsp.EndTime
  314. }
  315. // 判断是否需要补数据,与上一条数据的间距不是一分钟
  316. index := len(cycleDatas) - 1
  317. sources := time.Unix(int64(cycleDatas[index].ST), 0)
  318. diff := sources.Sub(endTime)
  319. if diff.Minutes() > 0 {
  320. minute := int(diff.Minutes())
  321. for i := 0; i < minute; i++ {
  322. st := cycleDatas[index].ST + i*60
  323. stt := time.Unix(int64(st), 0).Format("2006-01-02 15:04:05")
  324. cycleDatas = append(cycleDatas, models.CycleData{
  325. GC: cycleDatas[index].GC,
  326. Open: cycleDatas[index].Open,
  327. High: cycleDatas[index].High,
  328. Low: cycleDatas[index].Low,
  329. Close: cycleDatas[index].Close,
  330. TV: cycleDatas[index].TV,
  331. TT: cycleDatas[index].TT,
  332. HV: cycleDatas[index].HV,
  333. SP: cycleDatas[index].SP,
  334. ST: st,
  335. SST: stt,
  336. })
  337. }
  338. }
  339. // 接时间戳排序
  340. sort.Slice(cycleDatas, func(i int, j int) bool {
  341. return cycleDatas[i].ST < cycleDatas[j].ST
  342. })
  343. }
  344. // 补数据第三步:补中间数据
  345. // 查询成功
  346. logger.GetLogger().Debugln("QueryTSData successed: %v", queryTSDataRsp)
  347. appG.Response(http.StatusOK, e.SUCCESS, queryTSDataRsp)
  348. }
  349. // getTradeDay 获取结算计划中天数间隔的方法
  350. // - tradeDay: 交易日周几
  351. // - weekDay: 开始或结束周几
  352. // - Returns: 天数间隔
  353. func getTradeDay(tradeDay, weekDay int) int {
  354. if tradeDay == weekDay {
  355. return 0
  356. }
  357. if weekDay == 0 {
  358. weekDay = 7
  359. }
  360. betWeekDay := weekDay - tradeDay
  361. if betWeekDay < 0 {
  362. betWeekDay = betWeekDay + 7
  363. }
  364. if betWeekDay >= 4 {
  365. betWeekDay = betWeekDay - 7
  366. }
  367. return betWeekDay
  368. }