Browse Source

Merge branch 'feature/eta_2.1.4' into debug

hsun 1 week ago
parent
commit
3d3cb4b34a
31 changed files with 4114 additions and 621 deletions
  1. 4 0
      controllers/data_manage/excel/excel_info.go
  2. 93 3
      controllers/trade_analysis/trade_analysis.go
  3. 719 0
      controllers/trade_analysis/trade_analysis_correlation.go
  4. 842 0
      controllers/trade_analysis/trade_analysis_table.go
  5. 2 2
      controllers/trade_analysis/warehouse.go
  6. 80 0
      models/common/common_classify.go
  7. 18 0
      models/data_manage/excel/excel_info.go
  8. 10 0
      models/data_manage/trade_analysis/base_from_trade_exchange.go
  9. 35 0
      models/data_manage/trade_analysis/request/trade_analysis_correlation.go
  10. 37 0
      models/data_manage/trade_analysis/request/trade_analysis_table.go
  11. 17 0
      models/data_manage/trade_analysis/response/trade_analysis_correlation.go
  12. 18 0
      models/data_manage/trade_analysis/response/trade_analysis_table.go
  13. 246 359
      models/data_manage/trade_analysis/trade_analysis.go
  14. 43 0
      models/data_manage/trade_analysis/trade_analysis_correlation.go
  15. 41 0
      models/data_manage/trade_analysis/trade_analysis_table.go
  16. 188 0
      models/data_manage/trade_analysis/trade_analysis_table_column.go
  17. 13 13
      models/data_manage/trade_analysis/warehouse_process_classify.go
  18. 12 10
      models/db.go
  19. 135 0
      routers/commentsRouter.go
  20. 2 0
      routers/router.go
  21. 11 11
      services/data/common_classify.go
  22. 19 19
      services/data/common_classify_ctx.go
  23. 21 0
      services/data/edb_classify.go
  24. 25 0
      services/data/trade_analysis/trade_analysis.go
  25. 417 0
      services/data/trade_analysis/trade_analysis_correlation.go
  26. 410 132
      services/data/trade_analysis/trade_analysis_data.go
  27. 333 67
      services/data/trade_analysis/trade_analysis_interface.go
  28. 287 0
      services/data/trade_analysis/trade_analysis_table.go
  29. 19 0
      services/excel_info.go
  30. 8 0
      utils/common.go
  31. 9 5
      utils/constants.go

+ 4 - 0
controllers/data_manage/excel/excel_info.go

@@ -615,6 +615,10 @@ func (c *ExcelInfoController) List() {
 				// excel表格按钮权限
 				list[k].Button = excel2.GetBalanceExcelInfoOpButton(sysUser.AdminId, v.SysUserId, v.HaveOperaAuth, v.ExcelInfoId)
 			}
+			// 持仓分析表格
+			if v.Source == utils.TRADE_ANALYSIS_TABLE || v.Source == utils.TRADE_ANALYSIS_CORRELATION_TABLE {
+				list[k].Button = services.GetTradeAnalysisTableOpButton(v.SysUserId, sysUser.AdminId, sysUser.RoleTypeCode, v.HaveOperaAuth)
+			}
 		}
 
 	}

+ 93 - 3
controllers/trade_analysis/trade_analysis.go

@@ -227,9 +227,6 @@ func (this *TradeAnalysisController) GetTradeClassifyList() {
 		// 郑商所
 		if v.Exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
 			name := trade_analysis.GetZhengzhouClassifyName(v.ClassifyName)
-			if name == "" {
-				continue
-			}
 			if classifyExist[name] {
 				continue
 			}
@@ -424,3 +421,96 @@ func (this *TradeAnalysisController) GetTradeFuturesCompanyList() {
 	br.Ret = 200
 	br.Success = true
 }
+
+// GetTradeExchangeClassifyTree
+// @Title 获取交易所-品种树
+// @Description 获取交易所-品种树
+// @Param   IsTotal  query  bool  false  "是否显示全部交易所"
+// @Success 200 {object} data_manage.BaseFromTradeExchangeItem
+// @router /exchange_classify/tree [get]
+func (this *TradeAnalysisController) GetTradeExchangeClassifyTree() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	isTotal, _ := this.GetBool("IsTotal", false)
+
+	// 交易所
+	var cond string
+	var pars []interface{}
+	exchangeOb := new(tradeAnalysisModel.BaseFromTradeExchange)
+	if !isTotal {
+		cond += fmt.Sprintf(` AND %s = ?`, exchangeOb.Cols().AnalysisState)
+		pars = append(pars, 1)
+	}
+	exchanges, e := exchangeOb.GetItemsByCondition(cond, pars, []string{}, "")
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取交易所列表失败, %v", e)
+		return
+	}
+
+	// 品种
+	classifyOb := new(tradeAnalysisModel.BaseFromTradeClassify)
+	fields := []string{classifyOb.Cols().ClassifyName, classifyOb.Cols().Exchange}
+	classifies, e := classifyOb.GetClassifyItemsByCondition(``, make([]interface{}, 0), fields, "id ASC")
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取品种列表失败, %v", e)
+		return
+	}
+	exchangeClassify := make(map[string][]*tradeAnalysisModel.BaseFromTradeClassify, 0)
+	for _, v := range classifies {
+		if exchangeClassify[v.Exchange] == nil {
+			exchangeClassify[v.Exchange] = make([]*tradeAnalysisModel.BaseFromTradeClassify, 0)
+		}
+		exchangeClassify[v.Exchange] = append(exchangeClassify[v.Exchange], v)
+	}
+
+	// 构建树
+	resp := make([]*tradeAnalysisModel.TradeExchangeClassifyNode, 0)
+	classifyExist := make(map[string]bool)
+	for _, v := range exchanges {
+		t := new(tradeAnalysisModel.TradeExchangeClassifyNode)
+		t.UniqueFlag = v.Exchange
+		t.NodeType = 1
+		t.NodeName = v.ExchangeName
+		t.NodeValue = v.Exchange
+		t.Children = make([]*tradeAnalysisModel.TradeExchangeClassifyNode, 0)
+		for _, c := range exchangeClassify[v.Exchange] {
+			// 郑商所
+			classifyName := c.ClassifyName
+			if v.Exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
+				classifyName = trade_analysis.GetZhengzhouClassifyName(c.ClassifyName)
+				if classifyExist[classifyName] {
+					continue
+				}
+				classifyExist[classifyName] = true
+			}
+			tc := new(tradeAnalysisModel.TradeExchangeClassifyNode)
+			tc.UniqueFlag = fmt.Sprintf("%s-%s", v.Exchange, classifyName)
+			tc.ParentFlag = v.Exchange
+			tc.NodeType = 2
+			tc.NodeName = classifyName
+			tc.NodeValue = classifyName
+			t.Children = append(t.Children, tc)
+		}
+		resp = append(resp, t)
+	}
+
+	br.Data = resp
+	br.Msg = "获取成功"
+	br.Ret = 200
+	br.Success = true
+}

+ 719 - 0
controllers/trade_analysis/trade_analysis_correlation.go

@@ -0,0 +1,719 @@
+package trade_analysis
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/data_manage"
+	"eta/eta_api/models/data_manage/excel"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	tradeAnalysisRequest "eta/eta_api/models/data_manage/trade_analysis/request"
+	tradeAnalysisResponse "eta/eta_api/models/data_manage/trade_analysis/response"
+	"eta/eta_api/services"
+	tradeAnalysisService "eta/eta_api/services/data/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/tealeg/xlsx"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// TradeAnalysisCorrelationController 相关性分析
+type TradeAnalysisCorrelationController struct {
+	controllers.BaseAuthController
+}
+
+// Preview
+// @Title 表格预览
+// @Description 表格预览
+// @Param	request	body request.CorrelationTableResp true "type json string"
+// @Success 200 {object} response.TableResp
+// @router /correlation/preview [post]
+func (this *TradeAnalysisCorrelationController) Preview() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.CorrelationTablePreviewReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	// 校验表格配置
+	pass, tips := tradeAnalysisService.CheckAnalysisCorrelationExtraConfig(req.TableConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+
+	// 获取表格数据
+	tableData, e := tradeAnalysisService.GetCorrelationTableRowsDataByConfig(req.TableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格数据失败, %v", e)
+		return
+	}
+
+	// 标的指标名称
+	if req.TableConfig.BaseEdbInfoId > 0 {
+		baseEdb, e := data_manage.GetEdbInfoById(req.TableConfig.BaseEdbInfoId)
+		if e != nil && e.Error() != utils.ErrNoRow() {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取标的指标失败, %v", e)
+			return
+		}
+		if baseEdb != nil {
+			req.TableConfig.BaseEdbName = baseEdb.EdbName
+		}
+	}
+
+	resp := new(tradeAnalysisResponse.CorrelationTableResp)
+	resp.TableName = strings.TrimSpace(req.TableName)
+	resp.ClassifyId = req.ClassifyId
+	resp.TableConfig = req.TableConfig
+	resp.TableData = tableData
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Save
+// @Title 保存表格
+// @Description 保存表格
+// @Param	request	body request.CorrelationTableSaveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /correlation/save [post]
+func (this *TradeAnalysisCorrelationController) Save() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.CorrelationTableSaveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	// 校验表格配置
+	pass, tips := tradeAnalysisService.CheckAnalysisCorrelationExtraConfig(req.TableConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+	req.TableName = strings.TrimSpace(req.TableName)
+	if req.TableName == "" {
+		br.Msg = "请输入表格名称"
+		return
+	}
+	if req.ClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+	configByte, e := json.Marshal(req.TableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格配置JSON格式化失败, %v", e)
+		return
+	}
+	extraConfig := string(configByte)
+
+	// 表格数据
+	tableData, e := tradeAnalysisService.GetCorrelationTableRowsDataByConfig(req.TableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格数据失败, %v", e)
+		return
+	}
+	contentByte, e := json.Marshal(tableData)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格数据JSON格式化失败, %v", e)
+		return
+	}
+	excelContent := string(contentByte)
+
+	// 新增表格
+	var excelId int
+	if req.ExcelInfoId <= 0 {
+		timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) + "_" + utils.GetRandString(10)
+		newExcel := &excel.ExcelInfo{
+			ExcelName:          req.TableName,
+			Source:             utils.TRADE_ANALYSIS_CORRELATION_TABLE,
+			ExcelType:          1,
+			UniqueCode:         utils.MD5(utils.EXCEL_DATA_PREFIX + "_" + timestamp),
+			ExcelClassifyId:    req.ClassifyId,
+			SysUserId:          sysUser.AdminId,
+			SysUserRealName:    sysUser.RealName,
+			Content:            excelContent,
+			ExtraConfig:        extraConfig,
+			UpdateUserId:       sysUser.AdminId,
+			UpdateUserRealName: sysUser.RealName,
+			ModifyTime:         time.Now(),
+			CreateTime:         time.Now(),
+		}
+		if e := newExcel.Create(); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("新增表格失败, %v", e)
+			return
+		}
+		excelId = newExcel.ExcelInfoId
+	}
+
+	// 更新表格
+	if req.ExcelInfoId > 0 {
+		excelItem, e := excel.GetExcelInfoById(req.ExcelInfoId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "表格不存在, 请刷新页面"
+				return
+			}
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+			return
+		}
+		excelItem.ExcelName = req.TableName
+		excelItem.ExcelClassifyId = req.ClassifyId
+		excelItem.Content = excelContent
+		excelItem.ExtraConfig = extraConfig
+		excelItem.UpdateUserId = sysUser.AdminId
+		excelItem.UpdateUserRealName = sysUser.RealName
+		excelItem.ModifyTime = time.Now()
+		updateCols := []string{"ExcelName", "ExcelClassifyId", "ExtraConfig", "Content", "UpdateUserId", "UpdateUserRealName", "ModifyTime"}
+		if e = excelItem.Update(updateCols); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("更新表格信息失败, %v", e)
+			return
+		}
+		excelId = excelItem.ExcelInfoId
+	}
+	if excelId <= 0 {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格ID异常: %d", excelId)
+		return
+	}
+
+	br.Data = excelId
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Detail
+// @Title 表格详情
+// @Description 表格详情
+// @Param   ExcelInfoId  query  int  true  "表格ID"
+// @Success 200 {object} response.TableResp
+// @router /correlation/detail [get]
+func (this *TradeAnalysisCorrelationController) Detail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	excelId, _ := this.GetInt("ExcelInfoId", 0)
+	if excelId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID不可为空, ID: %d", excelId)
+		return
+	}
+
+	// 获取表格信息
+	excelItem, e := excel.GetExcelInfoById(excelId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "表格不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+	if excelItem.Source != utils.TRADE_ANALYSIS_CORRELATION_TABLE {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("表格类型异常, Source: %d", excelItem.Source)
+		return
+	}
+	var tableConfig tradeAnalysisModel.CorrelationTableExtraConfig
+	if e = json.Unmarshal([]byte(excelItem.ExtraConfig), &tableConfig); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+		return
+	}
+
+	// 根据配置获取表格行数据
+	tableRows := make([]*tradeAnalysisModel.CorrelationTableData, 0)
+	if excelItem.Content != "" {
+		if e = json.Unmarshal([]byte(excelItem.Content), &tableRows); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+			return
+		}
+	} else {
+		// 根据配置获取表格行数据
+		tableRows, e = tradeAnalysisService.GetCorrelationTableRowsDataByConfig(tableConfig)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+			return
+		}
+	}
+
+	// 标的指标名称
+	if tableConfig.BaseEdbInfoId > 0 {
+		baseEdb, e := data_manage.GetEdbInfoById(tableConfig.BaseEdbInfoId)
+		if e != nil && e.Error() != utils.ErrNoRow() {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取标的指标失败, %v", e)
+			return
+		}
+		if baseEdb != nil {
+			tableConfig.BaseEdbName = baseEdb.EdbName
+		}
+	}
+
+	// 响应
+	resp := new(tradeAnalysisResponse.CorrelationTableResp)
+	resp.ExcelInfoId = excelItem.ExcelInfoId
+	resp.TableName = excelItem.ExcelName
+	resp.ClassifyId = excelItem.ExcelClassifyId
+	resp.ModifyTime = utils.TimeTransferString(utils.FormatDateTime, excelItem.ModifyTime)
+	resp.TableConfig = tableConfig
+	resp.TableData = tableRows
+	resp.Button = services.GetTradeAnalysisTableOpButton(excelItem.SysUserId, sysUser.AdminId, sysUser.RoleTypeCode, false)
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// SaveAs
+// @Title 表格另存为
+// @Description 表格另存为
+// @Param	request	body request.CorrelationTableSaveAsReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /correlation/save_as [post]
+func (this *TradeAnalysisCorrelationController) SaveAs() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.CorrelationTableSaveAsReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	req.TableName = strings.TrimSpace(req.TableName)
+	if req.TableName == "" {
+		br.Msg = "请输入表格名称"
+		return
+	}
+	if req.ClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+
+	// 原表格
+	excelItem, e := excel.GetExcelInfoById(req.ExcelInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "原表格不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 新表格
+	timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) + "_" + utils.GetRandString(10)
+	newExcel := &excel.ExcelInfo{
+		ExcelName:          req.TableName,
+		Source:             utils.TRADE_ANALYSIS_CORRELATION_TABLE,
+		ExcelType:          1,
+		UniqueCode:         utils.MD5(utils.EXCEL_DATA_PREFIX + "_" + timestamp),
+		ExcelClassifyId:    req.ClassifyId,
+		SysUserId:          sysUser.AdminId,
+		SysUserRealName:    sysUser.RealName,
+		Content:            excelItem.Content,
+		ExtraConfig:        excelItem.ExtraConfig,
+		UpdateUserId:       sysUser.AdminId,
+		UpdateUserRealName: sysUser.RealName,
+		ModifyTime:         time.Now(),
+		CreateTime:         time.Now(),
+	}
+	if e := newExcel.Create(); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("新增表格失败, %v", e)
+		return
+	}
+
+	br.Data = newExcel.ExcelInfoId
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Refresh
+// @Title 刷新表格
+// @Description 刷新表格
+// @Param	request	body request.CorrelationTableRefreshReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /correlation/refresh [post]
+func (this *TradeAnalysisCorrelationController) Refresh() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.CorrelationTableRefreshReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	if req.ExcelInfoId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID有误: %d", req.ExcelInfoId)
+		return
+	}
+
+	cacheKey := fmt.Sprintf("%s_%d", utils.CACHE_EXCEL_REFRESH, req.ExcelInfoId)
+	if utils.Rc.IsExist(cacheKey) {
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "系统处理中,请勿频繁操作"
+		return
+	}
+	utils.Rc.SetNX(cacheKey, 1, 2*time.Minute)
+	defer func() {
+		_ = utils.Rc.Delete(cacheKey)
+	}()
+
+	// 获取表格信息
+	item, e := excel.GetExcelInfoById(req.ExcelInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "表格不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 获取表格内容
+	var tableConfig tradeAnalysisModel.CorrelationTableExtraConfig
+	if item.ExtraConfig == "" {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格配置为空, ExcelId: %d", item.ExcelInfoId)
+		return
+	}
+	if e = json.Unmarshal([]byte(item.ExtraConfig), &tableConfig); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格配置解析失败, ExcelId: %d, Err: %v", item.ExcelInfoId, e)
+		return
+	}
+	tableData, e := tradeAnalysisService.GetCorrelationTableRowsDataByConfig(tableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格数据失败, %v", e)
+		return
+	}
+	content, e := json.Marshal(tableData)
+	if e != nil {
+		br.Msg = "表格数据JSON格式化失败"
+		br.ErrMsg = fmt.Sprintf("表格数据JSON格式化失败, %v", e)
+		return
+	}
+	item.Content = string(content)
+
+	// 更新内容
+	updateCols := []string{"Content", "ModifyTime"}
+	if e = item.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("更新表格数据失败, %v", e)
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Remove
+// @Title 删除表格
+// @Description 删除表格
+// @Param	request	body request.CorrelationTableRemoveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /correlation/remove [post]
+func (this *TradeAnalysisCorrelationController) Remove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.CorrelationTableRemoveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	if req.ExcelInfoId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID有误: %d", req.ExcelInfoId)
+		return
+	}
+
+	// 获取表格信息
+	excelItem, e := excel.GetExcelInfoById(req.ExcelInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "操作成功"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 软删
+	excelItem.IsDelete = 1
+	excelItem.UpdateUserId = sysUser.AdminId
+	excelItem.UpdateUserRealName = sysUser.RealName
+	excelItem.ModifyTime = time.Now()
+	updateCols := []string{"IsDelete", "UpdateUserId", "UpdateUserRealName", "ModifyTime"}
+	if e = excelItem.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("删除表格信息失败, %v", e)
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Download
+// @Title 下载表格
+// @Description 下载表格
+// @Param   ExcelInfoId  query  int  true  "表格ID"
+// @Success 200  下载成功
+// @router /correlation/download [get]
+func (this *TradeAnalysisCorrelationController) Download() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+			utils.FileLog.Info(fmt.Sprintf("多空分析表格-下载失败, ErrMsg: %s", br.ErrMsg))
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	excelId, _ := this.GetInt("ExcelInfoId")
+	if excelId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID有误: %d", excelId)
+		return
+	}
+
+	// 获取表格信息
+	excelItem, e := excel.GetExcelInfoById(excelId)
+	if e != nil {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 根据配置获取表格行数据
+	tableRows := make([]*tradeAnalysisModel.CorrelationTableData, 0)
+	if excelItem.Content != "" {
+		if e = json.Unmarshal([]byte(excelItem.Content), &tableRows); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+			return
+		}
+	} else {
+		// 根据配置获取表格行数据
+		var tableConfig tradeAnalysisModel.CorrelationTableExtraConfig
+		if e = json.Unmarshal([]byte(excelItem.ExtraConfig), &tableConfig); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+			return
+		}
+		tableRows, e = tradeAnalysisService.GetCorrelationTableRowsDataByConfig(tableConfig)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+			return
+		}
+	}
+
+	// 生成excel
+	dir, _ := os.Executable()
+	exPath := filepath.Dir(dir)
+	downFile := exPath + "/" + time.Now().Format(utils.FormatDateTimeUnSpace) + ".xlsx"
+	xlsxFile := xlsx.NewFile()
+
+	sheetNew := new(xlsx.Sheet)
+	sheetNew, e = xlsxFile.AddSheet("Sheet1")
+	if e != nil {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("生成Sheet失败, %v", e)
+		return
+	}
+
+	// 第一行-表头
+	headerRow := sheetNew.AddRow()
+	headerRow.AddCell().SetString("与标的指标的相关性矩阵")
+	for _, v := range tableRows {
+		cell := headerRow.AddCell()
+		if len(v.RowsData) == 0 {
+			continue
+		}
+		// TODO:向右合并-1到-10天数, 合并不知道为啥不生效...先占位处理吧
+		mergeNum := len(v.RowsData[0].DayData) - 1
+		cell.HMerge = mergeNum
+		for i := 0; i < mergeNum; i++ {
+			headerRow.AddCell()
+		}
+		cell.Value = fmt.Sprintf("滚动相关性(%d交易日)", v.CalculateValue)
+	}
+
+	// 第二行
+	secondRow := sheetNew.AddRow()
+	secondRow.AddCell().SetString("合约持仓")
+	for _, v := range tableRows {
+		if len(v.RowsData) == 0 {
+			continue
+		}
+		rowData := v.RowsData[0]
+		for _, d := range rowData.DayData {
+			showDay := strconv.Itoa(d.Day)
+			if d.Day == 0 {
+				showDay = "最新"
+			}
+			secondRow.AddCell().SetString(showDay)
+		}
+	}
+
+	// 数据行, 把多个表格的行数据合并到一起
+	var sortIndex []int // 仅用于排序
+	indexRowsData := make(map[int]*tradeAnalysisModel.CorrelationTableRowData, 0)
+	for _, v := range tableRows {
+		for rk, rd := range v.RowsData {
+			if indexRowsData[rk] == nil {
+				indexRowsData[rk] = rd
+				sortIndex = append(sortIndex, rk)
+				continue
+			}
+			indexRowsData[rk].DayData = append(indexRowsData[rk].DayData, rd.DayData...)
+		}
+	}
+	for k := range sortIndex {
+		rd := indexRowsData[k]
+		if rd == nil {
+			continue
+		}
+		row := sheetNew.AddRow()
+		row.AddCell().SetString(rd.RowName)
+		for _, dd := range rd.DayData {
+			row.AddCell().SetString(fmt.Sprintf("%.2f", dd.DataVal))
+		}
+	}
+
+	// 保存文件
+	if e = xlsxFile.Save(downFile); e != nil {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("保存文件内容失败, %v", e)
+		return
+	}
+	defer func() {
+		_ = os.Remove(downFile)
+	}()
+	fileName := fmt.Sprintf(`%s%s.xlsx`, excelItem.ExcelName, time.Now().Format("06.01.02"))
+	this.Ctx.Output.Download(downFile, fileName)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "下载成功"
+}

+ 842 - 0
controllers/trade_analysis/trade_analysis_table.go

@@ -0,0 +1,842 @@
+package trade_analysis
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/data_manage/excel"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	tradeAnalysisRequest "eta/eta_api/models/data_manage/trade_analysis/request"
+	tradeAnalysisResponse "eta/eta_api/models/data_manage/trade_analysis/response"
+	"eta/eta_api/services"
+	tradeAnalysisService "eta/eta_api/services/data/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/tealeg/xlsx"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// TradeAnalysisTableController 多空分析表格
+type TradeAnalysisTableController struct {
+	controllers.BaseAuthController
+}
+
+// Preview
+// @Title 表格预览
+// @Description 表格预览
+// @Param	request	body request.TablePreviewReq true "type json string"
+// @Success 200 {object} response.TableResp
+// @router /table/preview [post]
+func (this *TradeAnalysisTableController) Preview() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.TablePreviewReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	// 校验配置
+	pass, tips := tradeAnalysisService.CheckAnalysisTableExtraConfig(req.TableConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+
+	// 根据配置获取表格行数据
+	tableRows, e := tradeAnalysisService.GetTableRowsDataByConfig(req.TableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+		return
+	}
+
+	// 响应
+	resp := new(tradeAnalysisResponse.TableResp)
+	resp.TableData = tableRows
+	resp.TableName = strings.TrimSpace(req.TableName)
+	resp.ClassifyId = req.ClassifyId
+	resp.TableConfig = req.TableConfig
+	resp.TableCols = make([]*tradeAnalysisModel.TradeAnalysisTableColumnItem, 0)
+	if len(req.TableCols) > 0 {
+		resp.TableCols = req.TableCols
+	}
+	if len(req.TableCols) == 0 {
+		// 获取默认的表头信息
+		colOb := new(tradeAnalysisModel.TradeAnalysisTableColumn)
+		cond := fmt.Sprintf(` AND %s = 0`, colOb.Cols().ExcelInfoId)
+		defaultCols, e := colOb.GetItemsByCondition(cond, make([]interface{}, 0), []string{}, fmt.Sprintf("%s ASC", colOb.Cols().Sort))
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取默认表头设置失败, %v", e)
+			return
+		}
+		for _, v := range defaultCols {
+			resp.TableCols = append(resp.TableCols, v.Format2Item())
+		}
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Save
+// @Title 保存表格
+// @Description 保存表格
+// @Param	request	body request.TableSaveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /table/save [post]
+func (this *TradeAnalysisTableController) Save() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.TableSaveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	// 校验配置
+	pass, tips := tradeAnalysisService.CheckAnalysisTableExtraConfig(req.TableConfig)
+	if !pass {
+		br.Msg = tips
+		return
+	}
+	req.TableName = strings.TrimSpace(req.TableName)
+	if req.TableName == "" {
+		br.Msg = "请输入表格名称"
+		return
+	}
+	if req.ClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+	configByte, e := json.Marshal(req.TableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格配置JSON格式化失败, %v", e)
+		return
+	}
+	extraConfig := string(configByte)
+
+	// 表格数据
+	tableRows, e := tradeAnalysisService.GetTableRowsDataByConfig(req.TableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+		return
+	}
+	contentByte, e := json.Marshal(tableRows)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格数据JSON格式化失败, %v", e)
+		return
+	}
+	excelContent := string(contentByte)
+
+	// 新增表格
+	var excelId int
+	if req.ExcelInfoId <= 0 {
+		timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) + "_" + utils.GetRandString(10)
+		newExcel := &excel.ExcelInfo{
+			ExcelName:          req.TableName,
+			Source:             utils.TRADE_ANALYSIS_TABLE,
+			ExcelType:          1,
+			UniqueCode:         utils.MD5(utils.EXCEL_DATA_PREFIX + "_" + timestamp),
+			ExcelClassifyId:    req.ClassifyId,
+			SysUserId:          sysUser.AdminId,
+			SysUserRealName:    sysUser.RealName,
+			Content:            excelContent,
+			ExtraConfig:        extraConfig,
+			UpdateUserId:       sysUser.AdminId,
+			UpdateUserRealName: sysUser.RealName,
+			ModifyTime:         time.Now(),
+			CreateTime:         time.Now(),
+		}
+		if e := newExcel.Create(); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("新增表格失败, %v", e)
+			return
+		}
+		excelId = newExcel.ExcelInfoId
+	}
+
+	// 更新表格
+	if req.ExcelInfoId > 0 {
+		excelItem, e := excel.GetExcelInfoById(req.ExcelInfoId)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "表格不存在, 请刷新页面"
+				return
+			}
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+			return
+		}
+		excelItem.ExcelName = req.TableName
+		excelItem.ExcelClassifyId = req.ClassifyId
+		excelItem.Content = excelContent
+		excelItem.ExtraConfig = extraConfig
+		excelItem.UpdateUserId = sysUser.AdminId
+		excelItem.UpdateUserRealName = sysUser.RealName
+		excelItem.ModifyTime = time.Now()
+		updateCols := []string{"ExcelName", "ExcelClassifyId", "ExtraConfig", "Content", "UpdateUserId", "UpdateUserRealName", "ModifyTime"}
+		if e = excelItem.Update(updateCols); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("更新表格信息失败, %v", e)
+			return
+		}
+		excelId = excelItem.ExcelInfoId
+	}
+	if excelId <= 0 {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格ID异常: %d", excelId)
+		return
+	}
+
+	// 表头信息
+	columnOb := new(tradeAnalysisModel.TradeAnalysisTableColumn)
+	configColumns := make([]*tradeAnalysisModel.TradeAnalysisTableColumn, 0)
+	if len(req.TableCols) > 0 {
+		for _, v := range req.TableCols {
+			t := new(tradeAnalysisModel.TradeAnalysisTableColumn)
+			t.ExcelInfoId = excelId
+			t.ColumnKey = v.ColumnKey
+			t.ColumnName = v.ColumnName
+			t.ColumnNameEn = v.ColumnNameEn
+			t.Sort = v.Sort
+			t.IsMust = v.IsMust
+			t.IsShow = v.IsShow
+			t.IsSort = v.IsSort
+			t.CreateTime = time.Now()
+			t.ModifyTime = time.Now()
+			configColumns = append(configColumns, t)
+		}
+	} else {
+		// 获取默认的表头信息
+		cond := fmt.Sprintf(` AND %s = 0`, columnOb.Cols().ExcelInfoId)
+		defaultCols, e := columnOb.GetItemsByCondition(cond, make([]interface{}, 0), []string{}, fmt.Sprintf("%s ASC", columnOb.Cols().Sort))
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取默认表头设置失败, %v", e)
+			return
+		}
+		for _, v := range defaultCols {
+			v.Id = 0
+			v.ExcelInfoId = excelId
+			v.CreateTime = time.Now()
+			v.ModifyTime = time.Now()
+			configColumns = append(configColumns, v)
+		}
+	}
+
+	// 删除并新增表头
+	{
+		cond := fmt.Sprintf(`%s = ?`, columnOb.Cols().ExcelInfoId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, excelId)
+		if e = columnOb.RemoveByCondition(cond, pars); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("删除原表头配置失败, %v", e)
+			return
+		}
+		if e = columnOb.CreateMulti(configColumns); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("批量新增表头配置失败, %v", e)
+			return
+		}
+	}
+
+	br.Data = excelId
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Detail
+// @Title 表格详情
+// @Description 表格详情
+// @Param   ExcelInfoId  query  int  true  "表格ID"
+// @Success 200 {object} response.TableResp
+// @router /table/detail [get]
+func (this *TradeAnalysisTableController) Detail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	excelId, _ := this.GetInt("ExcelInfoId", 0)
+	if excelId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID不可为空, ID: %d", excelId)
+		return
+	}
+
+	// 获取表格信息
+	excelItem, e := excel.GetExcelInfoById(excelId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "表格不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+	if excelItem.Source != utils.TRADE_ANALYSIS_TABLE {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("表格类型异常, Source: %d", excelItem.Source)
+		return
+	}
+	var tableConfig tradeAnalysisModel.TableExtraConfig
+	if e = json.Unmarshal([]byte(excelItem.ExtraConfig), &tableConfig); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+		return
+	}
+
+	// 表格数据
+	tableRows := make([]*tradeAnalysisModel.TableRowData, 0)
+	if excelItem.Content != "" {
+		if e = json.Unmarshal([]byte(excelItem.Content), &tableRows); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+			return
+		}
+	} else {
+		// 根据配置获取表格行数据
+		tableRows, e = tradeAnalysisService.GetTableRowsDataByConfig(tableConfig)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+			return
+		}
+	}
+
+	// 响应
+	resp := new(tradeAnalysisResponse.TableResp)
+	resp.ExcelInfoId = excelItem.ExcelInfoId
+	resp.TableData = tableRows
+	resp.TableName = strings.TrimSpace(excelItem.ExcelName)
+	resp.ClassifyId = excelItem.ExcelClassifyId
+	resp.ModifyTime = utils.TimeTransferString(utils.FormatDateTime, excelItem.ModifyTime)
+	resp.TableConfig = tableConfig
+	resp.TableCols = make([]*tradeAnalysisModel.TradeAnalysisTableColumnItem, 0)
+	// 获取表头信息
+	{
+		colOb := new(tradeAnalysisModel.TradeAnalysisTableColumn)
+		cond := fmt.Sprintf(" AND %s = ?", colOb.Cols().ExcelInfoId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, excelId)
+		configCols, e := colOb.GetItemsByCondition(cond, pars, []string{}, fmt.Sprintf("%s ASC", colOb.Cols().Sort))
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取表格表头设置失败, %v", e)
+			return
+		}
+		// 表头信息为空
+		if len(configCols) == 0 {
+			defaultCols, e := colOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, fmt.Sprintf("%s ASC", colOb.Cols().Sort))
+			if e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("获取默认表头设置失败, %v", e)
+				return
+			}
+			configCols = defaultCols
+		}
+		for _, v := range configCols {
+			resp.TableCols = append(resp.TableCols, v.Format2Item())
+		}
+	}
+	resp.Button = services.GetTradeAnalysisTableOpButton(excelItem.SysUserId, sysUser.AdminId, sysUser.RoleTypeCode, false)
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// SaveAs
+// @Title 表格另存为
+// @Description 表格另存为
+// @Param	request	body request.TableSaveAsReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /table/save_as [post]
+func (this *TradeAnalysisTableController) SaveAs() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.TableSaveAsReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	req.TableName = strings.TrimSpace(req.TableName)
+	if req.TableName == "" {
+		br.Msg = "请输入表格名称"
+		return
+	}
+	if req.ClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+
+	// 原表格
+	excelItem, e := excel.GetExcelInfoById(req.ExcelInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "原表格不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 新表格
+	timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) + "_" + utils.GetRandString(10)
+	newExcel := &excel.ExcelInfo{
+		ExcelName:          req.TableName,
+		Source:             utils.TRADE_ANALYSIS_TABLE,
+		ExcelType:          1,
+		UniqueCode:         utils.MD5(utils.EXCEL_DATA_PREFIX + "_" + timestamp),
+		ExcelClassifyId:    req.ClassifyId,
+		SysUserId:          sysUser.AdminId,
+		SysUserRealName:    sysUser.RealName,
+		Content:            excelItem.Content,
+		ExtraConfig:        excelItem.ExtraConfig,
+		UpdateUserId:       sysUser.AdminId,
+		UpdateUserRealName: sysUser.RealName,
+		ModifyTime:         time.Now(),
+		CreateTime:         time.Now(),
+	}
+	if e := newExcel.Create(); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("新增表格失败, %v", e)
+		return
+	}
+
+	// 表头信息
+	columnOb := new(tradeAnalysisModel.TradeAnalysisTableColumn)
+	configColumns := make([]*tradeAnalysisModel.TradeAnalysisTableColumn, 0)
+	{
+		cond := fmt.Sprintf(` AND %s = ?`, columnOb.Cols().ExcelInfoId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, req.ExcelInfoId)
+		originColumns, e := columnOb.GetItemsByCondition(cond, pars, []string{}, fmt.Sprintf("%s ASC", columnOb.Cols().Sort))
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取默认表头设置失败, %v", e)
+			return
+		}
+		for _, v := range originColumns {
+			v.Id = 0
+			v.ExcelInfoId = newExcel.ExcelInfoId
+			v.CreateTime = time.Now()
+			v.ModifyTime = time.Now()
+			configColumns = append(configColumns, v)
+		}
+	}
+	// 没找到就获取默认的表头信息
+	if len(configColumns) == 0 {
+		cond := fmt.Sprintf(` AND %s = 0`, columnOb.Cols().ExcelInfoId)
+		defaultCols, e := columnOb.GetItemsByCondition(cond, make([]interface{}, 0), []string{}, fmt.Sprintf("%s ASC", columnOb.Cols().Sort))
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取默认表头设置失败, %v", e)
+			return
+		}
+		for _, v := range defaultCols {
+			v.Id = 0
+			v.ExcelInfoId = newExcel.ExcelInfoId
+			v.CreateTime = time.Now()
+			v.ModifyTime = time.Now()
+			configColumns = append(configColumns, v)
+		}
+	}
+	// 新增表头
+	if e = columnOb.CreateMulti(configColumns); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("批量新增表头配置失败, %v", e)
+		return
+	}
+
+	br.Data = newExcel.ExcelInfoId
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Refresh
+// @Title 刷新表格
+// @Description 刷新表格
+// @Param	request	body request.TableRefreshReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /table/refresh [post]
+func (this *TradeAnalysisTableController) Refresh() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.TableRefreshReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	if req.ExcelInfoId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID有误: %d", req.ExcelInfoId)
+		return
+	}
+
+	cacheKey := fmt.Sprintf("%s_%d", utils.CACHE_EXCEL_REFRESH, req.ExcelInfoId)
+	if utils.Rc.IsExist(cacheKey) {
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "系统处理中,请勿频繁操作"
+		return
+	}
+	utils.Rc.SetNX(cacheKey, 1, 2*time.Minute)
+	defer func() {
+		_ = utils.Rc.Delete(cacheKey)
+	}()
+
+	// 获取表格信息
+	item, e := excel.GetExcelInfoById(req.ExcelInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "表格不存在, 请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 获取表格内容
+	var tableConfig tradeAnalysisModel.TableExtraConfig
+	if item.ExtraConfig == "" {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格配置为空, ExcelId: %d", item.ExcelInfoId)
+		return
+	}
+	if e = json.Unmarshal([]byte(item.ExtraConfig), &tableConfig); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("表格配置解析失败, ExcelId: %d, Err: %v", item.ExcelInfoId, e)
+		return
+	}
+	tableData, e := tradeAnalysisService.GetTableRowsDataByConfig(tableConfig)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+		return
+	}
+	content, e := json.Marshal(tableData)
+	if e != nil {
+		br.Msg = "表格数据JSON格式化失败"
+		br.ErrMsg = fmt.Sprintf("表格数据JSON格式化失败, %v", e)
+		return
+	}
+	item.Content = string(content)
+
+	// 更新内容
+	updateCols := []string{"Content", "ModifyTime"}
+	if e = item.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("更新表格数据失败, %v", e)
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Remove
+// @Title 删除表格
+// @Description 删除表格
+// @Param	request	body request.TableRemoveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /table/remove [post]
+func (this *TradeAnalysisTableController) Remove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req tradeAnalysisRequest.TableRemoveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析异常: %v", e)
+		return
+	}
+	if req.ExcelInfoId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID有误: %d", req.ExcelInfoId)
+		return
+	}
+
+	// 获取表格信息
+	excelItem, e := excel.GetExcelInfoById(req.ExcelInfoId)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "操作成功"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 软删
+	excelItem.IsDelete = 1
+	excelItem.UpdateUserId = sysUser.AdminId
+	excelItem.UpdateUserRealName = sysUser.RealName
+	excelItem.ModifyTime = time.Now()
+	updateCols := []string{"IsDelete", "UpdateUserId", "UpdateUserRealName", "ModifyTime"}
+	if e = excelItem.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("删除表格信息失败, %v", e)
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Download
+// @Title 下载表格
+// @Description 下载表格
+// @Param   ExcelInfoId  query  int  true  "表格ID"
+// @Success 200  下载成功
+// @router /table/download [get]
+func (this *TradeAnalysisTableController) Download() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+			utils.FileLog.Info(fmt.Sprintf("多空分析表格-下载失败, ErrMsg: %s", br.ErrMsg))
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	excelId, _ := this.GetInt("ExcelInfoId")
+	if excelId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("表格ID有误: %d", excelId)
+		return
+	}
+
+	// 获取表格信息
+	excelItem, e := excel.GetExcelInfoById(excelId)
+	if e != nil {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("获取表格信息失败, %v", e)
+		return
+	}
+
+	// 获取表头信息
+	configCols := make([]*tradeAnalysisModel.TradeAnalysisTableColumn, 0)
+	{
+		colOb := new(tradeAnalysisModel.TradeAnalysisTableColumn)
+		cond := fmt.Sprintf(" AND %s = ?", colOb.Cols().ExcelInfoId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, excelId)
+		items, e := colOb.GetItemsByCondition(cond, pars, []string{}, fmt.Sprintf("%s ASC", colOb.Cols().Sort))
+		if e != nil {
+			br.Msg = "下载失败"
+			br.ErrMsg = fmt.Sprintf("获取表格表头设置失败, %v", e)
+			return
+		}
+		configCols = items
+		// 表头信息为空
+		if len(configCols) == 0 {
+			defaultCols, e := colOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, fmt.Sprintf("%s ASC", colOb.Cols().Sort))
+			if e != nil {
+				br.Msg = "下载失败"
+				br.ErrMsg = fmt.Sprintf("获取默认表头设置失败, %v", e)
+				return
+			}
+			configCols = defaultCols
+		}
+	}
+	if len(configCols) == 0 {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("表头信息异常")
+		return
+	}
+
+	// 表格数据
+	tableRows := make([]*tradeAnalysisModel.TableRowData, 0)
+	var tableConfig tradeAnalysisModel.TableExtraConfig
+	if excelItem.Content != "" {
+		if e = json.Unmarshal([]byte(excelItem.Content), &tableRows); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+			return
+		}
+	} else {
+		// 根据配置获取表格行数据
+		if e = json.Unmarshal([]byte(excelItem.ExtraConfig), &tableConfig); e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("解析表格配置失败, err: %v, config: %s", e, excelItem.ExtraConfig)
+			return
+		}
+		tableRows, e = tradeAnalysisService.GetTableRowsDataByConfig(tableConfig)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取表格行数据失败, %v", e)
+			return
+		}
+	}
+
+	// 生成excel
+	dir, _ := os.Executable()
+	exPath := filepath.Dir(dir)
+	downFile := exPath + "/" + time.Now().Format(utils.FormatDateTimeUnSpace) + ".xlsx"
+	xlsxFile := xlsx.NewFile()
+
+	sheetNew := new(xlsx.Sheet)
+	sheetNew, e = xlsxFile.AddSheet("Sheet1")
+	if e != nil {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("生成Sheet失败, %v", e)
+		return
+	}
+
+	// 第一行-表头
+	headerRow := sheetNew.AddRow()
+	var headerKeys []string
+	for _, v := range configCols {
+		if v.IsShow == 0 {
+			continue
+		}
+		headerRow.AddCell().SetValue(v.ColumnName)
+		headerKeys = append(headerKeys, v.ColumnKey)
+	}
+
+	// 数据行
+	for _, v := range tableRows {
+		row := sheetNew.AddRow()
+		rv := reflect.ValueOf(v)
+
+		// v为指针先解引用
+		if rv.Kind() == reflect.Ptr {
+			rv = rv.Elem()
+		}
+
+		// 获取对应key的值
+		for _, h := range headerKeys {
+			fieldVal := rv.FieldByName(h)
+			if fieldVal.IsValid() {
+				cell := row.AddCell()
+				cell.SetString(fmt.Sprint(fieldVal.Interface()))
+				continue
+			}
+			row.AddCell().SetString("")
+		}
+	}
+
+	// 保存文件
+	if e = xlsxFile.Save(downFile); e != nil {
+		br.Msg = "下载失败"
+		br.ErrMsg = fmt.Sprintf("保存文件内容失败, %v", e)
+		return
+	}
+	fileName := fmt.Sprintf(`%s%s.xlsx`, excelItem.ExcelName, time.Now().Format("06.01.02"))
+	this.Ctx.Output.Download(downFile, fileName)
+	defer func() {
+		_ = os.Remove(downFile)
+	}()
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "下载成功"
+}

+ 2 - 2
controllers/trade_analysis/warehouse.go

@@ -220,7 +220,7 @@ func (this *WarehouseController) Preview() {
 	}
 
 	// 获取指标数据, 该图表未用实际指标, 为了统一数据格式用ChartEdbInfoMapping
-	companyTradeData, e := tradeAnalysisService.GetOriginTradeData(extraConfig.Exchange, extraConfig.ClassifyName, extraConfig.Contracts, extraConfig.Companies, extraConfig.PredictRatio)
+	companyTradeData, e := tradeAnalysisService.GetWarehouseTradeData(extraConfig.Exchange, extraConfig.ClassifyName, extraConfig.Contracts, extraConfig.Companies, extraConfig.PredictRatio)
 	if e != nil {
 		br.Msg = "获取失败"
 		br.ErrMsg = fmt.Sprintf("获取期货公司持仓加总数据失败, %v", e)
@@ -687,7 +687,7 @@ func (this *WarehouseController) Detail() {
 	}
 
 	// 获取图表数据
-	companyTradeData, e := tradeAnalysisService.GetOriginTradeData(extraConfig.Exchange, extraConfig.ClassifyName, extraConfig.Contracts, extraConfig.Companies, extraConfig.PredictRatio)
+	companyTradeData, e := tradeAnalysisService.GetWarehouseTradeData(extraConfig.Exchange, extraConfig.ClassifyName, extraConfig.Contracts, extraConfig.Companies, extraConfig.PredictRatio)
 	if e != nil {
 		br.Msg = "获取失败"
 		br.ErrMsg = fmt.Sprintf("获取期货公司持仓加总数据失败, %v", e)

+ 80 - 0
models/common/common_classify.go

@@ -0,0 +1,80 @@
+package common
+
+import "time"
+
+// CommonClassify 通用分类
+type CommonClassify struct {
+	ClassifyId   int       `description:"分类ID"`
+	ClassifyName string    `description:"分类名称"`
+	ParentId     int       `description:"父级ID"`
+	RootId       int       `description:"顶级ID"`
+	Level        int       `description:"层级"`
+	LevelPath    string    `description:"层级路径"`
+	Sort         int       `description:"排序"`
+	CreateTime   time.Time `description:"创建时间"`
+	ModifyTime   time.Time `description:"修改时间"`
+}
+
+// CommonClassifyCols 通用分类基本字段
+type CommonClassifyCols struct {
+	ClassifyId   string `description:"分类ID"`
+	ClassifyName string `description:"分类名称"`
+	ParentId     string `description:"父级id"`
+	RootId       string `description:"顶级id"`
+	Level        string `description:"层级"`
+	LevelPath    string `description:"层级路径"`
+	Sort         string `description:"排序字段,越小越靠前,默认值:10"`
+	CreateTime   string `description:"创建时间"`
+	ModifyTime   string `description:"修改时间"`
+}
+
+// CommonClassifyObj 通用分类对象
+type CommonClassifyObj struct {
+	ObjectId   int       `description:"对象ID"`
+	ClassifyId int       `description:"分类ID"`
+	Sort       int       `description:"排序"`
+	CreateTime time.Time `description:"创建时间"`
+	ModifyTime time.Time `description:"修改时间"`
+}
+
+// CommonClassifyObjCols 通用分类对象基本字段
+type CommonClassifyObjCols struct {
+	ObjectId   string `description:"对象ID"`
+	ClassifyId string `description:"分类ID"`
+	Sort       string `description:"排序"`
+	CreateTime string `description:"创建时间"`
+	ModifyTime string `description:"修改时间"`
+}
+
+// CommonClassifyMoveReq 移动分类
+type CommonClassifyMoveReq struct {
+	ClassifyId       int `description:"分类ID"`
+	ParentClassifyId int `description:"父级分类ID"`
+	PrevClassifyId   int `description:"上一个兄弟节点分类ID"`
+	NextClassifyId   int `description:"下一个兄弟节点分类ID"`
+	ObjectId         int `description:"对象ID(指标/图表..), 如果对象ID>0则移动对象, 否则认为移动分类"`
+	PrevObjectId     int `description:"上一个对象ID"`
+	NextObjectId     int `description:"下一个对象ID"`
+}
+
+// ExtraPermissionClassifyStrategy 是一个带有额外权限校验的装饰器
+//type ExtraPermissionClassifyStrategy struct {
+//	BaseClassifyStrategy
+//}
+
+// UpdateCommonClassify 覆盖基础策略的UpdateClassify方法,并添加额外的权限校验
+//func (s *ExtraPermissionClassifyStrategy) UpdateCommonClassify(classify *CommonClassify) error {
+//	// 额外的权限校验
+//	if !checkExtraPermission(classify) {
+//		return fmt.Errorf("无操作权限")
+//	}
+//
+//	// 调用基础策略的UpdateClassify方法
+//	return s.BaseClassifyStrategy.UpdateCommonClassify(classify)
+//}
+//
+//// checkExtraPermission 进行额外的权限校验
+//func checkExtraPermission(classify *CommonClassify) bool {
+//	// 实现额外权限校验逻辑
+//	return true
+//}

+ 18 - 0
models/data_manage/excel/excel_info.go

@@ -34,6 +34,7 @@ type ExcelInfo struct {
 	RelExcelInfoId     int       `description:"平衡表里静态表关联的动态表excel id"`
 	VersionName        string    `description:"静态表版本名称"`
 	SourcesFrom        string    `description:"图表来源"`
+	ExtraConfig        string    `description:"额外配置:如多空分析、相关性表格参数"`
 }
 
 // Update 更新 excel表格基础信息
@@ -814,3 +815,20 @@ func UpdateExcelInfoClassifyIdByIds(classifyId int, excelIds []int) (err error)
 	_, err = o.Raw(sql, classifyId, excelIds).Exec()
 	return
 }
+
+func (m *ExcelInfo) Create() (err error) {
+	o := orm.NewOrmUsingDB("data")
+	id, err := o.Insert(m)
+	if err != nil {
+		return
+	}
+	m.ExcelInfoId = int(id)
+	return
+}
+
+func (m *ExcelInfo) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM excel_info WHERE 1=1 %s`, condition)
+	err = o.Raw(sql, pars).QueryRow(&count)
+	return
+}

+ 10 - 0
models/data_manage/trade_analysis/base_from_trade_exchange.go

@@ -120,3 +120,13 @@ func (m *BaseFromTradeExchange) Format2Item() (item *BaseFromTradeExchangeItem)
 	item.Sort = m.Sort
 	return
 }
+
+// TradeExchangeClassifyNode 交易所-分类树节点
+type TradeExchangeClassifyNode struct {
+	UniqueFlag string                       `description:"唯一标识"`
+	ParentFlag string                       `description:"父级标识"`
+	NodeType   int                          `description:"类型: 1-交易所; 2-品种"`
+	NodeName   string                       `description:"节点名称"`
+	NodeValue  string                       `description:"节点实际值"`
+	Children   []*TradeExchangeClassifyNode `description:"子节点"`
+}

+ 35 - 0
models/data_manage/trade_analysis/request/trade_analysis_correlation.go

@@ -0,0 +1,35 @@
+package request
+
+import tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+
+// CorrelationTablePreviewReq 相关性表格预览
+type CorrelationTablePreviewReq struct {
+	TableName   string                                         `description:"表格名称"`
+	ClassifyId  int                                            `description:"分类ID"`
+	TableConfig tradeAnalysisModel.CorrelationTableExtraConfig `description:"表格参数"`
+}
+
+// CorrelationTableSaveReq 保存表格
+type CorrelationTableSaveReq struct {
+	ExcelInfoId int                                            `description:"表格ID"`
+	TableName   string                                         `description:"表格名称"`
+	ClassifyId  int                                            `description:"分类ID"`
+	TableConfig tradeAnalysisModel.CorrelationTableExtraConfig `description:"表格参数"`
+}
+
+// CorrelationTableSaveAsReq 表格另存为
+type CorrelationTableSaveAsReq struct {
+	ExcelInfoId int    `description:"表格ID"`
+	TableName   string `description:"表格名称"`
+	ClassifyId  int    `description:"分类ID"`
+}
+
+// CorrelationTableRefreshReq 刷新表格请求
+type CorrelationTableRefreshReq struct {
+	ExcelInfoId int `description:"表格ID"`
+}
+
+// CorrelationTableRemoveReq 删除表格请求
+type CorrelationTableRemoveReq struct {
+	ExcelInfoId int `description:"表格ID"`
+}

+ 37 - 0
models/data_manage/trade_analysis/request/trade_analysis_table.go

@@ -0,0 +1,37 @@
+package request
+
+import tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+
+// TablePreviewReq 预览表格
+type TablePreviewReq struct {
+	TableName   string                                             `description:"表格名称"`
+	ClassifyId  int                                                `description:"分类ID"`
+	TableConfig tradeAnalysisModel.TableExtraConfig                `description:"表格参数"`
+	TableCols   []*tradeAnalysisModel.TradeAnalysisTableColumnItem `description:"自定义表头"`
+}
+
+// TableSaveReq 保存表格
+type TableSaveReq struct {
+	ExcelInfoId int                                                `description:"表格ID"`
+	TableName   string                                             `description:"表格名称"`
+	ClassifyId  int                                                `description:"分类ID"`
+	TableConfig tradeAnalysisModel.TableExtraConfig                `description:"表格参数"`
+	TableCols   []*tradeAnalysisModel.TradeAnalysisTableColumnItem `description:"自定义表头"`
+}
+
+// TableSaveAsReq 表格另存为
+type TableSaveAsReq struct {
+	ExcelInfoId int    `description:"表格ID"`
+	TableName   string `description:"表格名称"`
+	ClassifyId  int    `description:"分类ID"`
+}
+
+// TableRefreshReq 刷新表格请求
+type TableRefreshReq struct {
+	ExcelInfoId int `description:"表格ID"`
+}
+
+// TableRemoveReq 删除表格请求
+type TableRemoveReq struct {
+	ExcelInfoId int `description:"表格ID"`
+}

+ 17 - 0
models/data_manage/trade_analysis/response/trade_analysis_correlation.go

@@ -0,0 +1,17 @@
+package response
+
+import (
+	excelModel "eta/eta_api/models/data_manage/excel"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+)
+
+// CorrelationTableResp 相关性表格响应
+type CorrelationTableResp struct {
+	ExcelInfoId int                                            `description:"表格ID"`
+	TableName   string                                         `description:"表格名称"`
+	ClassifyId  int                                            `description:"分类ID"`
+	ModifyTime  string                                         `description:"最近保存时间"`
+	TableConfig tradeAnalysisModel.CorrelationTableExtraConfig `description:"表格配置"`
+	TableData   []*tradeAnalysisModel.CorrelationTableData     `description:"表格数据"`
+	Button      excelModel.ExcelInfoDetailButton               `description:"表格的按钮权限"`
+}

+ 18 - 0
models/data_manage/trade_analysis/response/trade_analysis_table.go

@@ -0,0 +1,18 @@
+package response
+
+import (
+	excelModel "eta/eta_api/models/data_manage/excel"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+)
+
+// TableResp 表格响应
+type TableResp struct {
+	ExcelInfoId int                                                `description:"表格ID"`
+	TableName   string                                             `description:"表格名称"`
+	ClassifyId  int                                                `description:"分类ID"`
+	ModifyTime  string                                             `description:"最近保存时间"`
+	TableConfig tradeAnalysisModel.TableExtraConfig                `description:"表格配置"`
+	TableCols   []*tradeAnalysisModel.TradeAnalysisTableColumnItem `description:"表头"`
+	TableData   []*tradeAnalysisModel.TableRowData                 `description:"表格数据"`
+	Button      excelModel.ExcelInfoDetailButton                   `description:"表格的按钮权限"`
+}

+ 246 - 359
models/data_manage/trade_analysis/trade_analysis.go

@@ -4,10 +4,68 @@ import (
 	"eta/eta_api/utils"
 	"fmt"
 	"github.com/beego/beego/v2/client/orm"
+	"strings"
 	"time"
 )
 
-// 上期能源持仓榜单表
+const (
+	TradeDataTypeNull      = 0 // 无值
+	TradeDataTypeOrigin    = 1 // 原始值
+	TradeDataTypeCalculate = 2 // 推算值
+
+	WarehouseBuyChartType     = 1 // 多单图
+	WarehouseSoldChartType    = 2 // 空单图
+	WarehousePureBuyChartType = 3 // 净多单图
+
+	WarehouseDefaultUnit      = "手"
+	WarehouseDefaultFrequency = "日度"
+
+	GuangZhouTopCompanyAliasName = "日成交持仓排名" // 广期所TOP20对应的公司名称
+	GuangZhouSeatNameBuy         = "持买单量"    // 广期所指标名称中的多单名称
+	GuangZhouSeatNameSold        = "持卖单量"    // 广期所指标名称中的空单名称
+	GuangZhouTopSeatNameBuy      = "持买单量总计"  // 广期所指标名称中的TOP20多单名称
+	GuangZhouTopSeatNameSold     = "持卖单量总计"  // 广期所指标名称中的TOP20空单名称
+	GuangZhouTopSeatNameDeal     = "成交量总计"   // 广期所指标名称中的TOP20成交量名称
+)
+
+const (
+	TradeExchangeDalian    = "dalian"
+	TradeExchangeZhengzhou = "zhengzhou"
+	TradeExchangeGuangzhou = "guangzhou"
+)
+
+var WarehouseTypeSuffixNames = map[int]string{
+	WarehouseBuyChartType:     "席位多单",
+	WarehouseSoldChartType:    "席位空单",
+	WarehousePureBuyChartType: "席位净多单",
+}
+
+// GuangzhouSeatNameValType 广期所数据名称对应的席位方向
+var GuangzhouSeatNameValType = map[string]int{
+	GuangZhouSeatNameBuy:     1,
+	GuangZhouSeatNameSold:    2,
+	GuangZhouTopSeatNameBuy:  1,
+	GuangZhouTopSeatNameSold: 2,
+	GuangZhouTopSeatNameDeal: 3,
+}
+
+// 合约查询方式
+var (
+	ContractQueryTypeTop   = 1 // 主力合约
+	ContractQueryTypeTop2  = 2 // 成交量前2
+	ContractQueryTypeTop3  = 3 // 成交量前3
+	ContractQueryTypeAll   = 4 // 所有合约(多个)
+	ContractQueryTypeTotal = 5 // 合约加总(1个)
+)
+
+// 合约方向
+var (
+	ContractPositionBuy     = 1 // 多单
+	ContractPositionSold    = 2 // 空单
+	ContractPositionPureBuy = 3 // 净多单
+)
+
+// TradePositionTop 持仓榜单
 type TradePositionTop struct {
 	Id            uint64    `gorm:"primaryKey;column:id" json:"id"`
 	ClassifyName  string    `gorm:"column:classify_name" json:"classify_name"`     //分类名称
@@ -173,220 +231,90 @@ type OriginTradeData struct {
 	ValType      int       `description:"数据类型: 1-多单; 2-空单"`
 }
 
-// GetTradeDataByClassifyAndCompany 根据品种和公司名称获取持仓数据
-func GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, companies []string) (items []*OriginTradeData, err error) {
+// BaseFromTradeCommonIndex 郑商所/大商所/上期所/上期能源指标表通用字段
+type BaseFromTradeCommonIndex struct {
+	Rank          int       `description:"排名"`
+	DealShortName string    `description:"成交量公司简称"`
+	DealName      string    `description:"成交量指标名称"`
+	DealCode      string    `description:"成交量指标编码"`
+	DealValue     int       `description:"成交量"`
+	DealChange    int       `description:"成交变化量"`
+	BuyShortName  string    `description:"持买单量公司简称"`
+	BuyName       string    `description:"持买单量指标名称"`
+	BuyCode       string    `description:"持买单量指标编码"`
+	BuyValue      int       `description:"持买单量"`
+	BuyChange     int       `description:"持买单量变化量"`
+	SoldShortName string    `description:"持卖单量公司简称"`
+	SoldName      string    `description:"持卖单量指标名称"`
+	SoldCode      string    `description:"持卖单量指标编码"`
+	SoldValue     int       `description:"持卖单量"`
+	SoldChange    int       `description:"持卖单变化量"`
+	Frequency     string    `description:"频度"`
+	ClassifyName  string    `description:"品种"`
+	ClassifyType  string    `description:"合约"`
+	CreateTime    time.Time `description:"创建时间"`
+	ModifyTime    time.Time `description:"更新时间"`
+	DataTime      time.Time `description:"数据日期"`
+}
+
+// GetTradeDataByContracts 根据合约获取持仓数据
+func GetTradeDataByContracts(exchange string, classifyNames, contracts []string, startDate, endDate time.Time) (items []*BaseFromTradeCommonIndex, err error) {
 	if exchange == "" {
 		err = fmt.Errorf("数据表名称有误")
 		return
 	}
-	if len(contracts) == 0 || len(companies) == 0 {
-		return
+	var cond string
+	var pars []interface{}
+	if len(classifyNames) > 0 {
+		cond += fmt.Sprintf(` AND classify_name IN (%s)`, utils.GetOrmInReplace(len(classifyNames)))
+		pars = append(pars, classifyNames)
 	}
-	condBuy := fmt.Sprintf(`classify_name = ? AND classify_type IN (%s)`, utils.GetOrmInReplace(len(contracts)))
-	parsBuy := make([]interface{}, 0)
-	parsBuy = append(parsBuy, classifyName, contracts)
-
-	condSold := fmt.Sprintf(`classify_name = ? AND classify_type IN (%s)`, utils.GetOrmInReplace(len(contracts)))
-	parsSold := make([]interface{}, 0)
-	parsSold = append(parsSold, classifyName, contracts)
-
-	// 是否含有TOP20
-	var hasTop bool
-	var condCompanies []string
-	for _, v := range companies {
-		if v == TradeFuturesCompanyTop20 {
-			hasTop = true
-			continue
-		}
-		condCompanies = append(condCompanies, v)
+	if len(contracts) > 0 {
+		cond += fmt.Sprintf(` AND classify_type IN (%s)`, utils.GetOrmInReplace(len(contracts)))
+		pars = append(pars, contracts)
 	}
-	if !hasTop {
-		if len(condCompanies) == 0 {
-			err = fmt.Errorf("查询条件-期货公司异常")
-			return
-		}
-		condBuy += fmt.Sprintf(` AND buy_short_name IN (%s)`, utils.GetOrmInReplace(len(condCompanies)))
-		parsBuy = append(parsBuy, condCompanies)
-		condSold += fmt.Sprintf(` AND sold_short_name IN (%s)`, utils.GetOrmInReplace(len(condCompanies)))
-		parsSold = append(parsSold, condCompanies)
-	} else {
-		// 这里rank=0或者999是因为大商所的数据并不只有999
-		if len(condCompanies) > 0 {
-			condBuy += fmt.Sprintf(` AND (rank = 999 OR rank = 0 OR buy_short_name IN (%s))`, utils.GetOrmInReplace(len(condCompanies)))
-			condSold += fmt.Sprintf(` AND (rank = 999 OR rank = 0 OR sold_short_name IN (%s))`, utils.GetOrmInReplace(len(condCompanies)))
-			parsBuy = append(parsBuy, condCompanies)
-			parsSold = append(parsSold, condCompanies)
-		} else {
-			condBuy += ` AND (rank = 999 OR rank = 0)`
-			condSold += ` AND (rank = 999 OR rank = 0)`
-		}
+	if !startDate.IsZero() && !endDate.IsZero() {
+		cond += ` AND (data_time BETWEEN ? AND ?)`
+		pars = append(pars, startDate.Format(utils.FormatDate), endDate.Format(utils.FormatDate))
 	}
-
+	fields := []string{"rank", "buy_short_name", "buy_value", "buy_change", "sold_short_name", "sold_value", "sold_change", "classify_name", "classify_type", "data_time"}
 	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
-	sql := `SELECT
-			rank,
-			buy_short_name AS company_name,
-			buy_value AS val,
-			buy_change AS val_change,
-			classify_name,
-			classify_type,
-			data_time,
-			1 AS val_type 
-		FROM
-			%s 
-		WHERE
-			%s
-		UNION ALL
-		(
-		SELECT
-			rank,
-			sold_short_name,
-			sold_value,
-			sold_change,
-			classify_name,
-			classify_type,
-			data_time,
-			2 AS val_type 
-		FROM
-			%s 
-		WHERE
-			%s
-		)`
-	sql = fmt.Sprintf(sql, tableName, condBuy, tableName, condSold)
-	o := orm.NewOrmUsingDB("data")
-	_, err = o.Raw(sql, parsBuy, parsSold).QueryRows(&items)
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s ORDER BY data_time DESC`, strings.Join(fields, ","), tableName, cond)
+	_, err = orm.NewOrmUsingDB("data").Raw(sql, pars).QueryRows(&items)
 	return
 }
 
-// GetTradeZhengzhouDataByClassifyAndCompany 郑商所-根据品种和公司名称获取持仓数据
-func GetTradeZhengzhouDataByClassifyAndCompany(exchange string, contracts, companies []string) (items []*OriginTradeData, err error) {
-	if exchange == "" {
-		err = fmt.Errorf("数据表名称有误")
-		return
+// GetZhengzhouTradeDataByContracts 郑商所-根据合约获取持仓数据
+func GetZhengzhouTradeDataByContracts(classifyNames []string, startDate, endDate time.Time) (items []*BaseFromTradeCommonIndex, err error) {
+	var cond string
+	var pars []interface{}
+	if len(classifyNames) > 0 {
+		cond += fmt.Sprintf(` AND classify_name IN (%s)`, utils.GetOrmInReplace(len(classifyNames)))
+		pars = append(pars, classifyNames)
 	}
-	if len(contracts) == 0 || len(companies) == 0 {
-		return
+	if !startDate.IsZero() && !endDate.IsZero() {
+		cond += ` AND (data_time BETWEEN ? AND ?)`
+		pars = append(pars, startDate.Format(utils.FormatDate), endDate.Format(utils.FormatDate))
 	}
-	condBuy := fmt.Sprintf(`classify_name IN (%s)`, utils.GetOrmInReplace(len(contracts)))
-	parsBuy := make([]interface{}, 0)
-	parsBuy = append(parsBuy, contracts)
-
-	condSold := fmt.Sprintf(`classify_name IN (%s)`, utils.GetOrmInReplace(len(contracts)))
-	parsSold := make([]interface{}, 0)
-	parsSold = append(parsSold, contracts)
-
-	// 是否含有TOP20
-	var hasTop bool
-	var condCompanies []string
-	for _, v := range companies {
-		if v == TradeFuturesCompanyTop20 {
-			hasTop = true
-			continue
-		}
-		condCompanies = append(condCompanies, v)
-	}
-	if !hasTop {
-		if len(condCompanies) == 0 {
-			err = fmt.Errorf("查询条件-期货公司异常")
-			return
-		}
-		condBuy += fmt.Sprintf(` AND buy_short_name IN (%s)`, utils.GetOrmInReplace(len(condCompanies)))
-		parsBuy = append(parsBuy, condCompanies)
-		condSold += fmt.Sprintf(` AND sold_short_name IN (%s)`, utils.GetOrmInReplace(len(condCompanies)))
-		parsSold = append(parsSold, condCompanies)
-	} else {
-		if len(condCompanies) > 0 {
-			condBuy += fmt.Sprintf(` AND (rank = 999 OR buy_short_name IN (%s))`, utils.GetOrmInReplace(len(condCompanies)))
-			condSold += fmt.Sprintf(` AND (rank = 999 OR sold_short_name IN (%s))`, utils.GetOrmInReplace(len(condCompanies)))
-			parsBuy = append(parsBuy, condCompanies)
-			parsSold = append(parsSold, condCompanies)
-		} else {
-			condBuy += ` AND rank = 999`
-			condSold += ` AND rank = 999`
-		}
-	}
-
-	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
-	sql := `SELECT
-			rank,
-			buy_short_name AS company_name,
-			buy_value AS val,
-			buy_change AS val_change,
-			classify_name AS classify_type,
-			data_time,
-			1 AS val_type 
-		FROM
-			%s 
-		WHERE
-			%s
-		UNION ALL
-		(
-		SELECT
-			rank,
-			sold_short_name,
-			sold_value,
-			sold_change,
-			classify_name AS classify_type,
-			data_time,
-			2 AS val_type 
-		FROM
-			%s 
-		WHERE
-			%s
-		)`
-	sql = fmt.Sprintf(sql, tableName, condBuy, tableName, condSold)
-	o := orm.NewOrmUsingDB("data")
-	_, err = o.Raw(sql, parsBuy, parsSold).QueryRows(&items)
+	// ps.classify_name实为合约代码
+	fields := []string{"rank", "buy_short_name", "buy_value", "buy_change", "sold_short_name", "sold_value", "sold_change", "classify_name AS classify_type", "data_time"}
+	sql := fmt.Sprintf(`SELECT %s FROM base_from_trade_zhengzhou_index WHERE 1=1 %s ORDER BY data_time DESC`, strings.Join(fields, ","), cond)
+	_, err = orm.NewOrmUsingDB("data").Raw(sql, pars).QueryRows(&items)
 	return
 }
 
 // ContractCompanyTradeData [合约-期货公司]持仓数据
 type ContractCompanyTradeData struct {
-	CompanyName  string                          `description:"期货公司名称"`
+	Exchange     string                          `description:"交易所"`
+	ClassifyName string                          `description:"品种"`
 	ClassifyType string                          `description:"合约代码"`
+	CompanyName  string                          `description:"期货公司名称"`
+	IsTotal      bool                            `description:"是否为合约加总"`
 	StartDate    time.Time                       `description:"数据开始日期"`
 	EndDate      time.Time                       `description:"数据结束日期"`
 	DataList     []*ContractCompanyTradeDataList `description:"数据序列"`
 }
 
-const (
-	TradeDataTypeNull      = 0 // 无值
-	TradeDataTypeOrigin    = 1 // 原始值
-	TradeDataTypeCalculate = 2 // 推算值
-
-	WarehouseBuyChartType     = 1 // 多单图
-	WarehouseSoldChartType    = 2 // 空单图
-	WarehousePureBuyChartType = 3 // 净多单图
-
-	WarehouseDefaultUnit      = "手"
-	WarehouseDefaultFrequency = "日度"
-
-	GuangZhouTopCompanyAliasName = "日成交持仓排名" // 广期所TOP20对应的公司名称
-	GuangZhouSeatNameBuy         = "持买单量"    // 广期所指标名称中的多单名称
-	GuangZhouSeatNameSold        = "持卖单量"    // 广期所指标名称中的空单名称
-	GuangZhouTopSeatNameBuy      = "持买单量总计"  // 广期所指标名称中的TOP20多单名称
-	GuangZhouTopSeatNameSold     = "持卖单量总计"  // 广期所指标名称中的TOP20空单名称
-)
-
-const (
-	TradeExchangeZhengzhou = "zhengzhou"
-	TradeExchangeGuangzhou = "guangzhou"
-)
-
-var WarehouseTypeSuffixNames = map[int]string{
-	WarehouseBuyChartType:     "席位多单",
-	WarehouseSoldChartType:    "席位空单",
-	WarehousePureBuyChartType: "席位净多单",
-}
-
-// GuangzhouSeatNameValType 广期所数据名称对应的席位方向
-var GuangzhouSeatNameValType = map[string]int{
-	GuangZhouSeatNameBuy:     1,
-	GuangZhouSeatNameSold:    2,
-	GuangZhouTopSeatNameBuy:  1,
-	GuangZhouTopSeatNameSold: 2,
-}
-
 // ContractCompanyTradeDataList [合约-期货公司]持仓数据详情
 type ContractCompanyTradeDataList struct {
 	Date              time.Time `description:"数据日期"`
@@ -404,139 +332,6 @@ type ContractCompanyTradeDataList struct {
 	PureBuyChangeType int       `description:"净多单持仓增减类型: 0-无值; 1-原始值; 2-推算值"`
 }
 
-// GetLastTradeDataByClassify 获取[合约]末位多空单数据
-func GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*OriginTradeData, err error) {
-	if exchange == "" {
-		err = fmt.Errorf("数据表名称有误")
-		return
-	}
-	if len(contracts) == 0 {
-		return
-	}
-	contractReplacer := utils.GetOrmInReplace(len(contracts))
-
-	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
-	sql := `SELECT 
-			tpt.rank,
-			tpt.buy_short_name AS company_name,
-			tpt.buy_value AS val,
-			tpt.buy_change AS val_change,
-			tpt.classify_name,
-			tpt.classify_type,
-			tpt.data_time,
-			1 AS val_type
-		FROM 
-			%s tpt
-		JOIN 
-			(
-				SELECT
-					data_time, classify_type, MAX(rank) AS max_rank
-				FROM 
-					%s
-				WHERE 
-					classify_name = ? AND classify_type IN (%s) AND buy_short_name <> ''
-				GROUP BY 
-					data_time,
-					classify_type
-			) sub
-		ON
-			tpt.data_time = sub.data_time AND tpt.classify_type = sub.classify_type AND tpt.rank = sub.max_rank
-		WHERE 
-			tpt.classify_name = ? AND tpt.classify_type IN (%s)
-		UNION ALL
-		(
-		SELECT 
-			tpt.rank, tpt.sold_short_name, tpt.sold_value, tpt.sold_change, tpt.classify_name, tpt.classify_type, tpt.data_time, 2 AS val_type
-		FROM 
-			%s tpt
-		JOIN 
-			(
-				SELECT 
-					data_time, classify_type, MAX(rank) AS max_rank
-				FROM 
-					%s
-				WHERE 
-					classify_name = ? AND classify_type IN (%s) AND sold_short_name <> ''
-				GROUP BY 
-					data_time, classify_type
-			) sub
-		ON 
-			tpt.data_time = sub.data_time AND tpt.classify_type = sub.classify_type AND tpt.rank = sub.max_rank
-		WHERE 
-			tpt.classify_name = ? AND tpt.classify_type IN (%s)
-		)`
-	sql = fmt.Sprintf(sql, tableName, tableName, contractReplacer, contractReplacer, tableName, tableName, contractReplacer, contractReplacer)
-	o := orm.NewOrmUsingDB("data")
-	_, err = o.Raw(sql, classifyName, contracts, classifyName, contracts, classifyName, contracts, classifyName, contracts).QueryRows(&items)
-	return
-}
-
-// GetLastTradeZhengzhouDataByClassify 郑商所-获取[合约]末位多空单数据
-func GetLastTradeZhengzhouDataByClassify(exchange string, contracts []string) (items []*OriginTradeData, err error) {
-	if exchange == "" {
-		err = fmt.Errorf("数据表名称有误")
-		return
-	}
-	if len(contracts) == 0 {
-		return
-	}
-	contractReplacer := utils.GetOrmInReplace(len(contracts))
-
-	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
-	sql := `SELECT 
-			tpt.rank,
-			tpt.buy_short_name AS company_name,
-			tpt.buy_value AS val,
-			tpt.buy_change AS val_change,
-  			tpt.classify_name AS classify_type,
-			tpt.data_time,
-			1 AS val_type
-		FROM 
-			%s tpt
-		JOIN 
-			(
-				SELECT
-					data_time, classify_name, MAX(rank) AS max_rank
-				FROM 
-					%s
-				WHERE 
-					classify_name IN (%s) AND buy_short_name <> ''
-				GROUP BY 
-					data_time,
-					classify_name
-			) sub
-		ON
-			tpt.data_time = sub.data_time AND tpt.classify_name = sub.classify_name AND tpt.rank = sub.max_rank
-		WHERE 
-			tpt.classify_name IN (%s)
-		UNION ALL
-		(
-		SELECT 
-			tpt.rank, tpt.sold_short_name, tpt.sold_value, tpt.sold_change, tpt.classify_name AS classify_type, tpt.data_time, 2 AS val_type
-		FROM 
-			%s tpt
-		JOIN 
-			(
-				SELECT 
-					data_time, classify_name, MAX(rank) AS max_rank
-				FROM 
-					%s
-				WHERE 
-					classify_name IN (%s) AND sold_short_name <> ''
-				GROUP BY 
-					data_time, classify_name
-			) sub
-		ON 
-			tpt.data_time = sub.data_time AND tpt.classify_name = sub.classify_name AND tpt.rank = sub.max_rank
-		WHERE 
-			tpt.classify_name IN (%s)
-		)`
-	sql = fmt.Sprintf(sql, tableName, tableName, contractReplacer, contractReplacer, tableName, tableName, contractReplacer, contractReplacer)
-	o := orm.NewOrmUsingDB("data")
-	_, err = o.Raw(sql, contracts, contracts, contracts, contracts).QueryRows(&items)
-	return
-}
-
 type BaseFromTradeGuangzhouIndex struct {
 	BaseFromTradeGuangzhouIndexId    int       `orm:"column(base_from_trade_guangzhou_index_id);pk"`
 	BaseFromTradeGuangzhouClassifyId int       `description:"分类id"`
@@ -550,10 +345,27 @@ type BaseFromTradeGuangzhouIndex struct {
 	ModifyTime                       time.Time `description:"修改日期"`
 }
 
-func GetBaseFromTradeGuangzhouIndexByClassifyId(classifyId int) (list []*BaseFromTradeGuangzhouIndex, err error) {
+func GetBaseFromTradeGuangzhouIndex(classifyIds []int, contracts []string, indexKeyword string) (list []*BaseFromTradeGuangzhouIndex, err error) {
 	o := orm.NewOrmUsingDB("data")
-	sql := `SELECT * FROM base_from_trade_guangzhou_index WHERE base_from_trade_guangzhou_classify_id = ?`
-	_, err = o.Raw(sql, classifyId).QueryRows(&list)
+	cond := ``
+	pars := make([]interface{}, 0)
+	if len(classifyIds) > 0 {
+		cond += fmt.Sprintf(` AND b.base_from_trade_guangzhou_classify_id IN (%s)`, utils.GetOrmInReplace(len(classifyIds)))
+		pars = append(pars, classifyIds)
+	}
+	if len(contracts) > 0 {
+		cond += fmt.Sprintf(` AND b.contract IN (%s)`, utils.GetOrmInReplace(len(contracts)))
+		pars = append(pars, contracts)
+	}
+	if indexKeyword != "" {
+		cond += fmt.Sprintf(` AND a.index_name LIKE ?`)
+		pars = append(pars, indexKeyword)
+	}
+	sql := `SELECT a.* FROM base_from_trade_guangzhou_index AS a
+		JOIN base_from_trade_guangzhou_contract AS b ON a.base_from_trade_guangzhou_contract_id = b.base_from_trade_guangzhou_contract_id
+		WHERE 1=1 %s`
+	sql = fmt.Sprintf(sql, cond)
+	_, err = o.Raw(sql, pars).QueryRows(&list)
 	return
 }
 
@@ -568,45 +380,120 @@ type BaseFromTradeGuangzhouData struct {
 	ModifyTime                    time.Time `description:"修改日期"`
 }
 
-// GetBaseFromTradeGuangzhouDataByIndexIds 获取指标数据
-func GetBaseFromTradeGuangzhouDataByIndexIds(indexIds []int) (list []*BaseFromTradeGuangzhouData, err error) {
+// GetBaseFromTradeGuangzhouDataByIndexIds 广期所-获取指标数据
+func GetBaseFromTradeGuangzhouDataByIndexIds(indexIds []int, startDate, endDate time.Time) (list []*BaseFromTradeGuangzhouData, err error) {
 	if len(indexIds) == 0 {
 		return
 	}
-	o := orm.NewOrmUsingDB("data")
-	sql := fmt.Sprintf(`SELECT * FROM base_from_trade_guangzhou_data WHERE base_from_trade_guangzhou_index_id IN (%s) ORDER BY base_from_trade_guangzhou_index_id`, utils.GetOrmInReplace(len(indexIds)))
-	_, err = o.Raw(sql, indexIds).QueryRows(&list)
+	cond := fmt.Sprintf(` AND base_from_trade_guangzhou_index_id IN (%s)`, utils.GetOrmInReplace(len(indexIds)))
+	pars := make([]interface{}, 0)
+	pars = append(pars, indexIds)
+	if !startDate.IsZero() && !endDate.IsZero() {
+		if startDate.Equal(endDate) {
+			cond += ` AND data_time = ?`
+			pars = append(pars, startDate.Format(utils.FormatDate))
+		}
+		if !startDate.Equal(endDate) {
+			cond += ` AND (data_time BETWEEN ? AND ?)`
+			pars = append(pars, startDate.Format(utils.FormatDate), endDate.Format(utils.FormatDate))
+		}
+	}
+	sql := fmt.Sprintf(`SELECT * FROM base_from_trade_guangzhou_data WHERE 1=1 %s ORDER BY base_from_trade_guangzhou_index_id`, cond)
+	_, err = orm.NewOrmUsingDB("data").Raw(sql, pars).QueryRows(&list)
 	return
 }
 
-// GetBaseFromTradeGuangzhouMinDataByIndexIds 获取指标中的末位数据
-func GetBaseFromTradeGuangzhouMinDataByIndexIds(indexIds []int) (list []*BaseFromTradeGuangzhouData, err error) {
-	indexLen := len(indexIds)
-	if indexLen == 0 {
+// ContractTopRankData TOP20合约排名数据
+type ContractTopRankData struct {
+	Exchange      string    `description:"交易所"`
+	DealValue     int       `description:"成交量"`
+	BuyValue      int       `description:"多单持仓量"`
+	BuyChange     int       `description:"多单变化"`
+	SoldValue     int       `description:"空单持仓量"`
+	SoldChange    int       `description:"空单变化"`
+	PureBuyValue  int       `description:"净多单持仓量"`
+	PureBuyChange int       `description:"净多单变化"`
+	ClassifyName  string    `description:"品种名称"`
+	ClassifyType  string    `description:"合约代码"`
+	DataTime      time.Time `description:"数据日期"`
+}
+
+// GetContractTopRankData 获取合约TOP20根据当日成交量排名
+func GetContractTopRankData(exchange string, classifyNames []string, dataDate time.Time) (items []*ContractTopRankData, err error) {
+	if exchange == "" {
+		err = fmt.Errorf("数据表名称有误")
 		return
 	}
-	o := orm.NewOrmUsingDB("data")
-	sql := fmt.Sprintf(`SELECT 
-			t1.data_time,
-			t1.min_value AS value
-		FROM 
-			(
-				SELECT 
-					data_time,
-					MIN(value) AS min_value
-				FROM 
-					base_from_trade_guangzhou_data
-				WHERE 
-					base_from_trade_guangzhou_index_id IN (%s)
-				GROUP BY 
-					data_time
-			) t1
-		JOIN 
-			base_from_trade_guangzhou_data t2
-		ON 
-			t1.data_time = t2.data_time AND t1.min_value = t2.value AND t2.base_from_trade_guangzhou_index_id IN (%s)
-		GROUP BY 
-			t1.data_time`, utils.GetOrmInReplace(indexLen), utils.GetOrmInReplace(indexLen))
-	_, err = o.Raw(sql, indexIds, indexIds).QueryRows(&list)
+	if len(classifyNames) == 0 {
+		return
+	}
+	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
+	// 大商所存在TOP20的rank=0
+	queryRank := ` rank = 999`
+	if exchange == TradeExchangeDalian {
+		queryRank = ` (rank = 999 OR rank = 0)`
+	}
+	sql := `SELECT * FROM %s WHERE data_time = ? AND classify_name IN (%s) AND %s GROUP BY classify_type ORDER BY deal_value DESC`
+	sql = fmt.Sprintf(sql, tableName, utils.GetOrmInReplace(len(classifyNames)), queryRank)
+	_, err = orm.NewOrmUsingDB("data").Raw(sql, dataDate.Format(utils.FormatDate), classifyNames).QueryRows(&items)
+	return
+}
+
+// GetZhengzhouContractTopRankData 郑商所-获取合约根据当日成交量排名
+func GetZhengzhouContractTopRankData(classifyNames []string, dataDate time.Time) (items []*ContractTopRankData, err error) {
+	if len(classifyNames) == 0 {
+		return
+	}
+	sql := `SELECT * FROM base_from_trade_zhengzhou_index WHERE data_time = ? AND classify_name IN (%s) AND rank = 999 GROUP BY classify_name ORDER BY deal_value DESC`
+	sql = fmt.Sprintf(sql, utils.GetOrmInReplace(len(classifyNames)))
+	_, err = orm.NewOrmUsingDB("data").Raw(sql, dataDate.Format(utils.FormatDate), classifyNames).QueryRows(&items)
+	return
+}
+
+// ContractCompanyTradeEdb [合约-期货公司]指标
+type ContractCompanyTradeEdb struct {
+	Exchange         string                         `description:"交易所"`
+	ClassifyName     string                         `description:"品种"`
+	ClassifyType     string                         `description:"合约代码"`
+	CompanyName      string                         `description:"期货公司名称"`
+	IsTotal          bool                           `description:"是否为合约加总"`
+	ContractPosition int                            `description:"合约方向"`
+	StartDate        time.Time                      `description:"数据开始日期"`
+	EndDate          time.Time                      `description:"数据结束日期"`
+	DataList         []*ContractCompanyTradeEdbData `description:"数据序列"`
+}
+
+// ContractCompanyTradeEdbData [合约-期货公司]指标数据
+type ContractCompanyTradeEdbData struct {
+	DataTime time.Time `description:"数据日期"`
+	Val      int       `description:"数据值"`
+}
+
+// GetClassifyNewestDataTime 获取品种最新数据日期
+func GetClassifyNewestDataTime(exchange string, classifyNames []string) (dateTime time.Time, err error) {
+	if exchange == "" {
+		err = fmt.Errorf("数据表名称有误")
+		return
+	}
+	if len(classifyNames) == 0 {
+		return
+	}
+	tableName := fmt.Sprintf("base_from_trade_%s_index", exchange)
+	sql := `SELECT data_time FROM %s WHERE classify_name IN (%s) ORDER BY data_time DESC LIMIT 1`
+	sql = fmt.Sprintf(sql, tableName, utils.GetOrmInReplace(len(classifyNames)))
+	err = orm.NewOrmUsingDB("data").Raw(sql, classifyNames).QueryRow(&dateTime)
+	return
+}
+
+// GetGuangzhouClassifyNewestDataTime 广期所-获取品种最新数据日期
+func GetGuangzhouClassifyNewestDataTime(indexIds []int) (dateTime time.Time, err error) {
+	if len(indexIds) == 0 {
+		return
+	}
+	cond := fmt.Sprintf(` AND base_from_trade_guangzhou_index_id IN (%s)`, utils.GetOrmInReplace(len(indexIds)))
+	pars := make([]interface{}, 0)
+	pars = append(pars, indexIds)
+	sql := fmt.Sprintf(`SELECT data_time FROM base_from_trade_guangzhou_data WHERE 1=1 %s ORDER BY data_time DESC LIMIT 1`, cond)
+	err = orm.NewOrmUsingDB("data").Raw(sql, pars).QueryRow(&dateTime)
 	return
 }

+ 43 - 0
models/data_manage/trade_analysis/trade_analysis_correlation.go

@@ -0,0 +1,43 @@
+package trade_analysis
+
+// CorrelationTableExtraConfig 相关性表格配置
+type CorrelationTableExtraConfig struct {
+	BaseEdbInfoId    int                          `description:"标的指标ID"`
+	BaseEdbName      string                       `description:"标的指标名称(仅回显时用)"`
+	Exchange         string                       `description:"交易所标识"`
+	ClassifyName     string                       `description:"(单选)品种"`
+	CompanyNames     []string                     `description:"(多选)期货公司"`
+	ContractType     int                          `description:"合约类型: 1-主力合约; 2-成交量前2; 3-成交量前3; 4-所有合约(多个); 5-合约加总(1个)"`
+	ContractPosition int                          `description:"(单选)合约方向: 1-多; 2-空; 3-净多"`
+	PredictRatio     float64                      `description:"预估参数, 0-1之间"`
+	RollConfig       []CorrelationTableRollConfig `description:"滚动相关性配置"`
+}
+
+// CorrelationTableRollConfig 滚动相关性配置
+type CorrelationTableRollConfig struct {
+	CalculateValue int `description:"计算窗口"`
+	LeadValue      int `description:"领先期数"`
+}
+
+// CorrelationTableRowData 相关性表格行数据
+type CorrelationTableRowData struct {
+	Exchange     string                        `description:"交易所"`
+	ClassifyName string                        `description:"品种"`
+	ClassifyType string                        `description:"合约"`
+	CompanyName  string                        `description:"公司名称"`
+	RowName      string                        `description:"合约持仓名称"`
+	DayData      []*CorrelationTableRowDayData `description:"天数对应的相关性系数"`
+}
+
+// CorrelationTableRowDayData 相关性表格行数据值
+type CorrelationTableRowDayData struct {
+	Day      int     `description:"天数: 从0到-10, 0为最新数据, -1表示前一天的滚动相关性"`
+	DataDate string  `description:"对应的日期"`
+	DataVal  float64 `description:"相关性系数"`
+}
+
+// CorrelationTableData 相关性表格数据
+type CorrelationTableData struct {
+	CorrelationTableRollConfig `description:"滚动相关性配置, 每个配置单独为一张表"`
+	RowsData                   []*CorrelationTableRowData `description:"表格行数据"`
+}

+ 41 - 0
models/data_manage/trade_analysis/trade_analysis_table.go

@@ -0,0 +1,41 @@
+package trade_analysis
+
+// TableExtraConfig 表格配置
+type TableExtraConfig struct {
+	CompanyName  string                     `description:"期货公司"`
+	ClassifyList []TableExtraConfigClassify `description:"交易所品种信息"`
+	ContractType int                        `description:"合约类型: 1-主力合约; 2-成交量前2; 3-成交量前3; 4-所有合约(多个); 5-合约加总(1个)"`
+	DateType     int                        `description:"0-最新日期(默认); 1-固定日期"`
+	IntervalMove int                        `description:"前移期数"`
+	FixedDate    string                     `description:"固定日期"`
+	PredictRatio float64                    `description:"预估参数, 0-1之间"`
+}
+
+// TableExtraConfigClassify 表格配置品种
+type TableExtraConfigClassify struct {
+	Exchange      string   `description:"交易所"`
+	ClassifyNames []string `description:"品种"`
+}
+
+// TableRowData 表格行数据
+type TableRowData struct {
+	Exchange         string  `description:"交易所"`
+	ClassifyName     string  `description:"品种"`
+	ClassifyType     string  `description:"合约"`
+	BuyValue         int     `description:"多单持仓量"`
+	BuyChange        int     `description:"多单变化"`
+	SoldValue        int     `description:"空单持仓量"`
+	SoldChange       int     `description:"空单变化"`
+	PureBuyVal       int     `description:"净多单持仓量"`
+	PureBuyChange    int     `description:"净多单持仓增减"`
+	BuySoldRatio     float64 `description:"多空比"`
+	BuyTopRatio      float64 `description:"多单占前20比例"`
+	SoldTopRatio     float64 `description:"空单占前20比例"`
+	TopBuyValue      int     `description:"前20多单"`
+	TopSoldValue     int     `description:"前20空单"`
+	TopBuyChange     int     `description:"前20多单变动"`
+	TopSoldChange    int     `description:"前20空单变动"`
+	TopPureBuy       int     `description:"前20净多单"`
+	TopPureBuyChange int     `description:"前20净多单变动"`
+	TopBuySoldRatio  float64 `description:"前20多空比"`
+}

+ 188 - 0
models/data_manage/trade_analysis/trade_analysis_table_column.go

@@ -0,0 +1,188 @@
+package trade_analysis
+
+import (
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+	"strings"
+	"time"
+)
+
+// TradeAnalysisTableColumn 持仓分析-多空分析自定义列
+type TradeAnalysisTableColumn struct {
+	Id           int       `orm:"column(id);pk"`
+	ExcelInfoId  int       `description:"表格ID,0为模板"`
+	ColumnKey    string    `description:"字段标识"`
+	ColumnName   string    `description:"字段名称"`
+	ColumnNameEn string    `description:"英文字段名称"`
+	Sort         int       `description:"排序"`
+	IsMust       int       `description:"是否必选列:0-否;1-是"`
+	IsSort       int       `description:"是否允许排序:0-否;1-是"`
+	IsShow       int       `description:"是否展示:0-隐藏;1-显示"`
+	CreateTime   time.Time `description:"创建时间"`
+	ModifyTime   time.Time `description:"修改时间"`
+}
+
+func (m *TradeAnalysisTableColumn) TableName() string {
+	return "trade_analysis_table_column"
+}
+
+type TradeAnalysisTableColumnCols struct {
+	PrimaryId    string
+	ExcelInfoId  string
+	ColumnKey    string
+	ColumnName   string
+	ColumnNameEn string
+	Sort         string
+	IsMust       string
+	IsSort       string
+	IsShow       string
+	CreateTime   string
+	ModifyTime   string
+}
+
+func (m *TradeAnalysisTableColumn) Cols() TradeAnalysisTableColumnCols {
+	return TradeAnalysisTableColumnCols{
+		PrimaryId:    "id",
+		ExcelInfoId:  "excel_info_id",
+		ColumnKey:    "column_key",
+		ColumnName:   "column_name",
+		ColumnNameEn: "column_name_en",
+		Sort:         "sort",
+		IsMust:       "is_must",
+		IsSort:       "is_sort",
+		IsShow:       "is_show",
+		CreateTime:   "create_time",
+		ModifyTime:   "modify_time",
+	}
+}
+
+func (m *TradeAnalysisTableColumn) Create() (err error) {
+	o := orm.NewOrmUsingDB("data")
+	id, err := o.Insert(m)
+	if err != nil {
+		return
+	}
+	m.Id = int(id)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) CreateMulti(items []*TradeAnalysisTableColumn) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.InsertMulti(len(items), items)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) Update(cols []string) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.Update(m, cols...)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) Remove() (err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	_, err = o.Raw(sql, m.Id).Exec()
+	return
+}
+
+func (m *TradeAnalysisTableColumn) MultiRemove(ids []int) (err error) {
+	if len(ids) == 0 {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s IN (%s)`, m.TableName(), m.Cols().PrimaryId, utils.GetOrmInReplace(len(ids)))
+	_, err = o.Raw(sql, ids).Exec()
+	return
+}
+
+func (m *TradeAnalysisTableColumn) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	_, err = o.Raw(sql, pars).Exec()
+	return
+}
+
+func (m *TradeAnalysisTableColumn) GetItemById(id int) (item *TradeAnalysisTableColumn, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = o.Raw(sql, id).QueryRow(&item)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *TradeAnalysisTableColumn, err error) {
+	o := orm.NewOrmUsingDB("data")
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s %s LIMIT 1`, m.TableName(), condition, order)
+	err = o.Raw(sql, pars).QueryRow(&item)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = o.Raw(sql, pars).QueryRow(&count)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*TradeAnalysisTableColumn, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+func (m *TradeAnalysisTableColumn) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*TradeAnalysisTableColumn, err error) {
+	o := orm.NewOrmUsingDB("data")
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s LIMIT ?,?`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars, startSize, pageSize).QueryRows(&items)
+	return
+}
+
+// TradeAnalysisTableColumnItem 多因子系列信息
+type TradeAnalysisTableColumnItem struct {
+	ColumnKey    string `description:"字段标识"`
+	ColumnName   string `description:"字段名称"`
+	ColumnNameEn string `description:"英文字段名称"`
+	Sort         int    `description:"排序"`
+	IsMust       int    `description:"是否必选列:0-否;1-是"`
+	IsSort       int    `description:"是否允许排序:0-否;1-是"`
+	IsShow       int    `description:"是否展示:0-隐藏;1-显示"`
+}
+
+func (m *TradeAnalysisTableColumn) Format2Item() (item *TradeAnalysisTableColumnItem) {
+	item = new(TradeAnalysisTableColumnItem)
+	item.ColumnKey = m.ColumnKey
+	item.ColumnName = m.ColumnName
+	item.ColumnNameEn = m.ColumnNameEn
+	item.Sort = m.Sort
+	item.IsMust = m.IsMust
+	item.IsSort = m.IsSort
+	item.IsShow = m.IsShow
+	return
+}

+ 13 - 13
models/data_manage/trade_analysis/warehouse_process_classify.go

@@ -1,7 +1,7 @@
 package trade_analysis
 
 import (
-	"eta/eta_api/models"
+	"eta/eta_api/models/common"
 	"eta/eta_api/utils"
 	"fmt"
 	"github.com/beego/beego/v2/client/orm"
@@ -201,8 +201,8 @@ func (m *WareHouseProcessClassify) Format2Item() (item *WareHouseProcessClassify
 // ------------------------------------------------ 通用分类 ------------------------------------------------
 
 // GetCommonClassifyCols 通用分类字段映射
-func (m *WareHouseProcessClassify) GetCommonClassifyCols() models.CommonClassifyCols {
-	return models.CommonClassifyCols{
+func (m *WareHouseProcessClassify) GetCommonClassifyCols() common.CommonClassifyCols {
+	return common.CommonClassifyCols{
 		ClassifyId:   m.Cols().PrimaryId,
 		ClassifyName: m.Cols().ClassifyName,
 		ParentId:     m.Cols().ParentId,
@@ -216,13 +216,13 @@ func (m *WareHouseProcessClassify) GetCommonClassifyCols() models.CommonClassify
 }
 
 // GetCommonClassifyById 获取通用分类
-func (m *WareHouseProcessClassify) GetCommonClassifyById(classifyId int) (commonClassify *models.CommonClassify, err error) {
+func (m *WareHouseProcessClassify) GetCommonClassifyById(classifyId int) (commonClassify *common.CommonClassify, err error) {
 	item, e := m.GetItemById(classifyId)
 	if e != nil {
 		err = e
 		return
 	}
-	commonClassify = new(models.CommonClassify)
+	commonClassify = new(common.CommonClassify)
 	commonClassify.ClassifyId = item.WareHouseProcessClassifyId
 	commonClassify.ClassifyName = item.ClassifyName
 	commonClassify.ParentId = item.ParentId
@@ -236,13 +236,13 @@ func (m *WareHouseProcessClassify) GetCommonClassifyById(classifyId int) (common
 }
 
 // GetClassifyByParentIdAndName 实现获取分类信息的方法
-func (m *WareHouseProcessClassify) GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*models.CommonClassify, error) {
+func (m *WareHouseProcessClassify) GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*common.CommonClassify, error) {
 	// 实现获取分类信息的逻辑
 	return nil, nil
 }
 
 // UpdateCommonClassify 实现更新分类信息的方法
-func (m *WareHouseProcessClassify) UpdateCommonClassify(classify *models.CommonClassify, updateCols []string) (err error) {
+func (m *WareHouseProcessClassify) UpdateCommonClassify(classify *common.CommonClassify, updateCols []string) (err error) {
 	return
 }
 
@@ -271,7 +271,7 @@ func (m *WareHouseProcessClassify) GetClassifySortMaxByParentId(parentId int) (s
 	return
 }
 
-func (m *WareHouseProcessClassify) GetFirstClassifyByParentId(parentId int) (item *models.CommonClassify, err error) {
+func (m *WareHouseProcessClassify) GetFirstClassifyByParentId(parentId int) (item *common.CommonClassify, err error) {
 	//o := orm.NewOrmUsingDB("data")
 	//sql := ` SELECT * FROM edb_classify WHERE parent_id=? order by sort asc,classify_id asc limit 1`
 	//err = o.Raw(sql, parentId).QueryRow(&item)
@@ -290,16 +290,16 @@ func (m *WareHouseProcessClassify) SetClassifySortByParentId(parentId, classifyI
 }
 
 // GetCommonClassifyObjCols 通用分类对象字段映射
-func (m *WareHouseProcessClassify) GetCommonClassifyObjCols() models.CommonClassifyObjCols {
+func (m *WareHouseProcessClassify) GetCommonClassifyObjCols() common.CommonClassifyObjCols {
 	// TODO: 完善
-	return models.CommonClassifyObjCols{
+	return common.CommonClassifyObjCols{
 		ObjectId:   m.Cols().ClassifyName,
 		ClassifyId: m.Cols().PrimaryId,
 		Sort:       m.Cols().ParentId,
 	}
 }
 
-func (m *WareHouseProcessClassify) GetObjectById(objectId int) (*models.CommonClassifyObj, error) {
+func (m *WareHouseProcessClassify) GetObjectById(objectId int) (*common.CommonClassifyObj, error) {
 	// 实现获取分类信息的逻辑
 	return nil, nil
 }
@@ -313,7 +313,7 @@ func (m *WareHouseProcessClassify) GetObjectSortMaxByClassifyId(classifyId int)
 	return
 }
 
-func (m *WareHouseProcessClassify) GetFirstObjectByClassifyId(classifyId int) (item *models.CommonClassifyObj, err error) {
+func (m *WareHouseProcessClassify) GetFirstObjectByClassifyId(classifyId int) (item *common.CommonClassifyObj, err error) {
 	//o := orm.NewOrmUsingDB("data")
 	//sql := ` SELECT * FROM edb_info WHERE classify_id=? order by sort asc,edb_info_id asc limit 1`
 	//err = o.Raw(sql, classifyId).QueryRow(&item)
@@ -333,7 +333,7 @@ func (m *WareHouseProcessClassify) SetObjectSortByClassifyId(classifyId, sort, p
 }
 
 // UpdateCommonClassifyObj 更新通用分类对象
-func (m *WareHouseProcessClassify) UpdateCommonClassifyObj(object *models.CommonClassifyObj, updateCols []string) (err error) {
+func (m *WareHouseProcessClassify) UpdateCommonClassifyObj(object *common.CommonClassifyObj, updateCols []string) (err error) {
 	return
 }
 

+ 12 - 10
models/db.go

@@ -16,6 +16,7 @@ import (
 	future_good2 "eta/eta_api/models/data_manage/future_good"
 	"eta/eta_api/models/data_manage/stl"
 	"eta/eta_api/models/data_manage/supply_analysis"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
 	"eta/eta_api/models/data_stat"
 	edbmonitor "eta/eta_api/models/edb_monitor"
 	"eta/eta_api/models/eta_trial"
@@ -538,16 +539,17 @@ func initChartFramework() {
 // initExcel 初始化EXCEL
 func initExcel() {
 	orm.RegisterModel(
-		new(excel.ExcelClassify),        //ETA excel表格分类
-		new(excel.ExcelInfo),            //ETA excel表格
-		new(excel.ExcelDraft),           //ETA excel表格草稿
-		new(excel.ExcelSheet),           //ETA excel sheet
-		new(excel.ExcelSheetData),       //ETA excel sheet data
-		new(excel.ExcelEdbMapping),      //ETA excel 与 指标 的关系表
-		new(excel.ExcelWorker),          // 平衡表协作人表格
-		new(excel.ExcelChartEdb),        // 平衡表做图指标
-		new(excel.ExcelChartData),       // 平衡表作图数据
-		new(excel.ExcelInfoRuleMapping), //表格的管理规则
+		new(excel.ExcelClassify),                         //ETA excel表格分类
+		new(excel.ExcelInfo),                             //ETA excel表格
+		new(excel.ExcelDraft),                            //ETA excel表格草稿
+		new(excel.ExcelSheet),                            //ETA excel sheet
+		new(excel.ExcelSheetData),                        //ETA excel sheet data
+		new(excel.ExcelEdbMapping),                       //ETA excel 与 指标 的关系表
+		new(excel.ExcelWorker),                           // 平衡表协作人表格
+		new(excel.ExcelChartEdb),                         // 平衡表做图指标
+		new(excel.ExcelChartData),                        // 平衡表作图数据
+		new(excel.ExcelInfoRuleMapping),                  //表格的管理规则
+		new(tradeAnalysisModel.TradeAnalysisTableColumn), // 持仓分析表格-自定义列
 	)
 }
 

+ 135 - 0
routers/commentsRouter.go

@@ -9421,6 +9421,15 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
+        beego.ControllerComments{
+            Method: "GetTradeExchangeClassifyTree",
+            Router: `/exchange_classify/tree`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisController"],
         beego.ControllerComments{
             Method: "GetTradeExchangeList",
@@ -9439,6 +9448,132 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "Detail",
+            Router: `/correlation/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "Download",
+            Router: `/correlation/download`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "Preview",
+            Router: `/correlation/preview`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "Refresh",
+            Router: `/correlation/refresh`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "Remove",
+            Router: `/correlation/remove`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "Save",
+            Router: `/correlation/save`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisCorrelationController"],
+        beego.ControllerComments{
+            Method: "SaveAs",
+            Router: `/correlation/save_as`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "Detail",
+            Router: `/table/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "Download",
+            Router: `/table/download`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "Preview",
+            Router: `/table/preview`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "Refresh",
+            Router: `/table/refresh`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "Remove",
+            Router: `/table/remove`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "Save",
+            Router: `/table/save`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:TradeAnalysisTableController"],
+        beego.ControllerComments{
+            Method: "SaveAs",
+            Router: `/table/save_as`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/trade_analysis:WarehouseClassifyController"],
         beego.ControllerComments{
             Method: "AddChartClassify",

+ 2 - 0
routers/router.go

@@ -302,6 +302,8 @@ func init() {
 				&trade_analysis.TradeAnalysisController{},
 				&trade_analysis.WarehouseClassifyController{},
 				&trade_analysis.WarehouseController{},
+				&trade_analysis.TradeAnalysisTableController{},
+				&trade_analysis.TradeAnalysisCorrelationController{},
 			),
 		),
 		web.NSNamespace("/custom_analysis",

+ 11 - 11
services/data/common_classify.go

@@ -1,7 +1,7 @@
 package data
 
 import (
-	"eta/eta_api/models"
+	"eta/eta_api/models/common"
 	"eta/eta_api/utils"
 	"fmt"
 	"strconv"
@@ -9,17 +9,17 @@ import (
 	"time"
 )
 
-func CommonClassifyMove(req models.CommonClassifyMoveReq, strategy CommonClassifyStrategy) (tips string, err error) {
+func CommonClassifyMove(req common.CommonClassifyMoveReq, strategy CommonClassifyStrategy) (tips string, err error) {
 	ctx := NewCommonClassifyCtx(strategy)
 
 	var (
-		classify       *models.CommonClassify
-		parentClassify *models.CommonClassify
-		prevClassify   *models.CommonClassify
-		nextClassify   *models.CommonClassify
-		object         *models.CommonClassifyObj
-		prevObject     *models.CommonClassifyObj
-		nextObject     *models.CommonClassifyObj
+		classify       *common.CommonClassify
+		parentClassify *common.CommonClassify
+		prevClassify   *common.CommonClassify
+		nextClassify   *common.CommonClassify
+		object         *common.CommonClassifyObj
+		prevObject     *common.CommonClassifyObj
+		nextObject     *common.CommonClassifyObj
 		sortPrev       int
 		sortNext       int
 	)
@@ -125,7 +125,7 @@ func CommonClassifyMove(req models.CommonClassifyMoveReq, strategy CommonClassif
 	return
 }
 
-func moveCommonClassify(ctx *CommonClassifyCtx, req models.CommonClassifyMoveReq, classify, parentClassify, prevClassify, nextClassify *models.CommonClassify, prevObject, nextObject *models.CommonClassifyObj, sortPrev, sortNext int) (tips string, err error) {
+func moveCommonClassify(ctx *CommonClassifyCtx, req common.CommonClassifyMoveReq, classify, parentClassify, prevClassify, nextClassify *common.CommonClassify, prevObject, nextObject *common.CommonClassifyObj, sortPrev, sortNext int) (tips string, err error) {
 	// 校验层级以及父级分类下同名分类
 	if req.ParentClassifyId > 0 && parentClassify.Level == 6 {
 		tips = "最高只支持添加6级分类"
@@ -321,7 +321,7 @@ func moveCommonClassify(ctx *CommonClassifyCtx, req models.CommonClassifyMoveReq
 	return
 }
 
-func moveCommonClassifyObj(ctx *CommonClassifyCtx, req models.CommonClassifyMoveReq, prevClassify, nextClassify *models.CommonClassify, object, prevObject, nextObject *models.CommonClassifyObj, sortPrev, sortNext int) (tips string, err error) {
+func moveCommonClassifyObj(ctx *CommonClassifyCtx, req common.CommonClassifyMoveReq, prevClassify, nextClassify *common.CommonClassify, object, prevObject, nextObject *common.CommonClassifyObj, sortPrev, sortNext int) (tips string, err error) {
 	var objUpdateCols []string
 	colsMapping := ctx.GetCommonClassifyObjCols() // 分类对象表实际的字段映射
 

+ 19 - 19
services/data/common_classify_ctx.go

@@ -1,28 +1,28 @@
 package data
 
 import (
-	"eta/eta_api/models"
+	"eta/eta_api/models/common"
 )
 
 // CommonClassifyStrategy 通用分类策略接口
 type CommonClassifyStrategy interface {
-	GetCommonClassifyCols() models.CommonClassifyCols
-	GetCommonClassifyById(classifyId int) (*models.CommonClassify, error)
-	GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*models.CommonClassify, error)
+	GetCommonClassifyCols() common.CommonClassifyCols
+	GetCommonClassifyById(classifyId int) (*common.CommonClassify, error)
+	GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*common.CommonClassify, error)
 	GetClassifySortMaxByParentId(parentId int) (int, error)
-	GetFirstClassifyByParentId(parentId int) (*models.CommonClassify, error)
+	GetFirstClassifyByParentId(parentId int) (*common.CommonClassify, error)
 
 	SetClassifySortByParentId(parentId, classifyId, sort int, sortUpdate string) error
-	UpdateCommonClassify(classify *models.CommonClassify, updateCols []string) error
+	UpdateCommonClassify(classify *common.CommonClassify, updateCols []string) error
 	UpdateClassifyChildByParentId(classifyIds []int, rootId int, stepLevel int) error
 
-	GetCommonClassifyObjCols() models.CommonClassifyObjCols
-	GetObjectById(objectId int) (*models.CommonClassifyObj, error)
+	GetCommonClassifyObjCols() common.CommonClassifyObjCols
+	GetObjectById(objectId int) (*common.CommonClassifyObj, error)
 	GetObjectSortMaxByClassifyId(classifyId int) (int, error)
-	GetFirstObjectByClassifyId(classifyId int) (*models.CommonClassifyObj, error)
+	GetFirstObjectByClassifyId(classifyId int) (*common.CommonClassifyObj, error)
 
 	SetObjectSortByClassifyId(classifyId, sort, prevObjectId int, sortUpdate string) error
-	UpdateCommonClassifyObj(object *models.CommonClassifyObj, updateCols []string) (err error)
+	UpdateCommonClassifyObj(object *common.CommonClassifyObj, updateCols []string) (err error)
 }
 
 // CommonClassifyCtx 通用分类上下文
@@ -36,17 +36,17 @@ func NewCommonClassifyCtx(strategy CommonClassifyStrategy) *CommonClassifyCtx {
 }
 
 // GetCommonClassifyCols 通用分类字段映射
-func (c *CommonClassifyCtx) GetCommonClassifyCols() models.CommonClassifyCols {
+func (c *CommonClassifyCtx) GetCommonClassifyCols() common.CommonClassifyCols {
 	return c.strategy.GetCommonClassifyCols()
 }
 
 // GetCommonClassifyById 通过策略获取分类信息
-func (c *CommonClassifyCtx) GetCommonClassifyById(classifyId int) (*models.CommonClassify, error) {
+func (c *CommonClassifyCtx) GetCommonClassifyById(classifyId int) (*common.CommonClassify, error) {
 	return c.strategy.GetCommonClassifyById(classifyId)
 }
 
 // GetClassifyByParentIdAndName 通过策略获取分类信息
-func (c *CommonClassifyCtx) GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*models.CommonClassify, error) {
+func (c *CommonClassifyCtx) GetClassifyByParentIdAndName(parentId int, name string, excludeId int) (*common.CommonClassify, error) {
 	return c.strategy.GetClassifyByParentIdAndName(parentId, name, excludeId)
 }
 
@@ -56,7 +56,7 @@ func (c *CommonClassifyCtx) GetClassifySortMaxByParentId(parentId int) (int, err
 }
 
 // GetFirstClassifyByParentId 获取父级分类下首个分类
-func (c *CommonClassifyCtx) GetFirstClassifyByParentId(parentId int) (*models.CommonClassify, error) {
+func (c *CommonClassifyCtx) GetFirstClassifyByParentId(parentId int) (*common.CommonClassify, error) {
 	return c.strategy.GetFirstClassifyByParentId(parentId)
 }
 
@@ -66,7 +66,7 @@ func (c *CommonClassifyCtx) SetClassifySortByParentId(parentId, classifyId, sort
 }
 
 // UpdateCommonClassify 更新通用分类
-func (c *CommonClassifyCtx) UpdateCommonClassify(classify *models.CommonClassify, updateCols []string) error {
+func (c *CommonClassifyCtx) UpdateCommonClassify(classify *common.CommonClassify, updateCols []string) error {
 	return c.strategy.UpdateCommonClassify(classify, updateCols)
 }
 
@@ -76,12 +76,12 @@ func (c *CommonClassifyCtx) UpdateClassifyChildByParentId(classifyIds []int, roo
 }
 
 // GetCommonClassifyObjCols 通用分类对象字段映射
-func (c *CommonClassifyCtx) GetCommonClassifyObjCols() models.CommonClassifyObjCols {
+func (c *CommonClassifyCtx) GetCommonClassifyObjCols() common.CommonClassifyObjCols {
 	return c.strategy.GetCommonClassifyObjCols()
 }
 
 // GetObjectById 获取分类对象
-func (c *CommonClassifyCtx) GetObjectById(objectId int) (*models.CommonClassifyObj, error) {
+func (c *CommonClassifyCtx) GetObjectById(objectId int) (*common.CommonClassifyObj, error) {
 	return c.strategy.GetObjectById(objectId)
 }
 
@@ -91,7 +91,7 @@ func (c *CommonClassifyCtx) GetObjectSortMaxByClassifyId(classifyId int) (int, e
 }
 
 // GetFirstObjectByClassifyId 获取分类下首个对象
-func (c *CommonClassifyCtx) GetFirstObjectByClassifyId(classifyId int) (*models.CommonClassifyObj, error) {
+func (c *CommonClassifyCtx) GetFirstObjectByClassifyId(classifyId int) (*common.CommonClassifyObj, error) {
 	return c.strategy.GetFirstObjectByClassifyId(classifyId)
 }
 
@@ -101,6 +101,6 @@ func (c *CommonClassifyCtx) SetObjectSortByClassifyId(classifyId, sort, prevObje
 }
 
 // UpdateCommonClassifyObj 更新分类对象
-func (c *CommonClassifyCtx) UpdateCommonClassifyObj(object *models.CommonClassifyObj, updateCols []string) (err error) {
+func (c *CommonClassifyCtx) UpdateCommonClassifyObj(object *common.CommonClassifyObj, updateCols []string) (err error) {
 	return c.strategy.UpdateCommonClassifyObj(object, updateCols)
 }

+ 21 - 0
services/data/edb_classify.go

@@ -750,6 +750,27 @@ func DeleteCheck(classifyId, edbInfoId int, sysUser *system.Admin) (deleteStatus
 				return
 			}
 		}
+
+		// 判断是否用于持仓分析-相关性标的指标
+		{
+			ob := new(excel.ExcelInfo)
+			cond := ` AND extra_config LIKE ?`
+			keyword := fmt.Sprintf(`"BaseEdbInfoId":%d`, edbInfoId)
+			keyword = fmt.Sprint("%", keyword, "%")
+			pars := make([]interface{}, 0)
+			pars = append(pars, keyword)
+			count, e := ob.GetCountByCondition(cond, pars)
+			if e != nil {
+				errMsg = "删除失败"
+				err = fmt.Errorf("获取指标是否用于持仓分析-相关性表格失败, err: %v", e)
+				return
+			}
+			if count > 0 {
+				deleteStatus = 3
+				tipsMsg = "当前指标已用于持仓分析相关性表格, 不可删除"
+				return
+			}
+		}
 	}
 	return
 }

+ 25 - 0
services/data/trade_analysis/trade_analysis.go

@@ -406,3 +406,28 @@ func GetPositionTopDetail(req trade_analysis.GetPositionTopReq) (ret trade_analy
 
 	return
 }
+
+// GetZhengzhouContractsByClassifyNames 郑商所-获取所选品种下的所有合约
+func GetZhengzhouContractsByClassifyNames(classifyNames []string) (contracts []string, err error) {
+	var cond string
+	var pars []interface{}
+	classifyOb := new(trade_analysis.BaseFromTradeClassify)
+	cond += fmt.Sprintf(` AND %s = ?`, classifyOb.Cols().Exchange)
+	pars = append(pars, trade_analysis.TradeExchangeZhengzhou)
+	fields := []string{classifyOb.Cols().ClassifyName, classifyOb.Cols().Exchange}
+	list, e := classifyOb.GetClassifyItemsByCondition(cond, pars, fields, "id ASC")
+	if e != nil {
+		err = fmt.Errorf("获取郑商所品种合约失败, %v", e)
+		return
+	}
+	for _, v := range list {
+		classifyName := GetZhengzhouClassifyName(v.ClassifyName)
+		if classifyName == "" {
+			continue
+		}
+		if utils.InArrayByStr(classifyNames, classifyName) {
+			contracts = append(contracts, v.ClassifyName)
+		}
+	}
+	return
+}

+ 417 - 0
services/data/trade_analysis/trade_analysis_correlation.go

@@ -0,0 +1,417 @@
+package trade_analysis
+
+import (
+	"eta/eta_api/models/data_manage"
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"math"
+	"time"
+)
+
+// CheckAnalysisCorrelationExtraConfig 校验相关性表格配置
+func CheckAnalysisCorrelationExtraConfig(extraConfig tradeAnalysisModel.CorrelationTableExtraConfig) (pass bool, tips string) {
+	if extraConfig.BaseEdbInfoId <= 0 {
+		tips = "请选择标的指标"
+		return
+	}
+	if extraConfig.Exchange == "" {
+		tips = "请选择交易所"
+		return
+	}
+	if extraConfig.ClassifyName == "" {
+		tips = "请选择品种"
+		return
+	}
+	if len(extraConfig.CompanyNames) == 0 {
+		tips = "请选择期货公司"
+		return
+	}
+	if len(extraConfig.CompanyNames) > 5 {
+		tips = "最多选择5个期货公司"
+		return
+	}
+	typeArr := []int{tradeAnalysisModel.ContractQueryTypeTop, tradeAnalysisModel.ContractQueryTypeTop2, tradeAnalysisModel.ContractQueryTypeTop3, tradeAnalysisModel.ContractQueryTypeAll, tradeAnalysisModel.ContractQueryTypeTotal}
+	if !utils.InArrayByInt(typeArr, extraConfig.ContractType) {
+		tips = "请选择正确的合约"
+		return
+	}
+	positionArr := []int{tradeAnalysisModel.ContractPositionBuy, tradeAnalysisModel.ContractPositionSold, tradeAnalysisModel.ContractPositionPureBuy}
+	if !utils.InArrayByInt(positionArr, extraConfig.ContractPosition) {
+		tips = "请选择正确的合约方向"
+		return
+	}
+	if len(extraConfig.RollConfig) == 0 {
+		tips = "请设置滚动相关性配置"
+		return
+	}
+	for _, v := range extraConfig.RollConfig {
+		if v.CalculateValue < 2 || v.CalculateValue > 200 {
+			tips = "计算窗口请输入2-200的整数"
+			return
+		}
+		if v.LeadValue < 0 || v.LeadValue > 60 {
+			tips = "B领先A请输入0-60的整数"
+			return
+		}
+	}
+	if extraConfig.PredictRatio < 0 || extraConfig.PredictRatio > 1 {
+		tips = "请输入正确的估计参数"
+		return
+	}
+	pass = true
+	return
+}
+
+// GetCorrelationTableRowsDataByConfig 根据配置获取相关性表格数据
+func GetCorrelationTableRowsDataByConfig(tableConfig tradeAnalysisModel.CorrelationTableExtraConfig) (tables []*tradeAnalysisModel.CorrelationTableData, err error) {
+	// 获取标的指标数据
+	baseDateData := make(map[time.Time]float64)
+	{
+		baseEdb, e := data_manage.GetChartEdbMappingByEdbInfoId(tableConfig.BaseEdbInfoId)
+		if e != nil {
+			err = fmt.Errorf("获取持仓相关性, 标的指标mapping信息失败, %v", e)
+			return
+		}
+		baseEdbData := make([]*data_manage.EdbDataList, 0)
+		baseEdbData, e = data_manage.GetEdbDataList(baseEdb.Source, baseEdb.SubSource, baseEdb.EdbInfoId, "", "")
+		if e != nil {
+			err = fmt.Errorf("获取标的指标数据失败, %v", e)
+			return
+		}
+		for _, v := range baseEdbData {
+			t, e := time.ParseInLocation(utils.FormatDate, v.DataTime, time.Local)
+			if e != nil {
+				continue
+			}
+			baseDateData[t] = v.Value
+		}
+	}
+
+	// 获取品种最新数据日期
+	baseDate, e := GetTradeClassifyNewestDataTime(tableConfig.Exchange, []string{tableConfig.ClassifyName})
+	if e != nil {
+		err = fmt.Errorf("获取持仓品种最新数据日期失败, %v", e)
+		return
+	}
+
+	// 根据配置取出需要查询的合约
+	contracts, e := GetTopContractsByType(tableConfig.ContractType, tableConfig.Exchange, tableConfig.ClassifyName, baseDate)
+	if e != nil {
+		err = fmt.Errorf("获取持仓相关性合约失败, %v", e)
+		return
+	}
+
+	// [合约+期货公司]持仓数据
+	tradeData, e := GetCorrelationTableTradeData(tableConfig.Exchange, []string{tableConfig.ClassifyName}, contracts, tableConfig.CompanyNames, tableConfig.PredictRatio)
+	if e != nil {
+		err = fmt.Errorf("获取[合约+期货公司]持仓数据失败, %v", e)
+		return
+	}
+
+	// 持仓数据转为指标
+	tradeEdbList, tradeEdbDataMap, e := TransTradeData2EdbData(tradeData, tableConfig.ContractPosition)
+	if e != nil {
+		err = fmt.Errorf("[合约+期货公司]持仓数据转为指标失败, %v", e)
+		return
+	}
+
+	// 遍历相关性配置(目前最多有两个滚动相关性), 遍历指标, 分别计算滚动相关性
+	maxPre := 10 // 需要展示的以标的指标日期为准往前推N个交易日的滚动相关性
+	startDate := baseDate.AddDate(0, 0, -maxPre)
+	endDate := baseDate
+	rowDays := utils.GetTradingDays(baseDate.AddDate(0, 0, -20), baseDate) // 从20个自然日里截取11个数据对应的交易日,且当日排最前,后面依次为0、-1到-10
+	rowDays = utils.ReverseTimeSlice(rowDays)
+	rowDays = rowDays[:11]
+	tables = make([]*tradeAnalysisModel.CorrelationTableData, 0)
+	for _, rc := range tableConfig.RollConfig {
+		tableData := new(tradeAnalysisModel.CorrelationTableData)
+		tableData.CalculateValue = rc.CalculateValue
+		tableData.LeadValue = rc.LeadValue
+		tableData.RowsData = make([]*tradeAnalysisModel.CorrelationTableRowData, 0)
+
+		for kd, kv := range tradeEdbList {
+			row := new(tradeAnalysisModel.CorrelationTableRowData)
+			row.Exchange = kv.Exchange
+			row.CompanyName = kv.CompanyName
+			row.ClassifyName = kv.ClassifyName
+			row.ClassifyType = kv.ClassifyType
+			// 数据为合约加总时名称为合约名,否则为合约+期货公司+方向
+			if kv.IsTotal {
+				row.RowName = kv.ClassifyType
+			} else {
+				row.RowName = fmt.Sprintf("%s%s%s", kv.ClassifyType, kv.CompanyName, tradeAnalysisModel.WarehouseTypeSuffixNames[kv.ContractPosition])
+			}
+			row.DayData = make([]*tradeAnalysisModel.CorrelationTableRowDayData, 0)
+
+			// 这里加行数据
+			rd, e := CalculateRollCorrelationData(rc.CalculateValue, rc.LeadValue, baseDateData, tradeEdbDataMap[kd], startDate, endDate)
+			if e != nil {
+				err = fmt.Errorf("计算行数据-滚动相关性失败, %v", e)
+				return
+			}
+			for day, dt := range rowDays {
+				// 这里展示要加负号
+				var showDay int
+				if day == 0 {
+					showDay = 0
+				} else {
+					showDay = -day
+				}
+				row.DayData = append(row.DayData, &tradeAnalysisModel.CorrelationTableRowDayData{
+					Day:      showDay,
+					DataDate: dt.Format(utils.FormatDate),
+					DataVal:  rd[dt], // 对应日期无值的话就是0,无相关性
+				})
+			}
+			tableData.RowsData = append(tableData.RowsData, row)
+		}
+		tables = append(tables, tableData)
+	}
+	return
+}
+
+// GetTopContractsByType 根据配置获取需要取的合约
+func GetTopContractsByType(contractType int, exchange, classifyName string, baseDate time.Time) (contracts []string, err error) {
+	// 查询基准日期TOP20合约排名, 根据类型取出合约数
+	exchangeContracts := make(map[string][]*tradeAnalysisModel.ContractTopRankData) // 交易所最终取的合约
+	var contractMax int                                                             // 需要取出的品种对应的最大合约数
+	classifyMax := make(map[string]int)                                             // 品种对应的合约数
+	switch contractType {
+	case tradeAnalysisModel.ContractQueryTypeTop:
+		contractMax = 1
+	case tradeAnalysisModel.ContractQueryTypeTop2:
+		contractMax = 2
+	case tradeAnalysisModel.ContractQueryTypeTop3:
+		contractMax = 3
+	case tradeAnalysisModel.ContractQueryTypeAll, tradeAnalysisModel.ContractQueryTypeTotal:
+		contractMax = 999
+	}
+
+	// 遍历交易所, 查询各品种下的合约排名情况及TOP当日的多空单数据
+	contractRanks, e := GetTopContractRank(exchange, []string{classifyName}, baseDate)
+	if e != nil {
+		err = fmt.Errorf("获取基准日期合约排名失败, %v", e)
+		return
+	}
+
+	// ps.正常来讲这里查出来的合约是唯一的, 根据品种分组, 取出ContractType所需的合约数
+	for _, v := range contractRanks {
+		if classifyMax[v.ClassifyName] >= contractMax {
+			continue
+		}
+		classifyMax[v.ClassifyName] += 1
+		if exchangeContracts[v.Exchange] == nil {
+			exchangeContracts[v.Exchange] = make([]*tradeAnalysisModel.ContractTopRankData, 0)
+		}
+		contracts = append(contracts, v.ClassifyType)
+	}
+	return
+}
+
+// CalculateRollCorrelationData 计算滚动相关性
+func CalculateRollCorrelationData(calculateValue, leadValue int, baseEdbData map[time.Time]float64, changeEdbData map[time.Time]int, startDate, endDate time.Time) (dateRatio map[time.Time]float64, err error) {
+	//dataList = make([]*data_manage.EdbDataList, 0)
+
+	// 计算窗口,不包含第一天
+	//startDateTime, _ := time.ParseInLocation(utils.FormatDate, startDate, time.Local)
+	//startDate = startDateTime.AddDate(0, 0, 1).Format(utils.FormatDate)
+	startDate = startDate.AddDate(0, 0, 1)
+
+	//baseEdbInfo := edbInfoMappingA
+	//changeEdbInfo := edbInfoMappingB
+
+	// 获取时间基准指标在时间区间内的值
+	//aDataList := make([]*data_manage.EdbDataList, 0)
+	//switch baseEdbInfo.EdbInfoCategoryType {
+	//case 0:
+	//	aDataList, err = data_manage.GetEdbDataList(baseEdbInfo.Source, baseEdbInfo.SubSource, baseEdbInfo.EdbInfoId, startDate, endDate)
+	//case 1:
+	//	_, aDataList, _, _, err, _ = data.GetPredictDataListByPredictEdbInfoId(baseEdbInfo.EdbInfoId, startDate, endDate, true)
+	//default:
+	//	err = errors.New("指标base类型异常")
+	//	return
+	//}
+
+	// 获取变频指标所有日期的值, 插值法完善数据
+	//bDataList := make([]*data_manage.EdbDataList, 0)
+	//switch changeEdbInfo.EdbInfoCategoryType {
+	//case 0:
+	//	bDataList, err = data_manage.GetEdbDataList(changeEdbInfo.Source, changeEdbInfo.SubSource, changeEdbInfo.EdbInfoId, "", "")
+	//case 1:
+	//	_, bDataList, _, _, err, _ = data.GetPredictDataListByPredictEdbInfoId(changeEdbInfo.EdbInfoId, "", "", false)
+	//default:
+	//	err = errors.New("指标change类型异常")
+	//	return
+	//}
+
+	// 数据平移变频指标领先/滞后的日期(单位天)
+	// 2023-03-17 时间序列始终以指标A为基准, 始终是B进行平移
+	//baseDataList := make([]*data_manage.EdbDataList, 0)
+	//baseDataMap := make(map[string]float64)
+	//changeDataList := make([]*data_manage.EdbDataList, 0)
+	changeDataMap := make(map[time.Time]float64)
+
+	// A指标不管三七二十一,先变个频再说
+	//{
+	//	_, e := HandleDataByLinearRegression(aDataList, baseDataMap)
+	//	if e != nil {
+	//		err = fmt.Errorf("获取变频指标插值法Map失败, Err: %s", e.Error())
+	//		return
+	//	}
+	//	//baseDataList = tmpNewChangeDataList
+	//}
+	// B指标不管三七二十一,先变个频再说
+	//{
+	//	tmpNewChangeDataList, e := HandleDataByLinearRegression(bDataList, changeDataMap)
+	//	if e != nil {
+	//		err = fmt.Errorf("获取变频指标插值法Map失败, Err: %s", e.Error())
+	//		return
+	//	}
+	//	changeDataList = tmpNewChangeDataList
+	//
+	//	// 平移下日期
+	//	moveUnitDays := utils.FrequencyDaysMap[leadUnit]
+	//	_, changeDataMap = MoveDataDaysToNewDataList(changeDataList, leadValue*moveUnitDays)
+	//}
+
+	// TODO:平移下日期
+	//moveUnitDays := utils.FrequencyDaysMap[leadUnit]
+	//_, changeDataMap = MoveDataDaysToNewDataList(changeDataList, leadValue)
+	changeDataMap = MoveDataDaysToNewDataList(changeEdbData, leadValue)
+
+	// 计算计算时,需要多少个日期内数据
+	//calculateDay := utils.FrequencyDaysMap[calculateUnit] * calculateValue
+	calculateDay := calculateValue
+
+	var minRatio, maxRatio float64
+	dateRatio = make(map[time.Time]float64)
+	// 计算 每个日期的相关性值
+	{
+		startDateTime := startDate
+		//startDateTime, _ := time.ParseInLocation(utils.FormatDate, startDate, time.Local)
+		//if endDate == `` {
+		//	endDate = baseEdbInfo.EndDate
+		//}
+		//endDateTime, _ := time.ParseInLocation(utils.FormatDate, endDate, time.Local)
+		endDateTime := endDate.AddDate(0, 0, -(calculateDay - 1))
+
+		// 是否开始第一条数据
+		var isStart, isNotFirst bool
+		for currDay := startDateTime; !currDay.After(endDateTime); currDay = currDay.AddDate(0, 0, 1) {
+			yCalculateData := make([]float64, 0)
+			baseCalculateData := make([]float64, 0)
+
+			// 取出对应的基准日期的值
+			for i := 0; i < calculateDay; i++ {
+				//iDay := currDay.AddDate(0, 0, i).Format(utils.FormatDate)
+				iDay := currDay.AddDate(0, 0, i)
+
+				tmpBaseValue, ok1 := baseEdbData[iDay]
+				tmpChangeValue, ok2 := changeDataMap[iDay]
+				if !ok1 || !ok2 {
+					continue
+				}
+				baseCalculateData = append(baseCalculateData, tmpBaseValue)
+				yCalculateData = append(yCalculateData, tmpChangeValue)
+			}
+
+			// 没有数据的话,那就不返回
+			if len(baseCalculateData) == 0 {
+				continue
+			}
+			// 公式计算出领先/滞后频度对应点的相关性系数
+			ratio := utils.CalculateCorrelationByIntArr(baseCalculateData, yCalculateData)
+
+			// 过滤前面都是0的数据
+			{
+				if ratio != 0 {
+					isStart = true
+				}
+
+				if !isStart {
+					continue
+				}
+			}
+
+			dataTime := currDay.AddDate(0, 0, calculateDay-1)
+			//dataList = append(dataList, data_manage.EdbDataList{
+			//	//EdbDataId:     0,
+			//	EdbInfoId:     0,
+			//	DataTime:      dataTime.Format(utils.FormatDate),
+			//	DataTimestamp: dataTime.UnixNano() / 1e6,
+			//	Value:         ratio,
+			//})
+			// 保留4位小数
+			ratio = math.Round(ratio*10000) / 10000
+			dateRatio[dataTime] = ratio
+
+			if !isNotFirst {
+				minRatio = ratio
+				maxRatio = ratio
+				isNotFirst = true
+			}
+			if minRatio > ratio {
+				minRatio = ratio
+			}
+			if maxRatio < ratio {
+				maxRatio = ratio
+			}
+		}
+		//dataResp.DataList = dataList
+	}
+	return
+}
+
+// MoveDataDaysToNewDataList 平移指标数据生成新的数据序列
+func MoveDataDaysToNewDataList(originDateData map[time.Time]int, moveDay int) (newDateData map[time.Time]float64) {
+	// 取出最早最晚的日期
+	var minDate, maxDate time.Time
+	for k, _ := range originDateData {
+		if minDate.IsZero() || k.Before(minDate) {
+			minDate = k
+		}
+		if maxDate.IsZero() || k.After(maxDate) {
+			maxDate = k
+		}
+	}
+
+	// 处理领先、滞后数据
+	newDateMap := make(map[time.Time]float64)
+	for currDate, value := range originDateData {
+		newDate := currDate.AddDate(0, 0, moveDay)
+		newDateMap[newDate] = float64(value)
+	}
+	minDate = minDate.AddDate(0, 0, moveDay)
+	maxDate = maxDate.AddDate(0, 0, moveDay)
+
+	// 创建一个有序的数据序列, 用于处理找不到数据时取前个交易日值的情况
+	type OrderedData struct {
+		DataTime time.Time
+		Val      float64
+	}
+	newDataList := make([]*OrderedData, 0)
+
+	// 获取日期相差日
+	dayNum := utils.GetTimeSubDay(minDate, maxDate)
+	newDateData = make(map[time.Time]float64)
+	for i := 0; i <= dayNum; i++ {
+		currDate := minDate.AddDate(0, 0, i)
+		tmpValue, ok := newDateMap[currDate]
+		if !ok {
+			//找不到数据,那么就用前面的数据吧
+			if len(newDataList)-1 < 0 {
+				tmpValue = 0
+			} else {
+				tmpValue = newDataList[len(newDataList)-1].Val
+			}
+		}
+		tmpData := &OrderedData{
+			DataTime: currDate,
+			Val:      tmpValue,
+		}
+		newDateData[currDate] = tmpData.Val
+		newDataList = append(newDataList, tmpData)
+	}
+	return
+}

+ 410 - 132
services/data/trade_analysis/trade_analysis_data.go

@@ -144,144 +144,18 @@ func FormatCompanyTradeData2EdbMappings(companyTradeData []*tradeAnalysisModel.C
 	return
 }
 
-func GetOriginTradeData(exchange, classifyName string, contracts, companies []string, predictRatio float64) (companyTradeData []*tradeAnalysisModel.ContractCompanyTradeData, err error) {
-	// 各原始数据表期货公司名称不一致
-	companyMap := make(map[string]string)
-	{
-		ob := new(tradeAnalysisModel.TradeFuturesCompany)
-		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
-		if e != nil {
-			err = fmt.Errorf("获取期货公司名称失败: %v", e)
-			return
-		}
-		switch exchange {
-		case "zhengzhou":
-			for _, v := range list {
-				companyMap[v.CompanyName] = v.ZhengzhouName
-			}
-		case "dalian":
-			for _, v := range list {
-				companyMap[v.CompanyName] = v.DalianName
-			}
-		case "shanghai":
-			for _, v := range list {
-				companyMap[v.CompanyName] = v.ShanghaiName
-			}
-		case "cffex":
-			for _, v := range list {
-				companyMap[v.CompanyName] = v.CffexName
-			}
-		case "ine":
-			for _, v := range list {
-				companyMap[v.CompanyName] = v.IneName
-			}
-		case "guangzhou":
-			for _, v := range list {
-				companyMap[v.CompanyName] = v.GuangzhouName
-			}
-		}
-	}
-	var queryCompanies []string
-	for _, v := range companies {
-		if v == tradeAnalysisModel.TradeFuturesCompanyTop20 {
-			queryCompanies = append(queryCompanies, tradeAnalysisModel.TradeFuturesCompanyTop20)
-			continue
-		}
-		companyName, ok := companyMap[v]
-		if !ok {
-			utils.FileLog.Info(fmt.Sprintf("交易所%s公司名称映射不存在: %s", exchange, v))
-			continue
-		}
-		queryCompanies = append(queryCompanies, companyName)
-	}
-
-	// 郑商所/广期所查询方式不一样
-	var tradeAnalysis TradeAnalysisInterface
-	switch exchange {
-	case tradeAnalysisModel.TradeExchangeZhengzhou:
-		tradeAnalysis = &ZhengzhouTradeAnalysis{}
-	case tradeAnalysisModel.TradeExchangeGuangzhou:
-		tradeAnalysis = &GuangzhouTradeAnalysis{}
-	default:
-		tradeAnalysis = &BaseTradeAnalysis{}
-	}
-
-	// 获取多单/空单原始数据
-	originList, e := tradeAnalysis.GetTradeDataByClassifyAndCompany(exchange, classifyName, contracts, queryCompanies)
+func GetWarehouseTradeData(exchange, classifyName string, contracts, companies []string, predictRatio float64) (companyTradeData []*tradeAnalysisModel.ContractCompanyTradeData, err error) {
+	// 获取合约持仓数据
+	contractTradeData, lastBuyVal, lastSoldVal, e := GetContractCompanyTradeData(exchange, []string{classifyName}, contracts, companies, time.Time{}, time.Time{})
 	if e != nil {
-		err = fmt.Errorf("获取多空单原始数据失败, %v", e)
+		err = fmt.Errorf("获取合约-持仓数据失败, %v", e)
 		return
 	}
 
-	keyItems := make(map[string]*tradeAnalysisModel.ContractCompanyTradeData)
-	keyDateData := make(map[string]*tradeAnalysisModel.ContractCompanyTradeDataList)
-	keyDateDataExist := make(map[string]bool)
-	for _, v := range originList {
-		// Rank999和0对应的是TOP20
-		companyName := v.CompanyName
-		if v.Rank == 999 || v.Rank == 0 {
-			companyName = tradeAnalysisModel.TradeFuturesCompanyTop20
-		}
-
-		k := fmt.Sprintf("%s-%s", v.ClassifyType, companyName)
-		if keyItems[k] == nil {
-			keyItems[k] = new(tradeAnalysisModel.ContractCompanyTradeData)
-			keyItems[k].CompanyName = companyName
-			keyItems[k].ClassifyType = v.ClassifyType
-			keyItems[k].DataList = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
-		}
-
-		kd := fmt.Sprintf("%s-%s", k, v.DataTime.Format(utils.FormatDate))
-		if keyDateData[kd] == nil {
-			keyDateData[kd] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
-			keyDateData[kd].Date = v.DataTime
-		}
-		if v.ValType == 1 {
-			keyDateData[kd].BuyVal = v.Val
-			keyDateData[kd].BuyValType = tradeAnalysisModel.TradeDataTypeOrigin
-			keyDateData[kd].BuyChange = v.ValChange
-			keyDateData[kd].BuyChangeType = tradeAnalysisModel.TradeDataTypeOrigin
-		}
-		if v.ValType == 2 {
-			keyDateData[kd].SoldVal = v.Val
-			keyDateData[kd].SoldValType = tradeAnalysisModel.TradeDataTypeOrigin
-			keyDateData[kd].SoldChange = v.ValChange
-			keyDateData[kd].SoldChangeType = tradeAnalysisModel.TradeDataTypeOrigin
-		}
-		if !keyDateDataExist[kd] {
-			keyItems[k].DataList = append(keyItems[k].DataList, keyDateData[kd])
-			keyDateDataExist[kd] = true
-		}
-	}
-
-	// 获取[合约]每日的末位多空单
-	contractLastBuyDateVal := make(map[string]map[time.Time]int)
-	contractLastSoldDateVal := make(map[string]map[time.Time]int)
-	{
-		lastOriginList, e := tradeAnalysis.GetLastTradeDataByClassify(exchange, classifyName, contracts)
-		if e != nil {
-			err = fmt.Errorf("获取末位多空单原始数据失败, %v", e)
-			return
-		}
-		for _, v := range lastOriginList {
-			if v.ValType == 1 {
-				if contractLastBuyDateVal[v.ClassifyType] == nil {
-					contractLastBuyDateVal[v.ClassifyType] = make(map[time.Time]int)
-				}
-				contractLastBuyDateVal[v.ClassifyType][v.DataTime] = v.Val
-				continue
-			}
-			if contractLastSoldDateVal[v.ClassifyType] == nil {
-				contractLastSoldDateVal[v.ClassifyType] = make(map[time.Time]int)
-			}
-			contractLastSoldDateVal[v.ClassifyType][v.DataTime] = v.Val
-		}
-	}
-
 	// 填充[合约-公司]预估数据, 并根据[公司-多合约]分组, [公司]算作一个指标, 指标值为[多个合约]的计算加总
 	companyContracts := make(map[string][]*tradeAnalysisModel.ContractCompanyTradeData)
-	for _, v := range keyItems {
-		td, fd, ed, e := PredictingTradeData(v.DataList, contractLastBuyDateVal[v.ClassifyType], contractLastSoldDateVal[v.ClassifyType], predictRatio)
+	for _, v := range contractTradeData {
+		td, fd, ed, e := PredictingTradeData(v.DataList, lastBuyVal[v.ClassifyType], lastSoldVal[v.ClassifyType], predictRatio)
 		if e != nil {
 			err = fmt.Errorf("数据补全失败, %v", e)
 			return
@@ -644,3 +518,407 @@ func PredictingTradeData(originData []*tradeAnalysisModel.ContractCompanyTradeDa
 	}
 	return
 }
+
+// GetTopContractRank 获取TOP20根据成交量的合约排名
+func GetTopContractRank(exchange string, classifyNames []string, dataDate time.Time) (items []*tradeAnalysisModel.ContractTopRankData, err error) {
+	// 郑商所/广期所查询方式不一样
+	var tradeAnalysis TradeAnalysisInterface
+	switch exchange {
+	case tradeAnalysisModel.TradeExchangeZhengzhou:
+		tradeAnalysis = &ZhengzhouTradeAnalysis{}
+	case tradeAnalysisModel.TradeExchangeGuangzhou:
+		tradeAnalysis = &GuangzhouTradeAnalysis{}
+	default:
+		tradeAnalysis = &BaseTradeAnalysis{}
+	}
+
+	// 郑商所-需要把所选品种转为实际合约进行后续的查询
+	if exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
+		classifies, e := GetZhengzhouContractsByClassifyNames(classifyNames)
+		if e != nil {
+			err = fmt.Errorf("获取郑商所实际合约失败, %v", e)
+			return
+		}
+		classifyNames = classifies
+	}
+
+	// 获取多单/空单原始数据
+	rankData, e := tradeAnalysis.GetContractTopRankData(exchange, classifyNames, dataDate)
+	if e != nil {
+		err = fmt.Errorf("获取多空单原始数据失败, %v", e)
+		return
+	}
+	items = make([]*tradeAnalysisModel.ContractTopRankData, 0)
+	for _, v := range rankData {
+		v.Exchange = exchange
+		// 郑商所-这里注意把查出来的品种和合约赋值,不然后续是乱的
+		if v.Exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
+			v.ClassifyType = v.ClassifyName
+			v.ClassifyName = GetZhengzhouClassifyName(v.ClassifyName)
+		}
+		items = append(items, v)
+	}
+	return
+}
+
+// GetTableTradeData 获取多空分析表格持仓数据
+func GetTableTradeData(exchange string, classifyName string, contracts []string, companyName string, predictRatio float64, startDate, endDate time.Time, contractType int) (companyTradeData []*tradeAnalysisModel.ContractCompanyTradeData, err error) {
+	companyTradeData = make([]*tradeAnalysisModel.ContractCompanyTradeData, 0)
+
+	// 获取合约持仓数据
+	contractTradeData, lastBuyVal, lastSoldVal, e := GetContractCompanyTradeData(exchange, []string{classifyName}, contracts, []string{companyName}, startDate, endDate)
+	if e != nil {
+		err = fmt.Errorf("获取合约-持仓数据失败, %v", e)
+		return
+	}
+
+	// 填充[合约-公司]预估数据, 并根据[公司-多合约]分组, [公司]算作一个指标, 指标值为[多个合约]的计算加总
+	companyContracts := make(map[string][]*tradeAnalysisModel.ContractCompanyTradeData)
+	for _, v := range contractTradeData {
+		td, fd, ed, e := PredictingTradeData(v.DataList, lastBuyVal[v.ClassifyType], lastSoldVal[v.ClassifyType], predictRatio)
+		if e != nil {
+			err = fmt.Errorf("数据补全失败, %v", e)
+			return
+		}
+		v.DataList = td
+		v.StartDate = fd
+		v.EndDate = ed
+
+		// 合约类型参数不为合约加总时, 每个合约算一行数据
+		if contractType != tradeAnalysisModel.ContractQueryTypeTotal {
+			companyTradeData = append(companyTradeData, v)
+			continue
+		}
+
+		// 往下计算合约加总
+		if companyContracts[v.CompanyName] == nil {
+			companyContracts[v.CompanyName] = make([]*tradeAnalysisModel.ContractCompanyTradeData, 0)
+		}
+		companyContracts[v.CompanyName] = append(companyContracts[v.CompanyName], v)
+	}
+
+	// 类型为合约加总才往下合并
+	if contractType != tradeAnalysisModel.ContractQueryTypeTotal {
+		return
+	}
+
+	// 以[公司]为组, 计算合约加总
+	for k, v := range companyContracts {
+		companyData := new(tradeAnalysisModel.ContractCompanyTradeData)
+		companyData.CompanyName = k
+		companyData.DataList = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+		contractArr := make([]string, 0)
+
+		// 合约加总
+		sumDateData := make(map[time.Time]*tradeAnalysisModel.ContractCompanyTradeDataList)
+		for _, vv := range v {
+			contractArr = append(contractArr, vv.ClassifyType)
+			for _, dv := range vv.DataList {
+				if sumDateData[dv.Date] == nil {
+					sumDateData[dv.Date] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+					sumDateData[dv.Date].Date = dv.Date
+				}
+				// 数据类型以第一个非零值为准, 只处理多空和净多, 变化就不管了
+				if sumDateData[dv.Date].BuyValType == tradeAnalysisModel.TradeDataTypeNull && dv.BuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].BuyValType = dv.BuyValType
+				}
+				if sumDateData[dv.Date].BuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dv.BuyValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					sumDateData[dv.Date].BuyValType = dv.BuyValType
+				}
+				if dv.BuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].BuyVal += dv.BuyVal
+				}
+				// 空单
+				if sumDateData[dv.Date].SoldValType == tradeAnalysisModel.TradeDataTypeNull && dv.SoldValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].SoldValType = dv.SoldValType
+				}
+				if sumDateData[dv.Date].SoldValType == tradeAnalysisModel.TradeDataTypeOrigin && dv.SoldValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					sumDateData[dv.Date].SoldValType = dv.SoldValType
+				}
+				if dv.SoldValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].SoldVal += dv.SoldVal
+				}
+				// 净多单
+				if sumDateData[dv.Date].PureBuyValType == tradeAnalysisModel.TradeDataTypeNull && dv.PureBuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].PureBuyValType = dv.PureBuyValType
+				}
+				if sumDateData[dv.Date].PureBuyValType == tradeAnalysisModel.TradeDataTypeOrigin && dv.PureBuyValType == tradeAnalysisModel.TradeDataTypeCalculate {
+					sumDateData[dv.Date].PureBuyValType = dv.PureBuyValType
+				}
+				if dv.PureBuyValType > tradeAnalysisModel.TradeDataTypeNull {
+					sumDateData[dv.Date].PureBuyVal += dv.PureBuyVal
+				}
+			}
+
+			// 多个合约比对开始结束时间
+			if companyData.StartDate.IsZero() {
+				companyData.StartDate = vv.StartDate
+			}
+			if vv.StartDate.Before(companyData.StartDate) {
+				companyData.StartDate = vv.StartDate
+			}
+			if companyData.EndDate.IsZero() {
+				companyData.EndDate = vv.EndDate
+			}
+			if vv.EndDate.Before(companyData.EndDate) {
+				companyData.EndDate = vv.EndDate
+			}
+		}
+		for _, sv := range sumDateData {
+			companyData.DataList = append(companyData.DataList, sv)
+		}
+		sort.Slice(companyData.DataList, func(i, j int) bool {
+			return companyData.DataList[i].Date.Before(companyData.DataList[j].Date)
+		})
+		//companyData.ClassifyType = strings.Join(contractArr, ",")
+		companyData.Exchange = exchange
+		companyData.CompanyName = classifyName
+		companyData.ClassifyType = classifyName
+		companyTradeData = append(companyTradeData, companyData)
+	}
+	return
+}
+
+// GetCorrelationTableTradeData 获取相关性表格持仓数据
+func GetCorrelationTableTradeData(exchange string, classifyNames, contracts, companies []string, predictRatio float64) (tradeData []*tradeAnalysisModel.ContractCompanyTradeData, err error) {
+	// 获取合约持仓数据
+	contractTradeData, lastBuyVal, lastSoldVal, e := GetContractCompanyTradeData(exchange, classifyNames, contracts, companies, time.Time{}, time.Time{})
+	if e != nil {
+		err = fmt.Errorf("获取合约-持仓数据失败, %v", e)
+		return
+	}
+
+	// 填充预估数据
+	tradeData = make([]*tradeAnalysisModel.ContractCompanyTradeData, 0)
+	for _, v := range contractTradeData {
+		td, fd, ed, e := PredictingTradeData(v.DataList, lastBuyVal[v.ClassifyType], lastSoldVal[v.ClassifyType], predictRatio)
+		if e != nil {
+			err = fmt.Errorf("数据补全失败, %v", e)
+			return
+		}
+		v.DataList = td
+		v.StartDate = fd
+		v.EndDate = ed
+		tradeData = append(tradeData, v)
+	}
+	return
+}
+
+// TransTradeData2EdbData 持仓数据转为指标数据
+func TransTradeData2EdbData(tradeData []*tradeAnalysisModel.ContractCompanyTradeData, contractPosition int) (edbData []*tradeAnalysisModel.ContractCompanyTradeEdb, edbDataMap []map[time.Time]int, err error) {
+	if len(tradeData) == 0 {
+		return
+	}
+	edbData = make([]*tradeAnalysisModel.ContractCompanyTradeEdb, 0)
+	edbDataMap = make([]map[time.Time]int, 0)
+
+	for _, v := range tradeData {
+		newEdb := new(tradeAnalysisModel.ContractCompanyTradeEdb)
+		newEdb.Exchange = v.Exchange
+		newEdb.ClassifyName = v.ClassifyName
+		newEdb.ClassifyType = v.ClassifyType
+		newEdb.CompanyName = v.CompanyName
+		newEdb.IsTotal = v.IsTotal
+		newEdb.ContractPosition = contractPosition
+		newEdb.StartDate = v.StartDate
+		newEdb.EndDate = v.EndDate
+		newEdb.DataList = make([]*tradeAnalysisModel.ContractCompanyTradeEdbData, 0)
+		dataMap := make(map[time.Time]int)
+		for _, d := range v.DataList {
+			var vd int
+			switch contractPosition {
+			case tradeAnalysisModel.ContractPositionBuy:
+				if d.BuyValType == tradeAnalysisModel.TradeDataTypeNull {
+					continue
+				}
+				vd = d.BuyVal
+			case tradeAnalysisModel.ContractPositionSold:
+				if d.SoldValType == tradeAnalysisModel.TradeDataTypeNull {
+					continue
+				}
+				vd = d.SoldVal
+			case tradeAnalysisModel.ContractPositionPureBuy:
+				if d.PureBuyValType == tradeAnalysisModel.TradeDataTypeNull {
+					continue
+				}
+				vd = d.PureBuyVal
+			default:
+				continue
+			}
+			newEdb.DataList = append(newEdb.DataList, &tradeAnalysisModel.ContractCompanyTradeEdbData{
+				DataTime: d.Date,
+				Val:      vd,
+			})
+			dataMap[d.Date] = vd
+		}
+		edbData = append(edbData, newEdb)
+		edbDataMap = append(edbDataMap, dataMap)
+	}
+	return
+}
+
+// GetContractCompanyTradeData 获取合约持仓数据
+func GetContractCompanyTradeData(exchange string, classifyNames, contracts, companies []string, startDate, endDate time.Time) (contractTradeData map[string]*tradeAnalysisModel.ContractCompanyTradeData, lastBuyVal, lastSoldVal map[string]map[time.Time]int, err error) {
+	// 各原始数据表期货公司名称不一致
+	companyMap := make(map[string]string)
+	{
+		ob := new(tradeAnalysisModel.TradeFuturesCompany)
+		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取期货公司名称失败: %v", e)
+			return
+		}
+		switch exchange {
+		case "zhengzhou":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.ZhengzhouName
+			}
+		case "dalian":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.DalianName
+			}
+		case "shanghai":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.ShanghaiName
+			}
+		case "cffex":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.CffexName
+			}
+		case "ine":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.IneName
+			}
+		case "guangzhou":
+			for _, v := range list {
+				companyMap[v.CompanyName] = v.GuangzhouName
+			}
+		}
+	}
+	var queryCompanies []string
+	for _, v := range companies {
+		if v == tradeAnalysisModel.TradeFuturesCompanyTop20 {
+			queryCompanies = append(queryCompanies, tradeAnalysisModel.TradeFuturesCompanyTop20)
+			continue
+		}
+		companyName, ok := companyMap[v]
+		if !ok {
+			utils.FileLog.Info(fmt.Sprintf("交易所%s公司名称映射不存在: %s", exchange, v))
+			continue
+		}
+		queryCompanies = append(queryCompanies, companyName)
+	}
+
+	// 郑商所/广期所查询方式不一样
+	var tradeAnalysis TradeAnalysisInterface
+	switch exchange {
+	case tradeAnalysisModel.TradeExchangeZhengzhou:
+		tradeAnalysis = &ZhengzhouTradeAnalysis{}
+	case tradeAnalysisModel.TradeExchangeGuangzhou:
+		tradeAnalysis = &GuangzhouTradeAnalysis{}
+	default:
+		tradeAnalysis = &BaseTradeAnalysis{}
+	}
+
+	// 获取多单/空单原始数据
+	originList, _, lastOriginList, e := tradeAnalysis.GetTradeDataByContracts(exchange, classifyNames, contracts, queryCompanies, startDate, endDate)
+	if e != nil {
+		err = fmt.Errorf("获取多空单原始数据失败, %v", e)
+		return
+	}
+
+	// [合约-期货公司]数据分组
+	contractTradeData = make(map[string]*tradeAnalysisModel.ContractCompanyTradeData)
+	{
+		keyDateData := make(map[string]*tradeAnalysisModel.ContractCompanyTradeDataList)
+		keyDateDataExist := make(map[string]bool)
+		for _, v := range originList {
+			companyName := v.CompanyName
+
+			k := fmt.Sprintf("%s-%s", v.ClassifyType, companyName)
+			if contractTradeData[k] == nil {
+				contractTradeData[k] = new(tradeAnalysisModel.ContractCompanyTradeData)
+				contractTradeData[k].Exchange = exchange
+				contractTradeData[k].CompanyName = companyName
+				contractTradeData[k].ClassifyName = v.ClassifyName
+				contractTradeData[k].ClassifyType = v.ClassifyType
+				contractTradeData[k].DataList = make([]*tradeAnalysisModel.ContractCompanyTradeDataList, 0)
+			}
+
+			kd := fmt.Sprintf("%s-%s", k, v.DataTime.Format(utils.FormatDate))
+			if keyDateData[kd] == nil {
+				keyDateData[kd] = new(tradeAnalysisModel.ContractCompanyTradeDataList)
+				keyDateData[kd].Date = v.DataTime
+			}
+			if v.ValType == 1 {
+				keyDateData[kd].BuyVal = v.Val
+				keyDateData[kd].BuyValType = tradeAnalysisModel.TradeDataTypeOrigin
+				keyDateData[kd].BuyChange = v.ValChange
+				keyDateData[kd].BuyChangeType = tradeAnalysisModel.TradeDataTypeOrigin
+			}
+			if v.ValType == 2 {
+				keyDateData[kd].SoldVal = v.Val
+				keyDateData[kd].SoldValType = tradeAnalysisModel.TradeDataTypeOrigin
+				keyDateData[kd].SoldChange = v.ValChange
+				keyDateData[kd].SoldChangeType = tradeAnalysisModel.TradeDataTypeOrigin
+			}
+			if !keyDateDataExist[kd] {
+				contractTradeData[k].DataList = append(contractTradeData[k].DataList, keyDateData[kd])
+				keyDateDataExist[kd] = true
+			}
+		}
+	}
+
+	// 合约的[日期-末位值]
+	lastBuyVal = make(map[string]map[time.Time]int)
+	lastSoldVal = make(map[string]map[time.Time]int)
+	{
+		for _, v := range lastOriginList {
+			if v.ValType == 1 {
+				if lastBuyVal[v.ClassifyType] == nil {
+					lastBuyVal[v.ClassifyType] = make(map[time.Time]int)
+				}
+				lastBuyVal[v.ClassifyType][v.DataTime] = v.Val
+				continue
+			}
+			if lastSoldVal[v.ClassifyType] == nil {
+				lastSoldVal[v.ClassifyType] = make(map[time.Time]int)
+			}
+			lastSoldVal[v.ClassifyType][v.DataTime] = v.Val
+		}
+	}
+	return
+}
+
+// GetTradeClassifyNewestDataTime 获取数据最新日期
+func GetTradeClassifyNewestDataTime(exchange string, classifyNames []string) (dataTime time.Time, err error) {
+	var tradeAnalysis TradeAnalysisInterface
+	switch exchange {
+	case tradeAnalysisModel.TradeExchangeZhengzhou:
+		tradeAnalysis = &ZhengzhouTradeAnalysis{}
+	case tradeAnalysisModel.TradeExchangeGuangzhou:
+		tradeAnalysis = &GuangzhouTradeAnalysis{}
+	default:
+		tradeAnalysis = &BaseTradeAnalysis{}
+	}
+	if exchange == tradeAnalysisModel.TradeExchangeZhengzhou {
+		classifies, e := GetZhengzhouContractsByClassifyNames(classifyNames)
+		if e != nil {
+			err = fmt.Errorf("获取郑商所实际合约失败, %v", e)
+			return
+		}
+		classifyNames = classifies
+	}
+
+	d, e := tradeAnalysis.GetClassifyNewestDataTime(exchange, classifyNames)
+	if e != nil && e.Error() != utils.ErrNoRow() {
+		err = fmt.Errorf("获取品种最新数据日期失败, %v", e)
+		return
+	}
+	if !d.IsZero() {
+		dataTime = d
+	} else {
+		dataTime = time.Now().Local()
+	}
+	return
+}

+ 333 - 67
services/data/trade_analysis/trade_analysis_interface.go

@@ -4,63 +4,226 @@ import (
 	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
 	"eta/eta_api/utils"
 	"fmt"
-	"strconv"
+	"sort"
 	"strings"
+	"time"
 )
 
 // TradeAnalysisInterface 持仓分析查询接口
 type TradeAnalysisInterface interface {
-	GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) // 根据品种和公司获取原始数据
-	GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error)                       // 获取品种末位数据
+	GetTradeDataByContracts(exchange string, classifyNames, contracts, queryCompanies []string, startDate, endDate time.Time) (items []*tradeAnalysisModel.OriginTradeData, topItems, lastItems []*tradeAnalysisModel.OriginTradeData, err error)
+	GetContractTopRankData(exchange string, classifyNames []string, dataDate time.Time) (items []*tradeAnalysisModel.ContractTopRankData, err error)
+	GetClassifyNewestDataTime(exchange string, classifyNames []string) (dataTime time.Time, err error)
 }
 
 // BaseTradeAnalysis 通用交易所
 type BaseTradeAnalysis struct{}
 
-func (b *BaseTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
-	return tradeAnalysisModel.GetTradeDataByClassifyAndCompany(exchange, classifyName, contracts, queryCompanies)
+// GetTradeDataByContracts 根据合约获取公司的持仓数据、TOP20数据以及每日的末位数据
+func (b *BaseTradeAnalysis) GetTradeDataByContracts(exchange string, classifyNames, contracts, queryCompanies []string, startDate, endDate time.Time) (items, topItems, lastItems []*tradeAnalysisModel.OriginTradeData, err error) {
+	// 根据合约获取数据
+	originData, e := tradeAnalysisModel.GetTradeDataByContracts(exchange, classifyNames, contracts, startDate, endDate)
+	if e != nil {
+		err = fmt.Errorf("根据合约获取持仓数据失败, %v", e)
+		return
+	}
+	items, topItems, lastItems = formatOriginData2UseData(originData, queryCompanies)
+	return
+}
+
+// formatOriginData2UseData 原始数据转换为持仓数据、TOP20以及末位数据
+func formatOriginData2UseData(originData []*tradeAnalysisModel.BaseFromTradeCommonIndex, queryCompanies []string) (items, topItems, lastItems []*tradeAnalysisModel.OriginTradeData) {
+	items, topItems, lastItems = make([]*tradeAnalysisModel.OriginTradeData, 0), make([]*tradeAnalysisModel.OriginTradeData, 0), make([]*tradeAnalysisModel.OriginTradeData, 0)
+	buyMaxRank, soldMaxRang := make(map[string]*tradeAnalysisModel.BaseFromTradeCommonIndex), make(map[string]*tradeAnalysisModel.BaseFromTradeCommonIndex)
+	for _, v := range originData {
+		// TOP20(大商所的Rank存在为0的)
+		if v.Rank == 0 || v.Rank == 999 {
+			topBuy := &tradeAnalysisModel.OriginTradeData{
+				Rank:         v.Rank,
+				CompanyName:  tradeAnalysisModel.TradeFuturesCompanyTop20,
+				Val:          v.BuyValue,
+				ValChange:    v.BuyChange,
+				ValType:      1,
+				DataTime:     v.DataTime,
+				ClassifyName: v.ClassifyName,
+				ClassifyType: v.ClassifyType,
+			}
+			topSold := &tradeAnalysisModel.OriginTradeData{
+				Rank:         v.Rank,
+				CompanyName:  tradeAnalysisModel.TradeFuturesCompanyTop20,
+				Val:          v.SoldValue,
+				ValChange:    v.SoldChange,
+				ValType:      2,
+				DataTime:     v.DataTime,
+				ClassifyName: v.ClassifyName,
+				ClassifyType: v.ClassifyType,
+			}
+			topItems = append(topItems, topBuy, topSold)
+			continue
+		}
+
+		// 查询的公司-买单
+		contractDateKey := fmt.Sprintf("%s_%s", v.DataTime.Format(utils.FormatDate), v.ClassifyType)
+		if utils.InArrayByStr(queryCompanies, v.BuyShortName) {
+			items = append(items, &tradeAnalysisModel.OriginTradeData{
+				Rank:         v.Rank,
+				CompanyName:  v.BuyShortName,
+				Val:          v.BuyValue,
+				ValChange:    v.BuyChange,
+				ValType:      1,
+				DataTime:     v.DataTime,
+				ClassifyName: v.ClassifyName,
+				ClassifyType: v.ClassifyType,
+			})
+
+			// 比对[合约-数据日期]对应的rank,取出末位
+			if buyMaxRank[contractDateKey] != nil && v.Rank > buyMaxRank[contractDateKey].Rank {
+				buyMaxRank[contractDateKey] = v
+			}
+			if buyMaxRank[contractDateKey] == nil {
+				buyMaxRank[contractDateKey] = v
+			}
+		}
+
+		// 查询的公司-卖单
+		if utils.InArrayByStr(queryCompanies, v.SoldShortName) {
+			items = append(items, &tradeAnalysisModel.OriginTradeData{
+				Rank:         v.Rank,
+				CompanyName:  v.SoldShortName,
+				Val:          v.SoldValue,
+				ValChange:    v.SoldChange,
+				ValType:      2,
+				DataTime:     v.DataTime,
+				ClassifyName: v.ClassifyName,
+				ClassifyType: v.ClassifyType,
+			})
+
+			// 比对数据日期对应的rank,取出末位
+			if soldMaxRang[contractDateKey] != nil && v.Rank > soldMaxRang[contractDateKey].Rank {
+				soldMaxRang[contractDateKey] = v
+			}
+			if soldMaxRang[contractDateKey] == nil {
+				soldMaxRang[contractDateKey] = v
+			}
+		}
+	}
+
+	// 如果查询的公司中含TOP20,那么追加进items
+	var hasTop bool
+	if utils.InArrayByStr(queryCompanies, tradeAnalysisModel.TradeFuturesCompanyTop20) {
+		hasTop = true
+	}
+	if hasTop {
+		items = append(items, topItems...)
+	}
+
+	// 末位数据
+	for _, v := range buyMaxRank {
+		if v == nil {
+			continue
+		}
+		lastItems = append(lastItems, &tradeAnalysisModel.OriginTradeData{
+			Rank:         v.Rank,
+			CompanyName:  v.BuyShortName,
+			Val:          v.BuyValue,
+			ValChange:    v.BuyChange,
+			ValType:      1,
+			DataTime:     v.DataTime,
+			ClassifyName: v.ClassifyName,
+			ClassifyType: v.ClassifyType,
+		})
+	}
+	for _, v := range buyMaxRank {
+		if v == nil {
+			continue
+		}
+		lastItems = append(lastItems, &tradeAnalysisModel.OriginTradeData{
+			Rank:         v.Rank,
+			CompanyName:  v.SoldShortName,
+			Val:          v.SoldValue,
+			ValChange:    v.SoldChange,
+			ValType:      2,
+			DataTime:     v.DataTime,
+			ClassifyName: v.ClassifyName,
+			ClassifyType: v.ClassifyType,
+		})
+	}
+	return
+}
+
+func (b *BaseTradeAnalysis) GetContractTopRankData(exchange string, classifyNames []string, dataDate time.Time) (items []*tradeAnalysisModel.ContractTopRankData, err error) {
+	return tradeAnalysisModel.GetContractTopRankData(exchange, classifyNames, dataDate)
 }
 
-func (b *BaseTradeAnalysis) GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
-	return tradeAnalysisModel.GetLastTradeDataByClassify(exchange, classifyName, contracts)
+func (b *BaseTradeAnalysis) GetClassifyNewestDataTime(exchange string, classifyNames []string) (dataTime time.Time, err error) {
+	return tradeAnalysisModel.GetClassifyNewestDataTime(exchange, classifyNames)
 }
 
 // ZhengzhouTradeAnalysis 郑商所
 type ZhengzhouTradeAnalysis struct{}
 
-func (z *ZhengzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
-	return tradeAnalysisModel.GetTradeZhengzhouDataByClassifyAndCompany(exchange, contracts, queryCompanies)
+func (z *ZhengzhouTradeAnalysis) GetTradeDataByContracts(exchange string, classifyNames, contracts, queryCompanies []string, startDate, endDate time.Time) (items, topItems, lastItems []*tradeAnalysisModel.OriginTradeData, err error) {
+	// 根据品种获取合约
+	//classifies, e := GetZhengzhouContractsByClassifyNames(classifyNames)
+	//if e != nil {
+	//	err = fmt.Errorf("获取郑商所实际合约失败, %v", e)
+	//	return
+	//}
+	//contracts = classifies
+
+	// 根据合约获取数据
+	originData, e := tradeAnalysisModel.GetZhengzhouTradeDataByContracts(contracts, startDate, endDate)
+	if e != nil {
+		err = fmt.Errorf("根据合约获取持仓数据失败, %v", e)
+		return
+	}
+	items, topItems, lastItems = formatOriginData2UseData(originData, queryCompanies)
+	return
+}
+
+func (z *ZhengzhouTradeAnalysis) GetContractTopRankData(exchange string, classifyNames []string, dataDate time.Time) (items []*tradeAnalysisModel.ContractTopRankData, err error) {
+	return tradeAnalysisModel.GetZhengzhouContractTopRankData(classifyNames, dataDate)
 }
 
-func (z *ZhengzhouTradeAnalysis) GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
-	return tradeAnalysisModel.GetLastTradeZhengzhouDataByClassify(exchange, contracts)
+func (z *ZhengzhouTradeAnalysis) GetClassifyNewestDataTime(exchange string, classifyNames []string) (dataTime time.Time, err error) {
+	return tradeAnalysisModel.GetClassifyNewestDataTime(exchange, classifyNames)
 }
 
 // GuangzhouTradeAnalysis 广期所
 type GuangzhouTradeAnalysis struct{}
 
-func (g *GuangzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, classifyName string, contracts, queryCompanies []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
-	classifyIdMap := map[string]int{"si": 7, "lc": 8}
-	classifyId := classifyIdMap[classifyName]
-	if classifyId == 0 {
-		err = fmt.Errorf("品种有误")
-		return
-	}
+func (g *GuangzhouTradeAnalysis) GetTradeDataByContracts(exchange string, classifyNames, contracts, queryCompanies []string, startDate, endDate time.Time) (items, topItems, lastItems []*tradeAnalysisModel.OriginTradeData, err error) {
+	items, topItems, lastItems = make([]*tradeAnalysisModel.OriginTradeData, 0), make([]*tradeAnalysisModel.OriginTradeData, 0), make([]*tradeAnalysisModel.OriginTradeData, 0)
 
-	// TOP20
-	seatNameArr := []string{tradeAnalysisModel.GuangZhouSeatNameBuy, tradeAnalysisModel.GuangZhouSeatNameSold}
-	if utils.InArrayByStr(queryCompanies, tradeAnalysisModel.TradeFuturesCompanyTop20) {
-		seatNameArr = append(seatNameArr, tradeAnalysisModel.GuangZhouTopSeatNameBuy, tradeAnalysisModel.GuangZhouTopSeatNameSold)
+	// 取品种ID
+	classifyNameId := map[string]int{"si": 7, "lc": 8}
+	classifyIdName := map[int]string{7: "si", 8: "lc"}
+	var classifyIds []int
+	for _, v := range classifyNames {
+		if classifyNameId[v] > 0 {
+			classifyIds = append(classifyIds, classifyNameId[v])
+		}
 	}
 
-	// 查询品种下所有指标
-	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndexByClassifyId(classifyId)
+	// 查询指标
+	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndex(classifyIds, contracts, "")
 	if e != nil {
 		err = fmt.Errorf("获取广期所指标失败, %v", e)
 		return
 	}
 	var indexIds []int
+
+	// 过滤掉成交量,只取买单卖单
+	seatNameArr := []string{tradeAnalysisModel.GuangZhouSeatNameBuy, tradeAnalysisModel.GuangZhouSeatNameSold, tradeAnalysisModel.GuangZhouTopSeatNameBuy, tradeAnalysisModel.GuangZhouTopSeatNameSold}
 	indexInfo := make(map[int]*tradeAnalysisModel.OriginTradeData)
+
+	// 查询公司中是否含TOP20
+	var hasTop bool
+	if utils.InArrayByStr(queryCompanies, tradeAnalysisModel.TradeFuturesCompanyTop20) {
+		hasTop = true
+	}
+	isTopIndex := make(map[int]bool)
+
 	for _, v := range indexes {
 		// eg.永安期货_si2401_持买单量
 		nameArr := strings.Split(v.IndexName, "_")
@@ -68,28 +231,32 @@ func (g *GuangzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, clas
 			continue
 		}
 		companyName := nameArr[0]
+		var isTop bool
 		if nameArr[0] == tradeAnalysisModel.GuangZhouTopCompanyAliasName {
+			isTop = true
+			isTopIndex[v.BaseFromTradeGuangzhouIndexId] = true
 			companyName = tradeAnalysisModel.TradeFuturesCompanyTop20
 		}
 		if !utils.InArrayByStr(seatNameArr, nameArr[2]) {
 			continue
 		}
-		if !utils.InArrayByStr(queryCompanies, companyName) {
-			continue
-		}
-		if !utils.InArrayByStr(contracts, nameArr[1]) {
+		// 过滤掉非TOP20以及非查询公司
+		if !isTop && !utils.InArrayByStr(queryCompanies, companyName) {
 			continue
 		}
 		indexIds = append(indexIds, v.BaseFromTradeGuangzhouIndexId)
+
+		// 指标信息
 		if indexInfo[v.BaseFromTradeGuangzhouIndexId] == nil {
-			if tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]] == 0 {
+			contractType := tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]]
+			if contractType == 0 {
 				continue
 			}
 			indexInfo[v.BaseFromTradeGuangzhouIndexId] = new(tradeAnalysisModel.OriginTradeData)
 			indexInfo[v.BaseFromTradeGuangzhouIndexId].CompanyName = companyName
-			indexInfo[v.BaseFromTradeGuangzhouIndexId].ClassifyName = classifyName
+			indexInfo[v.BaseFromTradeGuangzhouIndexId].ClassifyName = classifyIdName[v.BaseFromTradeGuangzhouClassifyId]
 			indexInfo[v.BaseFromTradeGuangzhouIndexId].ClassifyType = nameArr[1]
-			indexInfo[v.BaseFromTradeGuangzhouIndexId].ValType = tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]]
+			indexInfo[v.BaseFromTradeGuangzhouIndexId].ValType = contractType
 		}
 	}
 	if len(indexIds) == 0 {
@@ -97,18 +264,19 @@ func (g *GuangzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, clas
 	}
 
 	// 查询指标数据
-	indexesData, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouDataByIndexIds(indexIds)
+	indexesData, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouDataByIndexIds(indexIds, startDate, endDate)
 	if e != nil {
 		err = fmt.Errorf("获取广期所指标数据失败, %v", e)
 		return
 	}
-	items = make([]*tradeAnalysisModel.OriginTradeData, 0)
+	// 取出持仓数据、TOP20,比对末位数据
+	contractMinData := make(map[string]*tradeAnalysisModel.OriginTradeData) // 合约末位
 	for _, v := range indexesData {
 		info, ok := indexInfo[v.BaseFromTradeGuangzhouIndexId]
 		if !ok {
 			continue
 		}
-		items = append(items, &tradeAnalysisModel.OriginTradeData{
+		t := &tradeAnalysisModel.OriginTradeData{
 			CompanyName:  info.CompanyName,
 			Val:          int(v.Value),
 			ValChange:    int(v.QtySub),
@@ -116,66 +284,164 @@ func (g *GuangzhouTradeAnalysis) GetTradeDataByClassifyAndCompany(exchange, clas
 			ClassifyName: info.ClassifyName,
 			ClassifyType: info.ClassifyType,
 			ValType:      info.ValType,
-		})
+		}
+
+		// 如果是TOP20的指标,查询公司中含有TOP20那么追加进items,否则仅追加进topItems, 且TOP20不参与末位数据的比对
+		if isTopIndex[v.BaseFromTradeGuangzhouIndexId] {
+			if hasTop {
+				items = append(items, t)
+			}
+			topItems = append(topItems, t)
+			continue
+		}
+		items = append(items, t)
+
+		// 比对末位数据
+		k := fmt.Sprintf("%s-%d", info.ClassifyType, info.ValType)
+		if contractMinData[k] == nil {
+			contractMinData[k] = t
+			continue
+		}
+		if t.Val < contractMinData[k].Val {
+			contractMinData[k] = t
+		}
+	}
+
+	// 末位数据
+	for _, v := range contractMinData {
+		lastItems = append(lastItems, v)
 	}
 	return
 }
 
-func (g *GuangzhouTradeAnalysis) GetLastTradeDataByClassify(exchange, classifyName string, contracts []string) (items []*tradeAnalysisModel.OriginTradeData, err error) {
-	classifyIdMap := map[string]int{"si": 7, "lc": 8}
-	classifyId := classifyIdMap[classifyName]
-	if classifyId == 0 {
-		err = fmt.Errorf("品种有误")
-		return
+func (g *GuangzhouTradeAnalysis) GetContractTopRankData(exchange string, classifyNames []string, dataTime time.Time) (items []*tradeAnalysisModel.ContractTopRankData, err error) {
+	items = make([]*tradeAnalysisModel.ContractTopRankData, 0)
+
+	// 取品种ID
+	classifyNameId := map[string]int{"si": 7, "lc": 8}
+	classifyIdName := map[int]string{7: "si", 8: "lc"}
+	var classifyIds []int
+	for _, v := range classifyNames {
+		if classifyNameId[v] > 0 {
+			classifyIds = append(classifyIds, classifyNameId[v])
+		}
 	}
-	seatNameArr := []string{tradeAnalysisModel.GuangZhouSeatNameBuy, tradeAnalysisModel.GuangZhouSeatNameSold}
 
-	// 查询品种下所有指标
-	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndexByClassifyId(classifyId)
+	// 查询TOP20指标
+	indexKeyword := fmt.Sprint("%", tradeAnalysisModel.GuangZhouTopCompanyAliasName, "%")
+	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndex(classifyIds, []string{}, indexKeyword)
 	if e != nil {
 		err = fmt.Errorf("获取广期所指标失败, %v", e)
 		return
 	}
+	var indexIds []int
 
-	// 获取各合约下的指标
-	contractIndexIds := make(map[string][]int)
+	indexIdContract := make(map[int]string)                                      // [指标ID-合约_方向]
+	contractRankData := make(map[string]*tradeAnalysisModel.ContractTopRankData) // [合约-合约TOP数据]
 	for _, v := range indexes {
 		// eg.永安期货_si2401_持买单量
 		nameArr := strings.Split(v.IndexName, "_")
 		if len(nameArr) != 3 {
 			continue
 		}
-		if !utils.InArrayByStr(contracts, nameArr[1]) {
+		contractCode := nameArr[1]
+		indexIds = append(indexIds, v.BaseFromTradeGuangzhouIndexId)
+
+		// 指标对应的[合约+方向]
+		k := fmt.Sprintf("%s_%d", contractCode, tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]])
+		indexIdContract[v.BaseFromTradeGuangzhouIndexId] = k
+
+		// 合约对应的数据
+		if contractRankData[contractCode] == nil {
+			contractRankData[contractCode] = new(tradeAnalysisModel.ContractTopRankData)
+			contractRankData[contractCode].Exchange = exchange
+			contractRankData[contractCode].ClassifyName = classifyIdName[v.BaseFromTradeGuangzhouClassifyId]
+			contractRankData[contractCode].ClassifyType = contractCode
+			contractRankData[contractCode].DataTime = dataTime
+		}
+	}
+	if len(indexIds) == 0 {
+		return
+	}
+
+	// 查询指标数据
+	indexesData, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouDataByIndexIds(indexIds, dataTime, dataTime)
+	if e != nil {
+		err = fmt.Errorf("获取广期所指标数据失败, %v", e)
+		return
+	}
+	for _, v := range indexesData {
+		k := indexIdContract[v.BaseFromTradeGuangzhouIndexId]
+		if k == "" {
 			continue
 		}
-		if !utils.InArrayByStr(seatNameArr, nameArr[2]) {
+		nameArr := strings.Split(k, "_")
+		if len(nameArr) != 2 {
+			continue
+		}
+		contractCode := nameArr[0]
+		if contractRankData[contractCode] == nil {
 			continue
 		}
-		if tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]] == 0 {
+
+		// 根据方向赋值:1-多单;2-空单;3-成交量
+		switch nameArr[1] {
+		case "1":
+			contractRankData[contractCode].BuyValue = int(v.Value)
+		case "2":
+			contractRankData[contractCode].SoldValue = int(v.Value)
+		case "3":
+			contractRankData[contractCode].DealValue = int(v.Value)
+		default:
 			continue
 		}
-		k := fmt.Sprintf("%s-%d", nameArr[1], tradeAnalysisModel.GuangzhouSeatNameValType[nameArr[2]])
-		contractIndexIds[k] = append(contractIndexIds[k], v.BaseFromTradeGuangzhouIndexId)
 	}
 
-	// ps.如果后面如果有空可以优化一下这里, 把末位数据每天写进一张表里面
-	for k, v := range contractIndexIds {
-		keyArr := strings.Split(k, "-")
-		contract := keyArr[0]
-		valType, _ := strconv.Atoi(keyArr[1])
-		lastVales, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouMinDataByIndexIds(v)
-		if e != nil {
-			err = fmt.Errorf("获取合约末位数据失败, %v", e)
-			return
+	// 根据成交量排序并返回排名数据
+	for _, v := range contractRankData {
+		items = append(items, v)
+	}
+	sort.Slice(items, func(i, j int) bool {
+		return items[i].DealValue > items[j].DealValue
+	})
+	return
+}
+
+func (g *GuangzhouTradeAnalysis) GetClassifyNewestDataTime(exchange string, classifyNames []string) (dataTime time.Time, err error) {
+	// 取品种ID
+	classifyNameId := map[string]int{"si": 7, "lc": 8}
+	var classifyIds []int
+	for _, v := range classifyNames {
+		if classifyNameId[v] > 0 {
+			classifyIds = append(classifyIds, classifyNameId[v])
 		}
-		for _, vv := range lastVales {
-			items = append(items, &tradeAnalysisModel.OriginTradeData{
-				Val:          int(vv.Value),
-				DataTime:     vv.DataTime,
-				ClassifyType: contract,
-				ValType:      valType,
-			})
+	}
+
+	// 查询TOP20的最新日期
+	indexKeyword := fmt.Sprint("%", tradeAnalysisModel.GuangZhouTopCompanyAliasName, "%")
+	indexes, e := tradeAnalysisModel.GetBaseFromTradeGuangzhouIndex(classifyIds, []string{}, indexKeyword)
+	if e != nil {
+		err = fmt.Errorf("获取广期所指标失败, %v", e)
+		return
+	}
+	var indexIds []int
+	for _, v := range indexes {
+		// eg.永安期货_si2401_持买单量
+		nameArr := strings.Split(v.IndexName, "_")
+		if len(nameArr) != 3 {
+			continue
 		}
+		indexIds = append(indexIds, v.BaseFromTradeGuangzhouIndexId)
+	}
+	if len(indexIds) == 0 {
+		return
+	}
+
+	d, e := tradeAnalysisModel.GetGuangzhouClassifyNewestDataTime(indexIds)
+	if e != nil {
+		err = fmt.Errorf("获取广期品种最新数据失败, %v", e)
+		return
 	}
+	dataTime = d
 	return
 }

+ 287 - 0
services/data/trade_analysis/trade_analysis_table.go

@@ -0,0 +1,287 @@
+package trade_analysis
+
+import (
+	tradeAnalysisModel "eta/eta_api/models/data_manage/trade_analysis"
+	"eta/eta_api/utils"
+	"fmt"
+	"math"
+	"strings"
+	"time"
+)
+
+// CheckAnalysisTableExtraConfig 校验表格配置
+func CheckAnalysisTableExtraConfig(extraConfig tradeAnalysisModel.TableExtraConfig) (pass bool, tips string) {
+	if extraConfig.CompanyName == "" {
+		tips = "请选择期货公司"
+		return
+	}
+	if len(extraConfig.ClassifyList) == 0 {
+		tips = "请选择品种"
+		return
+	}
+	var classifyTotal int
+	for _, v := range extraConfig.ClassifyList {
+		if v.Exchange == "" {
+			tips = "请选择交易所"
+			return
+		}
+		if len(v.ClassifyNames) == 0 {
+			tips = "请选择品种"
+			return
+		}
+		classifyTotal += len(v.ClassifyNames)
+	}
+	// 品种选择加个上限吧,过多SQL会很慢
+	if classifyTotal > 20 {
+		tips = "选择品种不超过20个"
+		return
+	}
+	typeArr := []int{tradeAnalysisModel.ContractQueryTypeTop, tradeAnalysisModel.ContractQueryTypeTop2, tradeAnalysisModel.ContractQueryTypeTop3, tradeAnalysisModel.ContractQueryTypeAll, tradeAnalysisModel.ContractQueryTypeTotal}
+	if !utils.InArrayByInt(typeArr, extraConfig.ContractType) {
+		tips = "请选择正确的合约"
+		return
+	}
+	if extraConfig.DateType == 1 && extraConfig.FixedDate == "" {
+		tips = "请选择固定日期"
+		return
+	}
+	if extraConfig.FixedDate != "" {
+		_, e := time.Parse(utils.FormatDate, extraConfig.FixedDate)
+		if e != nil {
+			tips = "固定日期格式有误"
+			return
+		}
+	}
+	if extraConfig.PredictRatio < 0 || extraConfig.PredictRatio > 1 {
+		tips = "请输入正确的估计参数"
+		return
+	}
+	pass = true
+	return
+}
+
+// GetAnalysisTableBaseDate 根据配置获取基准交易日期
+func GetAnalysisTableBaseDate(dateType, intervalMove int, fixedDate string) (baseDate time.Time, err error) {
+	if dateType == 1 {
+		// 固定日期
+		t, _ := time.ParseInLocation(utils.FormatDate, fixedDate, time.Local)
+		baseDate = t
+	} else {
+		// 最新交易日
+		st := time.Now().AddDate(0, 0, -7) // 默认前移7天, 在开始结束区间内取出指定交易日
+		if intervalMove > 0 {
+			period := intervalMove * 7
+			st = st.AddDate(0, 0, -period)
+		}
+		tradeDates := utils.GetTradingDays(st, time.Now())
+		dateLen := len(tradeDates)
+		if dateLen == 0 {
+			err = fmt.Errorf("交易日序列异常")
+			return
+		}
+		index := dateLen - 1
+		if intervalMove > 0 {
+			index -= intervalMove
+		}
+		if index < 0 || index > dateLen {
+			err = fmt.Errorf("交易日序列异常")
+			return
+		}
+		baseDate = tradeDates[index]
+	}
+	return
+}
+
+// CalculateTableRowData 计算表格行数据
+func CalculateTableRowData(exchange string, baseDate time.Time, topData []*tradeAnalysisModel.ContractTopRankData, contractsData []*tradeAnalysisModel.ContractCompanyTradeData) (rows []*tradeAnalysisModel.TableRowData, err error) {
+	contractTopData := make(map[string]*tradeAnalysisModel.ContractTopRankData)
+	for _, v := range topData {
+		contractTopData[v.ClassifyType] = v
+	}
+
+	rows = make([]*tradeAnalysisModel.TableRowData, 0)
+	for _, v := range contractsData {
+		// 取出基准日期的数据
+		cd := new(tradeAnalysisModel.ContractCompanyTradeDataList)
+		for _, d := range v.DataList {
+			if d.Date.Equal(baseDate) {
+				cd = d
+				break
+			}
+		}
+		// 合约基准日期无数据
+		if cd.Date.IsZero() {
+			continue
+		}
+
+		td, ok := contractTopData[v.ClassifyType]
+		if !ok {
+			// 无前20数据
+			continue
+		}
+		row := new(tradeAnalysisModel.TableRowData)
+		row.Exchange = exchange
+		row.ClassifyName = td.ClassifyName
+		row.ClassifyType = v.ClassifyType
+		row.BuyValue = cd.BuyVal
+		row.BuyChange = cd.BuyChange
+		row.SoldValue = cd.SoldVal
+		row.SoldChange = cd.SoldChange
+		row.PureBuyVal = cd.PureBuyVal
+		row.PureBuyChange = cd.PureBuyChange
+		row.TopBuyValue = td.BuyValue
+		row.TopSoldValue = td.SoldValue
+		row.TopBuyChange = td.BuyChange
+		row.TopSoldChange = td.SoldChange
+
+		// 计算值
+		row.BuySoldRatio = math.Round(float64(cd.BuyVal)/float64(cd.BuyVal+cd.SoldVal)*100) / 100
+		row.BuyTopRatio = math.Round(float64(cd.BuyVal)/float64(td.BuyValue)*100) / 100
+		row.SoldTopRatio = math.Round(float64(cd.SoldVal)/float64(td.SoldValue)*100) / 100
+		row.TopPureBuy = td.BuyValue - td.SoldValue
+		row.TopPureBuyChange = int(math.Abs(float64(td.BuyChange))) + int(math.Abs(float64(td.SoldChange))) // 净多变化=Abs(多单变化)+Abs(空单变化)
+		row.TopBuySoldRatio = math.Round(float64(td.BuyValue)/float64(td.BuyValue+td.SoldValue)*100) / 100
+		rows = append(rows, row)
+	}
+	return
+}
+
+// GetTableRowsDataByConfig 根据配置获取表格行数据
+func GetTableRowsDataByConfig(tableConfig tradeAnalysisModel.TableExtraConfig) (tableRows []*tradeAnalysisModel.TableRowData, err error) {
+	// 基准日期
+	baseDate, e := GetAnalysisTableBaseDate(tableConfig.DateType, tableConfig.IntervalMove, tableConfig.FixedDate)
+	if e != nil {
+		err = fmt.Errorf("获取基准日期失败, %v", e)
+		return
+	}
+
+	// 根据类型取出合约数
+	var contractMax int // 需要取出的品种对应的最大合约数
+	//classifyMax := make(map[string]int) // 品种对应的合约数
+	switch tableConfig.ContractType {
+	case tradeAnalysisModel.ContractQueryTypeTop:
+		contractMax = 1
+	case tradeAnalysisModel.ContractQueryTypeTop2:
+		contractMax = 2
+	case tradeAnalysisModel.ContractQueryTypeTop3:
+		contractMax = 3
+	case tradeAnalysisModel.ContractQueryTypeAll, tradeAnalysisModel.ContractQueryTypeTotal:
+		contractMax = 999
+	}
+
+	var sortRules []string                                                              // 最终排序(合约加总以品种排序,其他以为合约排序)
+	classifyContractsData := make(map[string][]*tradeAnalysisModel.ContractTopRankData) // 根据品种分组的合约数据
+	classifyContracts := make(map[string][]string)                                      // 品种合约
+	for _, v := range tableConfig.ClassifyList {
+		for _, classify := range v.ClassifyNames {
+			// 获取合约排名及持仓数据
+			contractRanks, e := GetTopContractRank(v.Exchange, []string{classify}, baseDate)
+			if e != nil {
+				err = fmt.Errorf("获取基准日期合约排名失败, %v", e)
+				return
+			}
+			if len(contractRanks) == 0 {
+				continue
+			}
+			flag := fmt.Sprintf("%s-%s", v.Exchange, classify)
+
+			// 取出指定数量的合约
+			var contractNum int
+			for _, rd := range contractRanks {
+				if contractNum >= contractMax {
+					continue
+				}
+				contractNum += 1
+				classifyContracts[classify] = append(classifyContracts[classify], rd.ClassifyType)
+
+				if classifyContractsData[flag] == nil {
+					classifyContractsData[flag] = make([]*tradeAnalysisModel.ContractTopRankData, 0)
+				}
+				classifyContractsData[flag] = append(classifyContractsData[flag], rd)
+
+				// 以合约排序
+				if tableConfig.ContractType != tradeAnalysisModel.ContractQueryTypeTotal {
+					sortRules = append(sortRules, rd.ClassifyType)
+				}
+			}
+
+			// 以品种排序
+			if tableConfig.ContractType == tradeAnalysisModel.ContractQueryTypeTotal {
+				sortRules = append(sortRules, classify)
+			}
+		}
+	}
+
+	mussyRows := make([]*tradeAnalysisModel.TableRowData, 0)
+	for k, contracts := range classifyContractsData {
+		var exchange, classifyName string
+		keyArr := strings.Split(k, "-")
+		if len(keyArr) != 2 {
+			continue
+		}
+		exchange = keyArr[0]
+		classifyName = keyArr[1]
+		classifyTypes := classifyContracts[classifyName]
+		if len(classifyTypes) == 0 {
+			continue
+		}
+
+		// 合约加总时,contracts也需要加总,且ClassifyType为品种名
+		if tableConfig.ContractType == tradeAnalysisModel.ContractQueryTypeTotal {
+			contracts = MergeClassifyTypeTopRankData(contracts)
+		}
+
+		contractRowData, e := GetTableTradeData(exchange, classifyName, classifyTypes, tableConfig.CompanyName, tableConfig.PredictRatio, baseDate.AddDate(0, 0, -5), baseDate.AddDate(0, 0, 5), tableConfig.ContractType)
+		if e != nil {
+			err = fmt.Errorf("获取公司合约多空单数据失败, %v", e)
+			return
+		}
+		if len(contractRowData) == 0 {
+			continue
+		}
+
+		// 计算行数据
+		rows, e := CalculateTableRowData(exchange, baseDate, contracts, contractRowData)
+		if e != nil {
+			err = fmt.Errorf("计算合约数据失败, %v", e)
+			return
+		}
+		mussyRows = append(mussyRows, rows...)
+	}
+
+	// 排序
+	tableRows = make([]*tradeAnalysisModel.TableRowData, 0)
+	contractRow := make(map[string]*tradeAnalysisModel.TableRowData)
+	for _, v := range mussyRows {
+		contractRow[v.ClassifyType] = v
+	}
+	for _, v := range sortRules {
+		t := contractRow[v]
+		if t != nil {
+			tableRows = append(tableRows, t)
+		}
+	}
+	return
+}
+
+// MergeClassifyTypeTopRankData 类型为合约加总时-合并当日的TOP数据
+func MergeClassifyTypeTopRankData(classifyTypesData []*tradeAnalysisModel.ContractTopRankData) (mergedData []*tradeAnalysisModel.ContractTopRankData) {
+	mergedData = make([]*tradeAnalysisModel.ContractTopRankData, 0)
+
+	mergeData := new(tradeAnalysisModel.ContractTopRankData)
+	for _, v := range classifyTypesData {
+		mergeData.Exchange = v.Exchange
+		mergeData.DealValue += v.DealValue
+		mergeData.BuyValue += v.BuyValue
+		mergeData.BuyChange += v.BuyChange
+		mergeData.SoldValue += v.SoldValue
+		mergeData.SoldChange += v.SoldChange
+		mergeData.PureBuyValue += v.PureBuyValue
+		mergeData.PureBuyChange += v.PureBuyChange
+		mergeData.ClassifyName = v.ClassifyName
+		mergeData.ClassifyType = v.ClassifyName // 合约合并后为品种名
+		mergeData.DataTime = v.DataTime
+	}
+	mergedData = append(mergedData, mergeData)
+	return
+}

+ 19 - 0
services/excel_info.go

@@ -3,6 +3,7 @@ package services
 import (
 	"encoding/json"
 	"eta/eta_api/models"
+	excelModel "eta/eta_api/models/data_manage/excel"
 	"eta/eta_api/models/system"
 	"eta/eta_api/utils"
 	"fmt"
@@ -61,3 +62,21 @@ func UpdateExcelEditMark(excelInfoId, nowUserId, status int, nowUserName string)
 	return
 }
 
+// GetTradeAnalysisTableOpButton 获取持仓分析表格的操作权限
+func GetTradeAnalysisTableOpButton(belongUserId, sysUserId int, roleTypeCode string, haveOperaAuth bool) (button excelModel.ExcelInfoDetailButton) {
+	// 这部分没有加到数据权限里,这里先注释掉
+	//if !haveOperaAuth {
+	//	return
+	//}
+	// 非管理员角色查看其他用户创建的表格,可刷新、另存为、下载表格;
+	button.RefreshButton = true
+	button.CopyButton = true
+	button.DownloadButton = true
+
+	// 创建人、管理员有权限编辑和删除
+	if belongUserId == sysUserId || roleTypeCode == utils.ROLE_TYPE_CODE_ADMIN || roleTypeCode == utils.ROLE_TYPE_CODE_FICC_ADMIN {
+		button.OpButton = true
+		button.DeleteButton = true
+	}
+	return
+}

+ 8 - 0
utils/common.go

@@ -2783,6 +2783,14 @@ func GetCurrentTime() string {
 	return time.Now().Format("2006-01-02 15:04:05")
 }
 
+// ReverseTimeSlice 反转时间类型切片
+func ReverseTimeSlice(s []time.Time) []time.Time {
+	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
+		s[i], s[j] = s[j], s[i]
+	}
+	return s
+}
+
 // RoundNumber 保留小数位数
 func RoundNumber(num string, decimalPlaces int, hasPercent bool) string {
 	numDecimal, _ := decimal.NewFromString(num)

+ 9 - 5
utils/constants.go

@@ -257,6 +257,8 @@ const (
 	CACHE_KEY_REPLACE_EDB         = "eta:replace_edb"                 //系统用户操作日志队列
 	CACHE_REPORT_SHARE_SHORT_Url  = "eta:report_share_url:report_id:" //报告短链映射key
 	CACHE_REPORT_SHARE_ORIGIN_Url = "eta:report_share_url:token:"     //短链与原始报告链接的映射key
+
+	CACHE_EXCEL_REFRESH = "CACHE_EXCEL_REFRESH" // 表格刷新
 )
 
 // 模板消息推送类型
@@ -324,11 +326,13 @@ const (
 
 // ETA表格
 const (
-	EXCEL_DEFAULT         = 1 // 自定义excel
-	TIME_TABLE            = 2 // 时间序列表格
-	MIXED_TABLE           = 3 // 混合表格
-	CUSTOM_ANALYSIS_TABLE = 4 // 自定义分析表格
-	BALANCE_TABLE         = 5 // 平衡表
+	EXCEL_DEFAULT                    = 1 // 自定义excel
+	TIME_TABLE                       = 2 // 时间序列表格
+	MIXED_TABLE                      = 3 // 混合表格
+	CUSTOM_ANALYSIS_TABLE            = 4 // 自定义分析表格
+	BALANCE_TABLE                    = 5 // 平衡表
+	TRADE_ANALYSIS_TABLE             = 6 // 持仓分析-多空分析表格
+	TRADE_ANALYSIS_CORRELATION_TABLE = 7 // 持仓分析-相关性表格
 )
 
 // 图表样式类型