diff --git a/app/admin/models/line_pre_order.go b/app/admin/models/line_pre_order.go index a3155d7..48f8caf 100644 --- a/app/admin/models/line_pre_order.go +++ b/app/admin/models/line_pre_order.go @@ -9,38 +9,40 @@ import ( type LinePreOrder struct { models.Model - ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型 (字典 exchange_type)"` - Pid int `json:"pid" gorm:"type:int unsigned;omitempty;comment:pid"` - MainId int `json:"mainId" gorm:"type:int;comment:主单id"` - ApiId int `json:"apiId" gorm:"type:varchar(255);omitempty;comment:api用户"` - GroupId string `json:"groupId" gorm:"type:int unsigned;omitempty;comment:交易对组id"` - Symbol string `json:"symbol" gorm:"type:varchar(255);omitempty;comment:交易对"` - QuoteSymbol string `json:"quoteSymbol" gorm:"type:varchar(255);omitempty;comment:计较货币"` - SignPrice string `json:"signPrice" gorm:"type:decimal(18,8);omitempty;comment:对标价"` - SignPriceU decimal.Decimal `json:"signPriceU" gorm:"type:decimal(18,8);omitempty;comment:交易对对标U的行情价"` - SignPriceType string `json:"signPriceType" gorm:"type:enum('new','tall','low','mixture','entrust','add');omitempty;comment:对标价类型: new=最新价格;tall=24小时最高;low=24小时最低;mixture=标记价;entrust=委托实价;add=补仓"` - Rate string `json:"rate" gorm:"type:decimal(18,2);omitempty;comment:下单百分比"` - Price string `json:"price" gorm:"type:decimal(18,8);omitempty;comment:触发价格"` - Num string `json:"num" gorm:"type:decimal(18,8);omitempty;comment:购买数量"` - BuyPrice string `json:"buyPrice" gorm:"type:decimal(18,8);omitempty;comment:购买金额"` - SymbolType int `json:"symbolType" gorm:"type:int;comment:交易对类型:1=现货;2=合约"` - OrderCategory int `json:"orderCategory" gorm:"type:int;comment:订单类型: 1=主单 2=对冲单 3-加仓单"` - Site string `json:"site" gorm:"type:enum('BUY','SELL');omitempty;comment:购买方向:BUY=买;SELL=卖"` - OrderSn string `json:"orderSn" gorm:"type:varchar(255);omitempty;comment:订单号"` - OrderType int `json:"orderType" gorm:"type:int;omitempty;comment:订单类型:0=主单 1=止盈 2=止损 3=平仓 4=减仓"` - Desc string `json:"desc" gorm:"type:text;omitempty;comment:订单描述"` - Status int `json:"status" gorm:"type:int;omitempty;comment:是否被触发:0=待触发;1=已触发;2=下单失败;4=已取消;5=委托中;6=已成交;9=已平仓"` - CoverType int `json:"coverType" gorm:"type:int unsigned;omitempty;comment:对冲类型 1= 现货对合约 2=合约对合约 3 合约对现货"` - ExpireTime time.Time `json:"expireTime" gorm:"comment:过期时间"` - MainOrderType string `json:"mainOrderType" gorm:"type:enum;comment:第一笔(主单类型) 限价(LIMIT)市价(MARKET)"` - LossAmount decimal.Decimal `json:"lossAmount" gorm:"type:decimal(18,8);comment:亏损金额(U)"` - TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"` - Child []LinePreOrder `json:"child" gorm:"-"` - ApiName string `json:"api_name" gorm:"->"` - ChildNum int64 `json:"child_num" gorm:"->"` - AddPositionStatus int `json:"add_position_status" gorm:"->"` - ReduceStatus int `json:"reduce_status" gorm:"->"` - Childs []LinePreOrder `json:"childs" gorm:"foreignKey:pid;references:id"` + ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型 (字典 exchange_type)"` + StrategyTemplateType int `json:"strategyTemplateType" gorm:"type:tinyint;comment:策略类型 0-无 1-波段涨跌幅"` + StrategyTemplateId int `json:"strategyTemplateId" gorm:"type:bigint;comment:策略模板id"` + Pid int `json:"pid" gorm:"type:int unsigned;omitempty;comment:pid"` + MainId int `json:"mainId" gorm:"type:int;comment:主单id"` + ApiId int `json:"apiId" gorm:"type:varchar(255);omitempty;comment:api用户"` + GroupId string `json:"groupId" gorm:"type:int unsigned;omitempty;comment:交易对组id"` + Symbol string `json:"symbol" gorm:"type:varchar(255);omitempty;comment:交易对"` + QuoteSymbol string `json:"quoteSymbol" gorm:"type:varchar(255);omitempty;comment:计较货币"` + SignPrice string `json:"signPrice" gorm:"type:decimal(18,8);omitempty;comment:对标价"` + SignPriceU decimal.Decimal `json:"signPriceU" gorm:"type:decimal(18,8);omitempty;comment:交易对对标U的行情价"` + SignPriceType string `json:"signPriceType" gorm:"type:enum('new','tall','low','mixture','entrust','add');omitempty;comment:对标价类型: new=最新价格;tall=24小时最高;low=24小时最低;mixture=标记价;entrust=委托实价;add=补仓"` + Rate string `json:"rate" gorm:"type:decimal(18,2);omitempty;comment:下单百分比"` + Price string `json:"price" gorm:"type:decimal(18,8);omitempty;comment:触发价格"` + Num string `json:"num" gorm:"type:decimal(18,8);omitempty;comment:购买数量"` + BuyPrice string `json:"buyPrice" gorm:"type:decimal(18,8);omitempty;comment:购买金额"` + SymbolType int `json:"symbolType" gorm:"type:int;comment:交易对类型:1=现货;2=合约"` + OrderCategory int `json:"orderCategory" gorm:"type:int;comment:订单类型: 1=主单 2=对冲单 3-加仓单"` + Site string `json:"site" gorm:"type:enum('BUY','SELL');omitempty;comment:购买方向:BUY=买;SELL=卖"` + OrderSn string `json:"orderSn" gorm:"type:varchar(255);omitempty;comment:订单号"` + OrderType int `json:"orderType" gorm:"type:int;omitempty;comment:订单类型:0=主单 1=止盈 2=止损 3=平仓 4=减仓"` + Desc string `json:"desc" gorm:"type:text;omitempty;comment:订单描述"` + Status int `json:"status" gorm:"type:int;omitempty;comment:是否被触发:0=待触发;1=已触发;2=下单失败;4=已取消;5=委托中;6=已成交;9=已平仓"` + CoverType int `json:"coverType" gorm:"type:int unsigned;omitempty;comment:对冲类型 1= 现货对合约 2=合约对合约 3 合约对现货"` + ExpireTime time.Time `json:"expireTime" gorm:"comment:过期时间"` + MainOrderType string `json:"mainOrderType" gorm:"type:enum;comment:第一笔(主单类型) 限价(LIMIT)市价(MARKET)"` + LossAmount decimal.Decimal `json:"lossAmount" gorm:"type:decimal(18,8);comment:亏损金额(U)"` + TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"` + Child []LinePreOrder `json:"child" gorm:"-"` + ApiName string `json:"api_name" gorm:"->"` + ChildNum int64 `json:"child_num" gorm:"->"` + AddPositionStatus int `json:"add_position_status" gorm:"->"` + ReduceStatus int `json:"reduce_status" gorm:"->"` + Childs []LinePreOrder `json:"childs" gorm:"foreignKey:pid;references:id"` // LinePreOrder 线上预埋单\ models.ModelTime models.ControlBy diff --git a/app/admin/service/dto/line_pre_order.go b/app/admin/service/dto/line_pre_order.go index 87a6522..ae716cc 100644 --- a/app/admin/service/dto/line_pre_order.go +++ b/app/admin/service/dto/line_pre_order.go @@ -186,6 +186,8 @@ func (s *LinePreOrderDeleteReq) GetId() interface{} { type LineAddPreOrderReq struct { ExchangeType string `json:"exchange_type" vd:"len($)>0"` //交易所类型 + StrategyTemplateType int `json:"strategy_template_type"` //策略类型 0-无 1-波段涨跌幅 + StrategyTemplateId int `json:"strategy_template_id"` //策略id OrderType int `json:"order_type"` //订单类型 Symbol string `json:"symbol"` //交易对 ApiUserId string `json:"api_id" ` //下单用户 @@ -283,6 +285,10 @@ func (req LineAddPreOrderReq) Valid() error { return errors.New("主单减仓价格百分比不能为空") } + if req.StrategyTemplateType == 1 && req.StrategyTemplateId == 0 { + return errors.New("请选择策略") + } + if req.ReduceNumRatio.IsZero() { return errors.New("主单减仓数量百分比不能为空") } @@ -367,6 +373,8 @@ type LineTreeOrder struct { // LineBatchAddPreOrderReq 批量添加订单请求参数 type LineBatchAddPreOrderReq struct { ExchangeType string `json:"exchange_type"` //交易所类型 字典exchange_type + StrategyTemplateType int `json:"strategy_template_type"` //策略类型 0-无 1-波段涨跌幅 + StrategyTemplateId int `json:"strategy_template_id"` //策略id SymbolType int `json:"symbol_type"` //主单交易对类型 0-现货 1-合约 OrderType int `json:"order_type"` //订单类型 SymbolGroupId string `json:"symbol_group_id"` //交易对组id @@ -404,6 +412,10 @@ func (req LineBatchAddPreOrderReq) CheckParams() error { return errors.New("请选择交易所") } + if req.StrategyTemplateType == 1 && req.StrategyTemplateId == 0 { + return errors.New("请选择策略") + } + if req.ApiUserId == "" { return errors.New("选择下单用户") } diff --git a/common/const/rediskey/redis_key.go b/common/const/rediskey/redis_key.go index 75aecee..53f2ac0 100644 --- a/common/const/rediskey/redis_key.go +++ b/common/const/rediskey/redis_key.go @@ -74,6 +74,13 @@ const ( AveRequestToken = "ave_request_token" // AVE请求token ) +const ( + //现货最后成交价 sort set {exchangeType,symbol} + SpotTickerLastPrice = "spot_ticker_last_price:%s:%s" + //合约最后成交价 sort set {exchangeType,symbol} + FutureTickerLastPrice = "fut_ticker_last_price:%s:%s" +) + // 用户下单 const ( MemberShipPre = "member_ship_pre:%v" //用户开通会员预下单 单价缓存{payable_amount} diff --git a/common/helper/redis_helper.go b/common/helper/redis_helper.go index 903976a..368cbfa 100644 --- a/common/helper/redis_helper.go +++ b/common/helper/redis_helper.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "math" "reflect" "strconv" "time" @@ -609,7 +610,7 @@ func (e *RedisHelper) SignelAdd(key string, score float64, member string) error _, err := e.client.ZRemRangeByScore(e.ctx, key, scoreStr, scoreStr).Result() if err != nil { - fmt.Printf("删除score失败", err.Error()) + fmt.Printf("删除score失败,err:%s", err.Error()) } _, err = e.client.ZAdd(e.ctx, key, &redis.Z{ Score: score, @@ -642,6 +643,46 @@ func (e *RedisHelper) DelSortSet(key, member string) error { return e.client.ZRem(e.ctx, key, member).Err() } +// RemoveBeforeScore 移除 Sorted Set 中分数小于等于指定值的数据 +// key: Sorted Set 的键 +// score: 分数上限,所有小于等于此分数的元素将被移除 +// 返回值: 移除的元素数量和可能的错误 +func (e *RedisHelper) RemoveBeforeScore(key string, score float64) (int64, error) { + if key == "" { + return 0, errors.New("key 不能为空") + } + if math.IsNaN(score) || math.IsInf(score, 0) { + return 0, errors.New("score 必须是有效数字") + } + + // 使用 ZRemRangeByScore 移除数据 + count, err := e.client.ZRemRangeByScore(e.ctx, key, "-inf", strconv.FormatFloat(score, 'f', -1, 64)).Result() + if err != nil { + return 0, fmt.Errorf("移除 Sorted Set 数据失败, key: %s, score: %f, err: %v", key, score, err) + } + + return count, nil +} + +// GetNextAfterScore 获取指定分数及之后的第一条数据(包含指定分数) +func (e *RedisHelper) GetNextAfterScore(key string, score float64) (string, error) { + // 使用 ZRangeByScore 获取大于等于 score 的第一条数据 + zs, err := e.client.ZRangeByScoreWithScores(e.ctx, key, &redis.ZRangeBy{ + Min: fmt.Sprintf("%f", score), // 包含指定分数 + Max: "+inf", // 上限为正无穷 + Offset: 0, // 从第 0 条开始 + Count: 1, // 只取 1 条 + }).Result() + + if err != nil { + return "", fmt.Errorf("获取数据失败: %v", err) + } + if len(zs) == 0 { + return "", nil // 没有符合条件的元素 + } + return zs[0].Member.(string), nil +} + /* 获取sort set 所有数据 */ diff --git a/services/futureservice/binancemarket.go b/services/futureservice/binancemarket.go index 00b71da..b8094e2 100644 --- a/services/futureservice/binancemarket.go +++ b/services/futureservice/binancemarket.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "go-admin/common/const/rediskey" "go-admin/common/global" "go-admin/common/helper" "go-admin/config" @@ -15,6 +16,7 @@ import ( "go-admin/models" log "github.com/go-admin-team/go-admin-core/logger" + "github.com/shopspring/decimal" "github.com/bytedance/sonic" "go.uber.org/zap" @@ -133,7 +135,19 @@ func handleTickerAllMessage(msg []byte) { // log.Debug(symbol, "ticker@all ws处理失败", err) continue } - tradeSet.LastPrice = utility.StringFloat64Cut(data["c"].(string), int32(tradeSet.PriceDigit)) + var utcTime int64 + var lastPrice decimal.Decimal + lastPriceKey := fmt.Sprintf(rediskey.FutureTickerLastPrice, global.EXCHANGE_BINANCE, symbol) + + if e, ok := data["E"]; ok { + utcTime = utility.ToInt64(e) + } + + if e, ok := data["c"]; ok { + lastPrice = utility.StrToDecimal(e.(string)).Truncate(int32(tradeSet.PriceDigit)) + } + + tradeSet.LastPrice = lastPrice.String() tradeSet.OpenPrice = utility.StrToFloatCut(data["o"].(string), int32(tradeSet.PriceDigit)) tradeSet.HighPrice = utility.StringFloat64Cut(data["h"].(string), int32(tradeSet.PriceDigit)) tradeSet.LowPrice = utility.StringFloat64Cut(data["l"].(string), int32(tradeSet.PriceDigit)) @@ -147,6 +161,16 @@ func handleTickerAllMessage(msg []byte) { log.Error(symbol, "ticker@all ws处理失败", err) } } + + //行情存储时间 + lastUtc := utcTime - 1000*60*60 + if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil { + log.Errorf("移除 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err) + } + + if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil { + log.Errorf("添加 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err) + } } if len(trades) > 0 { diff --git a/services/spotservice/binancemarket.go b/services/spotservice/binancemarket.go index 2b5c9c6..8759ad1 100644 --- a/services/spotservice/binancemarket.go +++ b/services/spotservice/binancemarket.go @@ -204,6 +204,7 @@ func handleTickerMessage(msg []byte) { } key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BINANCE, symbolName) + lastPriceKey := fmt.Sprintf(rediskey.SpotTickerLastPrice, global.EXCHANGE_BINANCE, symbolName) tcVal, _ := helper.DefaultRedis.GetString(key) tradeSet := models.TradeSet{} if err := sonic.UnmarshalString(tcVal, &tradeSet); err != nil { @@ -212,7 +213,19 @@ func handleTickerMessage(msg []byte) { } continue } - tradeSet.LastPrice = utility.StringFloat64Cut(dataMap["c"].(string), int32(tradeSet.PriceDigit)) + + var utcTime int64 + var lastPrice decimal.Decimal + + if e, ok := dataMap["E"]; ok { + utcTime = utility.ToInt64(e) + } + + if e, ok := dataMap["c"]; ok { + lastPrice = utility.StrToDecimal(e.(string)).Truncate(int32(tradeSet.PriceDigit)) + } + + tradeSet.LastPrice = lastPrice.String() tradeSet.OpenPrice = utility.StrToFloatCut(dataMap["o"].(string), int32(tradeSet.PriceDigit)) tradeSet.HighPrice = utility.StringFloat64Cut(dataMap["h"].(string), int32(tradeSet.PriceDigit)) tradeSet.LowPrice = utility.StringFloat64Cut(dataMap["l"].(string), int32(tradeSet.PriceDigit)) @@ -236,6 +249,17 @@ func handleTickerMessage(msg []byte) { log.Error("redis保存交易对失败", tradeSet.Coin, tradeSet.Currency) } } + + //行情存储时间 + lastUtc := utcTime - 1000*60*60 + + if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil { + log.Errorf("移除 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err) + } + + if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil { + log.Errorf("添加 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err) + } } //判断触发现货下单