package quote import ( "encoding/json" "mtp2_if/global/app" "mtp2_if/global/e" "mtp2_if/logger" "mtp2_if/models" "mtp2_if/utils" "net/http" "sort" "time" "github.com/gin-gonic/gin" ) // HistoryData 历史数据 type HistoryData struct { Opened float64 `json:"o"` // 开盘价 Highest float64 `json:"h"` // 最高价 Lowest float64 `json:"l"` // 最低价 Closed float64 `json:"c"` // 收盘价 TotleVolume int `json:"tv"` // 总量 TotleTurnover float64 `json:"tt"` // 总金额 HoldVolume int `json:"hv"` // 持仓量 Settle float64 `json:"s"` // 结算价,日线周期(包括)以上才有 TimeStamp time.Time `json:"ts"` // 时间 } // QueryHistoryDatasReq 查询行情历史数据请求参数 type QueryHistoryDatasReq struct { CycleType int `form:"cycleType" binding:"required"` GoodsCode string `form:"goodsCode" binding:"required"` StartTime string `form:"startTime"` EndTime string `form:"endTime"` Count int `form:"count"` IsAsc bool `form:"isAsc"` } // QueryHistoryDatas 查询行情历史数据 // @Summary 查询行情历史数据 // @Produce json // @Security ApiKeyAuth // @Param cycleType query int true "周期类型, 0-秒 1: 1分钟 2: 5分钟 3: 30分钟 4: 60分钟 120: 2小时 240: 4小时 11: 日线 10: Tik" // @Param goodsCode query string true "商品代码" // @Param startTime query string false "开始时间,格式:yyyy-MM-dd HH:mm:ss" // @Param endTime query string false "结束时间,格式:yyyy-MM-dd HH:mm:ss" // @Param count query int false "条数" // @Param isAsc query bool false "是否按时间顺序排序(默认为时间倒序排序)" // @Success 200 {object} HistoryData // @Failure 500 {object} app.Response // @Router /Quote/QueryHistoryDatas [get] // @Tags 行情服务 func QueryHistoryDatas(c *gin.Context) { appG := app.Gin{C: c} // 获取请求参数 var req QueryHistoryDatasReq if err := appG.C.ShouldBindQuery(&req); err != nil { logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil) return } // 转换时间 timeFormat := "2006-01-02 15:04:05" // go中的时间格式化必须是这个时间 var startTime *time.Time if len(req.StartTime) > 0 { st, err := time.ParseInLocation(timeFormat, req.StartTime, time.Local) if err != nil { logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil) return } startTime = &st } var endTime *time.Time if len(req.EndTime) > 0 { et, err := time.ParseInLocation(timeFormat, req.EndTime, time.Local) if err != nil { logger.GetLogger().Errorf("QueryHistoryDatas failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_QUERY_TIME_FORMAT_FAIL, nil) return } endTime = &et } // 查询数据 cycleDatas, err := models.GetHistoryCycleDatas(models.CycleType(req.CycleType), req.GoodsCode, startTime, endTime, req.Count, req.IsAsc) if err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil) return } // 获取目标商品信息 goods, err := models.GetGoodsByGoodsCode(req.GoodsCode) if err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil) return } // 计算最终价格 rst := make([]HistoryData, 0) for _, v := range cycleDatas { historyData := HistoryData{ Opened: utils.IntToFloat64(v.Open, int(goods.Decimalplace)), Highest: utils.IntToFloat64(v.High, int(goods.Decimalplace)), Lowest: utils.IntToFloat64(v.Low, int(goods.Decimalplace)), Closed: utils.IntToFloat64(v.Close, int(goods.Decimalplace)), TotleVolume: v.TV, TotleTurnover: float64(v.TT), HoldVolume: v.HV, Settle: utils.IntToFloat64(v.SP, int(goods.Decimalplace)), TimeStamp: time.Unix(int64(v.ST), 0), } rst = append(rst, historyData) } // 查询成功 logger.GetLogger().Debugln("QueryHistoryDatas successed: %v", rst) appG.Response(http.StatusOK, e.SUCCESS, rst) } // QueryTSDataReq 分时图数据查询请求参数 type QueryTSDataReq struct { GoodsCode string `form:"goodsCode" binding:"required"` // 商品代码 } // QueryTSDataRsp 分时图数据查询返回模型 type QueryTSDataRsp struct { GoodsCode string `json:"goodsCode"` // 商品代码 DecimalPlace int `json:"decimalPlace"` // 小数位 TradeDate string `json:"tradeDate"` // 交易日 StartTime string `json:"startTime"` // 开始时间 EndTime string `json:"endTime"` // 结束时间 PreSettle float32 `json:"preSettle"` // 昨结 HistoryDatas []HistoryData `json:"historyDatas"` // 历史数据 } // QueryTSData 分时图数据查询 // @Summary 分时图数据查询 // @Produce json // @Param GoodsCode query string true "商品代码" // @Success 200 {object} QueryTSDataRsp // @Failure 500 {object} app.Response // @Router /Quote/QueryTSData [get] // @Tags 行情服务 func QueryTSData(c *gin.Context) { appG := app.Gin{C: c} // 获取请求参数 var req QueryTSDataReq if err := appG.C.ShouldBindQuery(&req); err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil) return } var queryTSDataRsp QueryTSDataRsp // FIXME: - 一些不常变化的数据(如市场信息、商品信息等)应缓存到Redis中, 或缓存到服务内存 market, err := models.GetMarketByGoodsCode(req.GoodsCode) if err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil) return } if market == nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKET_FAILED, nil) return } // 获取目标品种交易日 // FIXME: - 由于mtp2.0目前未同步外部交易所品种的当前交易日, // 故通道交易的品种目前只能在交易系统的外部市场中获 // 取统一的交易日,后期应要求服务端同步外部数据 marketRun, err := models.GetMarketRun(int(market.Marketid)) if marketRun == nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_GET_MARKETRUN_FAILED, nil) return } // 获取目标品种的开休市计划 var runSteps []map[string]interface{} // 通道交易外部市场开休市计划表 - QuoteSourceGroupRunStep; 其它市场的 - MarketRunStepDetail if market.Trademode == 15 { // 外部市场 sourceRunSteps, err := models.FindQuoteSourceGroupRunStepsByMarket(*market) if err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil) return } for v := range sourceRunSteps { // struct -> json if jsonBytes, err := json.Marshal(v); err == nil { // json -> struct var runStepMap map[string]interface{} json.Unmarshal(jsonBytes, &runStepMap) runSteps = append(runSteps, runStepMap) } } } // 非外部市场或外部市场没有配置QuoteSourceGroupRunStep表数据的情况下,都从MarketRunStepDetail中获取数据 if len(runSteps) == 0 { sourceRunSteps, err := models.FindMarketRunStepDetails(int(market.Marketid)) if err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil) return } for _, v := range sourceRunSteps { // struct -> json if jsonBytes, err := json.Marshal(v); err == nil { // json -> struct var runStepMap map[string]interface{} json.Unmarshal(jsonBytes, &runStepMap) runSteps = append(runSteps, runStepMap) } } } // 构建分时图可直接使用的开休市数据 // 这里有一个知识点:TRADEWEEKDAY 与 STARTWEEKDAY,以及 TRADEWEEKDAY 与 ENDWEEKDAY 之间相差最多一天(管理端限制), // 所以目前并不支持正真的周五夜盘模式。我们在实现时不用做得太复杂。 // 当前交易日(周几)对应的开休市计划 tradeDate, err := time.ParseInLocation("2006-01-02", marketRun.Tradedate, time.Local) if err != nil { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_QUERY_FAIL, nil) return } curWeekRunSteps := make([]map[string]interface{}, 0) for _, v := range runSteps { tradeWeekDay := v["tradeweekday"].(int) if tradeWeekDay == int(tradeDate.Weekday()) { curWeekRunSteps = append(curWeekRunSteps, v) } } // 获取不到可用的开休市计划 if len(curWeekRunSteps) == 0 { logger.GetLogger().Errorf("QueryTSData failed: %s", err.Error()) appG.Response(http.StatusBadRequest, e.ERROR_GET_RUNSTEP_FAILED, nil) return } // 按 SECTIONID 顺序排序(管理端会按时间顺序添加开休市计划) sort.Slice(curWeekRunSteps, func(i int, j int) bool { return curWeekRunSteps[i]["sectionid"].(int) < curWeekRunSteps[j]["sectionid"].(int) }) // 把各开休市时间段转化为真实的时间 // // 查询成功 logger.GetLogger().Debugln("QueryTSData successed: %v", queryTSDataRsp) appG.Response(http.StatusOK, e.SUCCESS, queryTSDataRsp) }