diff --git a/services/binanceservice/binancerest.go b/services/binanceservice/binancerest.go index 14e3704..a048b69 100644 --- a/services/binanceservice/binancerest.go +++ b/services/binanceservice/binancerest.go @@ -1,7 +1,6 @@ package binanceservice import ( - "context" "errors" "fmt" DbModels "go-admin/app/admin/models" @@ -15,7 +14,6 @@ import ( "go-admin/pkg/httputils" "go-admin/pkg/utility" "strings" - "time" "github.com/shopspring/decimal" "gorm.io/gorm" @@ -405,227 +403,6 @@ func GetClient(apiUserInfo *DbModels.LineApiUser) *helper.BinanceClient { return client } -/* -判断是否触发 -*/ -func JudgeSpotPrice(trade models.TradeSet) { - key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) - preOrderVal, _ := helper.DefaultRedis.GetAllList(key) - db := GetDBConnection() - - if len(preOrderVal) == 0 { - return - } - - spotApi := SpotRestApi{} - for _, item := range preOrderVal { - preOrder := dto.PreOrderRedisList{} - if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { - log.Error("反序列化失败") - continue - } - - if preOrder.Symbol == trade.Coin+trade.Currency { - orderPrice, _ := decimal.NewFromString(preOrder.Price) - tradePrice, _ := decimal.NewFromString(trade.LastPrice) - //买入 - if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { - SpotOrderLock(db, &preOrder, item, spotApi) - } - } - } -} - -// 分布式锁下单 -// v 预下单信息 -// item 预下单源文本 -func SpotOrderLock(db *gorm.DB, v *dto.PreOrderRedisList, item string, spotApi SpotRestApi) { - lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) - - if ok, err := lock.AcquireWait(context.Background()); err != nil { - log.Error("获取锁失败", err) - return - } else if ok { - defer lock.Release() - - key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) - preOrder := DbModels.LinePreOrder{} - if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil { - log.Error("获取预下单失败", err) - - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Error("不存在待触发主单", item) - helper.DefaultRedis.LRem(key, item) - } - - return - } - - hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item) - - if !hasrecord { - log.Error("不存在待触发主单", item) - return - } - - price, _ := decimal.NewFromString(v.Price) - num, _ := decimal.NewFromString(preOrder.Num) - params := OrderPlacementService{ - ApiId: v.ApiId, - Symbol: v.Symbol, - Side: v.Site, - Type: preOrder.MainOrderType, - TimeInForce: "GTC", - Price: price, - Quantity: num, - NewClientOrderId: v.OrderSn, - } - preOrderVal, _ := sonic.MarshalString(&v) - - if err := spotApi.OrderPlace(db, params); err != nil { - log.Error("下单失败", v.Symbol, " err:", err) - err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status =0", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error - - if err != nil { - log.Error("下单失败后修改订单失败") - } - - if preOrderVal != "" { - if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { - log.Error("删除redis 预下单失败:", err) - } - } - - return - } - - if preOrderVal != "" { - if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { - log.Error("删除redis 预下单失败:", err) - } - } - - if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil { - log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1") - } - return - } else { - log.Error("获取锁失败") - return - } -} - -// 判断是否触发止损 -func JudgeSpotStopLoss(trade models.TradeSet) { - key := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE) - stopLossVal, _ := helper.DefaultRedis.GetAllList(key) - - if len(stopLossVal) == 0 { - return - } - - db := GetDBConnection() - spotApi := SpotRestApi{} - setting, err := GetSystemSetting(db) - - if err != nil { - log.Error("获取系统设置失败") - return - } - - tradeSet, err := GetTradeSet(trade.Coin+trade.Currency, 0) - - if err != nil { - log.Error("获取交易设置失败") - return - } - - for _, item := range stopLossVal { - stopOrder := dto.StopLossRedisList{} - if err := sonic.Unmarshal([]byte(item), &stopOrder); err != nil { - log.Error("反序列化失败") - continue - } - - if stopOrder.Symbol == trade.Coin+trade.Currency { - orderPrice := stopOrder.Price - tradePrice, _ := decimal.NewFromString(trade.LastPrice) - //买入 - if strings.ToUpper(stopOrder.Site) == "SELL" && - orderPrice.Cmp(tradePrice) >= 0 && - orderPrice.Cmp(decimal.Zero) > 0 && - tradePrice.Cmp(decimal.Zero) > 0 { - - SpotStopLossTrigger(db, stopOrder, spotApi, setting, tradeSet, key, item) - } - } - } -} - -// 触发现货止损 -func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi SpotRestApi, setting DbModels.LineSystemSetting, tradeSet models.TradeSet, key string, item string) { - lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, stopOrder.ApiId, stopOrder.Symbol), 20, 5, 100*time.Millisecond) - - if ok, err := lock.AcquireWait(context.Background()); err != nil { - log.Error("获取锁失败", err) - return - } else if ok { - defer lock.Release() - takeOrder := DbModels.LinePreOrder{} - if err := db.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type =1", stopOrder.PId).Find(&takeOrder).Error; err != nil { - log.Error("查询止盈单失败") - return - } - - apiInfo, _ := GetApiInfo(takeOrder.ApiId) - - if apiInfo.Id == 0 { - log.Error("现货止损 查询api用户不存在") - return - } - - var err error - for x := 1; x <= 4; x++ { - err = spotApi.CancelOpenOrderByOrderSn(apiInfo, takeOrder.Symbol, takeOrder.OrderSn) - - if err == nil { - break - } - - } - - if err != nil { - log.Error("现货止损撤单失败", err) - return - } - stopPreOrder, _ := GetOrderById(db, stopOrder.Id) - price := stopOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.StopLossPremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) - num := utility.StrToDecimal(takeOrder.Num).Truncate(int32(tradeSet.AmountDigit)) - - params := OrderPlacementService{ - ApiId: takeOrder.ApiId, - Side: takeOrder.Site, - Type: "LIMIT", - TimeInForce: "GTC", - Symbol: takeOrder.Symbol, - Price: price, - Quantity: num, - NewClientOrderId: stopPreOrder.OrderSn, - } - - if err := spotApi.OrderPlace(db, params); err != nil { - log.Errorf("现货止损挂单失败 id:%s err:%v", stopOrder.Id, err) - } - - if _, err := helper.DefaultRedis.LRem(key, item); err != nil { - log.Errorf("现货止损 删除缓存失败 id:%v err:%v", stopOrder.Id, err) - } - } else { - log.Error("获取锁失败") - } - -} - /* 获取api用户信息 */ diff --git a/services/binanceservice/futuresbinancerest.go b/services/binanceservice/futuresbinancerest.go index af5fd4e..4f68390 100644 --- a/services/binanceservice/futuresbinancerest.go +++ b/services/binanceservice/futuresbinancerest.go @@ -1,7 +1,6 @@ package binanceservice import ( - "context" "errors" "fmt" DbModels "go-admin/app/admin/models" @@ -723,129 +722,6 @@ func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decima return nil } -/* -判断合约触发 -*/ -func JudgeFuturesPrice(tradeSet models.TradeSet) { - preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE)) - db := GetDBConnection() - - if len(preOrderVal) == 0 { - return - } - futApi := FutRestApi{} - - for _, item := range preOrderVal { - preOrder := dto.PreOrderRedisList{} - if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { - log.Error("反序列化失败") - continue - } - - if preOrder.Symbol != tradeSet.Coin+tradeSet.Currency { - continue - } - - orderPrice, _ := decimal.NewFromString(preOrder.Price) - tradePrice, _ := decimal.NewFromString(tradeSet.LastPrice) - - if orderPrice.Cmp(decimal.Zero) == 0 || tradePrice.Cmp(decimal.Zero) == 0 { - continue - } - - //多 - if (strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0) || - (strings.ToUpper(preOrder.Site) == "SELL" && orderPrice.Cmp(tradePrice) <= 0) { - futTriggerOrder(db, &preOrder, item, futApi) - } - } -} - -// 分布式锁下单 -func futTriggerOrder(db *gorm.DB, v *dto.PreOrderRedisList, item string, futApi FutRestApi) { - lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 200, 5, 100*time.Millisecond) - - if ok, err := lock.AcquireWait(context.Background()); err != nil { - log.Debug("获取锁失败", err) - return - } else if ok { - defer lock.Release() - - key := fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE) - preOrder := DbModels.LinePreOrder{} - if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil { - log.Error("获取预下单失败", err) - - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Error("不存在待触发主单", item) - helper.DefaultRedis.LRem(key, item) - } - - return - } - - hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item) - - if !hasrecord { - log.Debug("预下单缓存中不存在", item) - return - } - - price, _ := decimal.NewFromString(v.Price) - num, _ := decimal.NewFromString(preOrder.Num) - - if price.Cmp(decimal.Zero) == 0 { - log.Error("价格不能为0") - return - } - - params := FutOrderPlace{ - ApiId: v.ApiId, - Symbol: v.Symbol, - Side: v.Site, - OrderType: preOrder.MainOrderType, - SideType: preOrder.MainOrderType, - Price: price, - Quantity: num, - NewClientOrderId: v.OrderSn, - } - preOrderVal, _ := sonic.MarshalString(&v) - - if err := futApi.OrderPlace(db, params); err != nil { - log.Error("下单失败", v.Symbol, " err:", err) - err := db.Model(&DbModels.LinePreOrder{}).Where("id =? and status='0'", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error - - if err != nil { - log.Error("更新预下单状态失败") - } - - if preOrderVal != "" { - if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { - log.Error("删除redis 预下单失败:", err) - } - } - - return - } - - if preOrderVal != "" { - if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { - log.Error("删除redis 预下单失败:", err) - } - } - - if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil { - log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1") - } - - return - } else { - log.Error("获取锁失败") - - return - } -} - // CancelFutOrder 通过单个订单号取消合约委托 // symbol 交易对 // newClientOrderId 系统自定义订单号 @@ -865,7 +741,7 @@ func (e FutRestApi) CancelFutOrder(apiUserInfo DbModels.LineApiUser, symbol stri if err != nil { var dataMap map[string]interface{} if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil { - return errors.New(fmt.Sprintf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, err.Error())) + return fmt.Errorf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, err.Error()) } code, ok := dataMap["code"] if ok { @@ -875,7 +751,7 @@ func (e FutRestApi) CancelFutOrder(apiUserInfo DbModels.LineApiUser, symbol stri errContent = err.Error() } - return errors.New(fmt.Sprintf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, errContent)) + return fmt.Errorf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, errContent) } } return nil @@ -920,7 +796,7 @@ func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol s // newClientOrderIdList 系统自定义的订单号, 最多支持10个订单 func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error { if len(newClientOrderIdList) > 10 { - return errors.New(fmt.Sprintf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, "最多支持10个订单")) + return fmt.Errorf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, "最多支持10个订单") } endpoint := "/fapi/v1/batchOrders" marshal, _ := sonic.Marshal(newClientOrderIdList) @@ -942,7 +818,7 @@ func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol var dataMap map[string]interface{} if err.Error() != "" { if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil { - return errors.New(fmt.Sprintf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, err.Error())) + return fmt.Errorf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, err.Error()) } } code, ok := dataMap["code"] diff --git a/services/binanceservice/futuresjudgeservice.go b/services/binanceservice/futuresjudgeservice.go new file mode 100644 index 0000000..abd751d --- /dev/null +++ b/services/binanceservice/futuresjudgeservice.go @@ -0,0 +1,376 @@ +package binanceservice + +import ( + "context" + "errors" + "fmt" + DbModels "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/common/const/rediskey" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/models" + "go-admin/pkg/utility" + "strings" + "time" + + "github.com/bytedance/sonic" + log "github.com/go-admin-team/go-admin-core/logger" + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +/* +判断合约触发 +*/ +func JudgeFuturesPrice(tradeSet models.TradeSet) { + preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE)) + db := GetDBConnection() + + if len(preOrderVal) == 0 { + return + } + futApi := FutRestApi{} + + for _, item := range preOrderVal { + preOrder := dto.PreOrderRedisList{} + if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if preOrder.Symbol != tradeSet.Coin+tradeSet.Currency { + continue + } + + orderPrice, _ := decimal.NewFromString(preOrder.Price) + tradePrice, _ := decimal.NewFromString(tradeSet.LastPrice) + + if orderPrice.Cmp(decimal.Zero) == 0 || tradePrice.Cmp(decimal.Zero) == 0 { + continue + } + + //多 + if (strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0) || + (strings.ToUpper(preOrder.Site) == "SELL" && orderPrice.Cmp(tradePrice) <= 0) { + futTriggerOrder(db, &preOrder, item, futApi) + } + } +} + +// 分布式锁下单 +func futTriggerOrder(db *gorm.DB, v *dto.PreOrderRedisList, item string, futApi FutRestApi) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 200, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Debug("获取锁失败", err) + return + } else if ok { + defer lock.Release() + + key := fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE) + preOrder := DbModels.LinePreOrder{} + if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil { + log.Error("获取预下单失败", err) + + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Error("不存在待触发主单", item) + helper.DefaultRedis.LRem(key, item) + } + + return + } + + hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item) + + if !hasrecord { + log.Debug("预下单缓存中不存在", item) + return + } + + price, _ := decimal.NewFromString(v.Price) + num, _ := decimal.NewFromString(preOrder.Num) + + if price.Cmp(decimal.Zero) == 0 { + log.Error("价格不能为0") + return + } + + params := FutOrderPlace{ + ApiId: v.ApiId, + Symbol: v.Symbol, + Side: v.Site, + OrderType: preOrder.MainOrderType, + SideType: preOrder.MainOrderType, + Price: price, + Quantity: num, + NewClientOrderId: v.OrderSn, + } + preOrderVal, _ := sonic.MarshalString(&v) + + if err := futApi.OrderPlace(db, params); err != nil { + log.Error("下单失败", v.Symbol, " err:", err) + err := db.Model(&DbModels.LinePreOrder{}).Where("id =? and status='0'", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error + + if err != nil { + log.Error("更新预下单状态失败") + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + return + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil { + log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1") + } + + return + } else { + log.Error("获取锁失败") + + return + } +} + +// 判断是否触发合约减仓 +func JudgeFuturesReduce(trade models.TradeSet) { + key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) + reduceVal, _ := helper.DefaultRedis.GetAllList(key) + + if len(reduceVal) == 0 { + return + } + + db := GetDBConnection() + spotApi := SpotRestApi{} + setting, err := GetSystemSetting(db) + + if err != nil { + log.Error("获取系统设置失败") + return + } + + tradeSet, err := GetTradeSet(trade.Coin+trade.Currency, 0) + + if err != nil { + log.Error("获取交易设置失败") + return + } + + for _, item := range reduceVal { + reduceOrder := ReduceListItem{} + if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if reduceOrder.Symbol == trade.Coin+trade.Currency { + orderPrice := reduceOrder.Price + tradePrice, _ := decimal.NewFromString(trade.LastPrice) + //买入 + if orderPrice.Cmp(decimal.Zero) > 0 && + tradePrice.Cmp(decimal.Zero) > 0 && + ((strings.ToUpper(reduceOrder.Side) == "SELL" && orderPrice.Cmp(tradePrice) >= 0) || + (strings.ToUpper(reduceOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) <= 0)) { + + SpotReduceTrigger(db, reduceOrder, spotApi, setting, tradeSet, key, item) + } + } + } +} + +// 触发合约减仓 +func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, tradeSet models.TradeSet, key, item string) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Error("获取锁失败", err) + return + } else if ok { + defer lock.Release() + takeOrder := DbModels.LinePreOrder{} + if err := db.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type =1", reduceOrder.Pid).Find(&takeOrder).Error; err != nil { + log.Error("查询止盈单失败") + return + } + + apiInfo, _ := GetApiInfo(takeOrder.ApiId) + + if apiInfo.Id == 0 { + log.Error("现货减仓 查询api用户不存在") + return + } + + var err error + for x := 1; x <= 4; x++ { + err = futApi.CancelFutOrder(apiInfo, takeOrder.Symbol, takeOrder.OrderSn) + + if err == nil { + break + } + + } + + if err != nil { + log.Error("合约止盈撤单失败", err) + return + } + price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) + num := utility.StrToDecimal(takeOrder.Num).Truncate(int32(tradeSet.AmountDigit)) + + params := FutOrderPlace{ + ApiId: reduceOrder.ApiId, + Side: reduceOrder.Side, + OrderType: "LIMIT", + Symbol: reduceOrder.Symbol, + Price: price, + Quantity: num, + NewClientOrderId: reduceOrder.OrderSn, + } + + if err := futApi.OrderPlace(db, params); err != nil { + log.Errorf("合约减仓挂单失败 id:%s err:%v", reduceOrder.Id, err) + } + + if _, err := helper.DefaultRedis.LRem(key, item); err != nil { + log.Errorf("合约减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err) + } + } else { + log.Error("获取锁失败") + } +} + +// 判断现货加仓 +func JudgeFutAddPosition(trade models.TradeSet) { + key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) + preOrderVal, _ := helper.DefaultRedis.GetAllList(key) + db := GetDBConnection() + + if len(preOrderVal) == 0 { + return + } + + futApi := FutRestApi{} + for _, item := range preOrderVal { + preOrder := AddPositionList{} + if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if preOrder.Symbol == trade.Coin+trade.Currency { + orderPrice := preOrder.Price + tradePrice, _ := decimal.NewFromString(trade.LastPrice) + + if orderPrice.Cmp(decimal.Zero) == 0 || tradePrice.Cmp(decimal.Zero) == 0 { + continue + } + + //多 + if (strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0) || + (strings.ToUpper(preOrder.Side) == "SELL" && orderPrice.Cmp(tradePrice) <= 0) { + FutAddPositionTrigger(db, &preOrder, item, futApi) + } + } + } +} + +// 合约加仓触发 +func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi FutRestApi) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Error("获取锁失败", err) + return + } else if ok { + defer lock.Release() + + setting, _ := GetSystemSetting(db) + tradeSet, _ := GetTradeSet(v.Symbol, 0) + + if tradeSet.LastPrice == "" { + log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) + return + } + + key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) + preOrder := DbModels.LinePreOrder{} + if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil { + log.Error("获取预下单失败", err) + + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Error("不存在待触发加仓主单", item) + helper.DefaultRedis.LRem(key, item) + } + + return + } + + hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item) + + if !hasrecord { + log.Error("不存在待触发加仓主单", item) + return + } + + price := v.Price + num, _ := decimal.NewFromString(preOrder.Num) + + if setting.AddPositionPremium.Cmp(decimal.Zero) > 0 { + price = price.Mul(decimal.NewFromInt(1).Sub(setting.AddPositionPremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) + } + + params := FutOrderPlace{ + ApiId: v.ApiId, + Symbol: v.Symbol, + Side: v.Side, + OrderType: "LIMIT", + Price: price, + Quantity: num, + NewClientOrderId: v.OrderSn, + } + preOrderVal, _ := sonic.MarshalString(&v) + + if err := futApi.OrderPlace(db, params); err != nil { + log.Error("下单失败", v.Symbol, " err:", err) + err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status =0", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error + + if err != nil { + log.Error("下单失败后修改订单失败") + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + return + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil { + log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1") + } + return + } else { + log.Error("获取锁失败") + return + } +} diff --git a/services/binanceservice/futuresrest.go b/services/binanceservice/futuresrest.go index ea59d08..714c27d 100644 --- a/services/binanceservice/futuresrest.go +++ b/services/binanceservice/futuresrest.go @@ -2,15 +2,18 @@ package binanceservice import ( "context" + "errors" "fmt" "go-admin/app/admin/models" DbModels "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" "go-admin/common/const/rediskey" "go-admin/common/global" "go-admin/common/helper" models2 "go-admin/models" "go-admin/pkg/utility" "go-admin/pkg/utility/snowflakehelper" + "strconv" "strings" "time" @@ -100,7 +103,6 @@ func handleFutOrderByType(db *gorm.DB, preOrder *DbModels.LinePreOrder, orderSta //减仓回调 case preOrder.OrderType == 4 && orderStatus == 6: handleReduceFilled(db, preOrder) - //止损成交 case preOrder.OrderType == 2 && orderStatus == 6: handleStopLoss(db, preOrder) @@ -161,33 +163,9 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { // 不是100%减仓 就需要挂止盈止损 if rate < 100 { - futApi := FutRestApi{} - positions, err := futApi.GetPositionV3(&apiUserInfo, preOrder.Symbol) - var num decimal.Decimal + totalNum, err := getFuturesPositionNum(apiUserInfo, preOrder, tradeSet) if err != nil { - logger.Errorf("handleMainReduceFilled 获取持仓信息失败,交易对:%s ,订单号:%s", preOrder.Symbol, preOrder.OrderSn) - return - } - - for _, item := range positions { - if item.Symbol == preOrder.Symbol { - positionAmt := utility.StrToDecimal(item.PositionAmt) - - //多 - if positionAmt.Cmp(decimal.Zero) > 0 && preOrder.Site == "SELL" { - num = positionAmt.Abs().Truncate(int32(tradeSet.AmountDigit)) - break - } else if positionAmt.Cmp(decimal.Zero) < 0 && preOrder.Site == "BUY" { - //空 - num = positionAmt.Abs().Truncate(int32(tradeSet.AmountDigit)) - break - } - } - } - - if num.Cmp(decimal.Zero) <= 0 { - logger.Errorf("handleMainReduceFilled 获取持仓数量为0,交易对:%s ,订单号:%s", preOrder.Symbol, preOrder.OrderSn) return } @@ -203,7 +181,7 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { takeProfitOrder.CreatedAt = time.Now() takeProfitOrder.BuyPrice = "0" takeProfitOrder.MainOrderType = "LIMIT" - takeProfitOrder.Num = num.String() + takeProfitOrder.Num = totalNum.String() orders = append(orders, takeProfitOrder) //有止损单 @@ -220,7 +198,7 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { stoploss.BuyPrice = "0" stoploss.Rate = "100" stoploss.MainOrderType = "LIMIT" - stoploss.Num = num.String() + stoploss.Num = totalNum.String() orders = append(orders, stoploss) } @@ -230,23 +208,12 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { return } - spotApi := SpotRestApi{} - paramsMap := OrderPlacementService{ - ApiId: takeProfitOrder.ApiId, - Symbol: takeProfitOrder.Symbol, - Side: takeProfitOrder.Site, - Type: "LIMIT", - TimeInForce: "GTC", - Price: utility.StrToDecimal(takeProfitOrder.Price), - Quantity: num, - NewClientOrderId: takeProfitOrder.OrderSn, - StopPrice: utility.StrToDecimal(takeProfitOrder.Price), - } - if err := spotApi.OrderPlace(db, paramsMap); err != nil { - logger.Errorf("减仓后重下止盈失败 减仓order_sn:%s err:%v", preOrder.OrderSn, err) - - if err2 := db.Model(&takeProfitOrder).Updates(map[string]interface{}{"status": 2, "": err.Error()}).Error; err2 != nil { - logger.Errorf("handleMainReduceFilled 更新止盈单失败:%v", err2) + futApi := FutRestApi{} + for _, v := range orders { + if v.OrderType == 1 { + processFutTakeProfitOrder(db, futApi, v, totalNum) + } else if v.OrderType == 2 { + processFutStopLossOrder(db, v, utility.StrToDecimal(v.Price), totalNum) } } } @@ -259,7 +226,7 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { return } - keySpotAddPosition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) + keyFutAddpositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) addPositionData := AddPositionList{ MainId: addPositionOrder.MainId, @@ -278,199 +245,121 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { return } - if err := helper.DefaultRedis.RPushList(keySpotAddPosition, addVal); err != nil { + if err := helper.DefaultRedis.RPushList(keyFutAddpositionKey, addVal); err != nil { logger.Errorf("handleMainReduceFilled 添加加仓单失败,订单号:%s err:%v", preOrder.OrderSn, err) } } +// 获取币安合约持仓 +func getFuturesPositionNum(apiUserInfo DbModels.LineApiUser, preOrder *DbModels.LinePreOrder, tradeSet models2.TradeSet) (decimal.Decimal, error) { + futApi := FutRestApi{} + positions, err := futApi.GetPositionV3(&apiUserInfo, preOrder.Symbol) + var num decimal.Decimal + + if err != nil { + logger.Errorf("handleMainReduceFilled 获取持仓信息失败,交易对:%s ,订单号:%s", preOrder.Symbol, preOrder.OrderSn) + return num, err + } + + for _, item := range positions { + if item.Symbol == preOrder.Symbol { + positionAmt := utility.StrToDecimal(item.PositionAmt) + + //多 + if positionAmt.Cmp(decimal.Zero) > 0 && preOrder.Site == "SELL" { + num = positionAmt.Abs().Truncate(int32(tradeSet.AmountDigit)) + break + } else if positionAmt.Cmp(decimal.Zero) < 0 && preOrder.Site == "BUY" { + //空 + num = positionAmt.Abs().Truncate(int32(tradeSet.AmountDigit)) + break + } + } + } + + if num.Cmp(decimal.Zero) <= 0 { + logger.Errorf("handleMainReduceFilled 获取持仓数量为0,交易对:%s ,订单号:%s", preOrder.Symbol, preOrder.OrderSn) + return num, errors.New("获取持仓数量为0") + } + return num, nil +} + // 平仓单成交 func handleClosePosition(db *gorm.DB, preOrder *DbModels.LinePreOrder) { - panic("unimplemented") + removeFutLossAndAddPosition(preOrder) + ids := []int{preOrder.Pid, preOrder.MainId} + //主单止盈成交 + if err := db.Model(&DbModels.LinePreOrder{}).Where("id IN ?", ids).Update("status", 9).Error; err != nil { + logger.Errorf("平仓单成功 修改主单状态失败 订单号:%s:", err) + } } // 止损单成交 func handleStopLoss(db *gorm.DB, preOrder *DbModels.LinePreOrder) { - // 获取主订单 - order, err := GetOrderById(db, preOrder.Pid) - if err != nil { - logger.Errorf("止损单成交 获取主单失败 订单号:%s err:%v", preOrder.OrderSn, err) - return + removeFutLossAndAddPosition(preOrder) + ids := []int{preOrder.Pid, preOrder.MainId} + //主单止损成交 + if err := db.Model(&DbModels.LinePreOrder{}).Where("id IN ?", ids).Update("status", 9).Error; err != nil { + logger.Errorf("止损单成功 修改主单状态失败 订单号:%s:", err) } - - // 检查订单类型 - if order.OrderCategory != 1 { - if err := db.Model(&order).Where("pid =? AND order_status <9 AND order_type =0", preOrder.Pid).Error; err != nil { - logger.Errorf("对冲止损单成交 修改对冲主单状态失败 订单号:%s err:%v", order.OrderSn, err) - } - return - } - - // 修改主单状态 - if err := db.Model(&order).Where("id =? AND order_status < 9", preOrder.Pid).Error; err != nil { - logger.Errorf("止损单成交 修改主单状态失败 订单号:%s err:%v", order.OrderSn, err) - return - } - - // 检查是否有对冲 - if order.CoverType <= 0 { - return - } - - triggerHedgeOrder(order, db, preOrder) -} - -// 下合约对冲单 -func triggerHedgeOrder(order DbModels.LinePreOrder, db *gorm.DB, preOrder *DbModels.LinePreOrder) bool { - symbolName := utility.ReplaceSuffix(order.Symbol, order.QuoteSymbol, "USDT") - // 获取交易对配置 - tradeSet, err := GetTradeSet(symbolName, 1) - if tradeSet.Coin == "" || err != nil { - logger.Errorf("止损单成交 获取交易对配置失败 交易对:%s 订单号:%s err:%v", symbolName, order.OrderSn, err) - return true - } - - // 检查对冲单购买金额 - // if order.HedgeBuyTotal.Cmp(decimal.Zero) <= 0 { - // logger.Errorf("止损单成交 对冲单购买金额为0 订单号:%s", order.OrderSn) - // return true - // } - - // 获取系统设置 - setting, err := GetSystemSetting(db) - if err != nil { - logger.Errorf("止损单成交 获取系统设置失败 订单号:%s err:%v", order.OrderSn, err) - return true - } - - // 获取API信息 - apiInfo, err := GetChildApiInfo(order.ApiId) - if err != nil { - logger.Errorf("止损单成交 获取api信息失败 订单号:%s err:%v", order.OrderSn, err) - return true - } - - // 计算价格 - price := utility.StrToDecimal(preOrder.Price).Truncate(int32(tradeSet.PriceDigit)) - if order.CoverType == 1 { - price = utility.StrToDecimal(tradeSet.LastPrice).Truncate(int32(tradeSet.PriceDigit)) - } - if price.Cmp(decimal.Zero) <= 0 { - logger.Errorf("单价异常 price:%v 止损单编号:%s 行情:%v", price, order.OrderSn, tradeSet) - return true - } - - // 创建对冲订单 - hedgeOrder, hedgeStoplossOrder, hedgeTakeProfitOrder := createHedgeOrders(order, price, &tradeSet, &apiInfo, &setting) - orders := []DbModels.LinePreOrder{hedgeOrder, hedgeStoplossOrder, hedgeTakeProfitOrder} - - // 批量插入订单 - if err := db.Model(&DbModels.LinePreOrder{}).CreateInBatches(orders, 1).Error; err != nil { - logger.Errorf("主单止损 重下对冲失败,止损单id:%s err:%v", order.OrderSn, err) - return true - } - - // 调用API下单 - if err := placeFutOrder(db, hedgeOrder, price, &apiInfo); err != nil { - logger.Errorf("主单止损 重下对冲失败,止损单号:%s err:%v", order.OrderSn, err) - } - return false -} - -// 创建对冲订单 -func createHedgeOrders(order DbModels.LinePreOrder, price decimal.Decimal, tradeSet *models2.TradeSet, apiInfo *DbModels.LineApiUser, setting *DbModels.LineSystemSetting) (DbModels.LinePreOrder, DbModels.LinePreOrder, DbModels.LinePreOrder) { - // buyPrice := order.HedgeBuyTotal.String() - - // if order.HedgeBuyType == 1 { - // mainTotal := utility.StrToDecimal(order.BuyPrice) - // buyPrice = mainTotal.Mul(order.HedgeBuyTotal).Truncate(int32(tradeSet.PriceDigit)).String() - // } - - hedgeOrder := DbModels.LinePreOrder{ - ApiId: apiInfo.Id, - ExchangeType: order.ExchangeType, - Pid: order.Pid, - Symbol: order.Symbol, - SymbolType: global.SYMBOL_FUTURES, - SignPrice: order.Price, - SignPriceType: "new", - Rate: "0", - // BuyPrice: buyPrice, - OrderCategory: 2, - OrderSn: utility.Int64ToString(snowflakehelper.GetOrderId()), - OrderType: 0, - CoverType: 0, - // MainOrderType: order.HedgeOrderType, - } - - hedgeStoplossOrder := order - hedgeTakeProfitOrder := order - - if order.Site == "BUY" { - hedgeOrder.Site = "SELL" - hedgeStoplossOrder.Site = "BUY" - hedgeTakeProfitOrder.Site = "BUY" - } else { - hedgeOrder.Site = "BUY" - hedgeStoplossOrder.Site = "SELL" - hedgeTakeProfitOrder.Site = "SELL" - } - - // 计算止盈止损价格 - // if hedgeOrder.Site == "BUY" { - // hedgeTakeProfitOrder.Price = price.Mul(decimal.NewFromInt(1).Add(order.HedgeTakeProfit.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() - // hedgeStoplossOrder.Price = price.Mul(decimal.NewFromInt(1).Sub(order.HedgeStopLoss.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() - // } else { - // hedgeTakeProfitOrder.Price = price.Mul(decimal.NewFromInt(1).Sub(order.HedgeTakeProfit.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() - // hedgeStoplossOrder.Price = price.Mul(decimal.NewFromInt(1).Add(order.HedgeStopLoss.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() - // } - - if hedgeOrder.MainOrderType == "LIMIT" { - coverRate := utility.StrToDecimal(setting.CoverOrderTypeBRate) - price = price.Mul(decimal.NewFromInt(1).Add(coverRate)).Truncate(int32(tradeSet.PriceDigit)) - } - - // num := order.HedgeBuyTotal.Div(price).Truncate(int32(tradeSet.AmountDigit)) - hedgeOrder.Price = price.String() - // hedgeOrder.Num = num.String() - - hedgeStoplossOrder.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) - // hedgeStoplossOrder.Rate = order.HedgeStopLoss.String() - // hedgeStoplossOrder.Num = num.String() - - hedgeTakeProfitOrder.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) - // hedgeTakeProfitOrder.Rate = order.HedgeTakeProfit.String() - // hedgeTakeProfitOrder.Num = num.String() - - return hedgeOrder, hedgeStoplossOrder, hedgeTakeProfitOrder -} - -// 调用合约API下单 -func placeFutOrder(db *gorm.DB, hedgeOrder DbModels.LinePreOrder, price decimal.Decimal, apiInfo *DbModels.LineApiUser) error { - futApi := FutRestApi{} - params := FutOrderPlace{ - ApiId: apiInfo.Id, - Symbol: hedgeOrder.Symbol, - Side: hedgeOrder.Site, - Quantity: utility.StrToDecimal(hedgeOrder.Num), - Price: price, - NewClientOrderId: hedgeOrder.OrderSn, - SideType: hedgeOrder.MainOrderType, - OrderType: hedgeOrder.MainOrderType, - } - return futApi.OrderPlace(db, params) } // 止盈单成交 func handleTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder) { - if preOrder.OrderCategory == 1 { - //主单止盈成交 - if err := db.Model(&DbModels.LinePreOrder{}).Where("id = ?", preOrder.Pid).Update("order_type", 3).Error; err != nil { - logger.Errorf("主单止盈成功修改主单状态失败 订单号:%s:", err) + removeFutLossAndAddPosition(preOrder) + + ids := []int{preOrder.Pid, preOrder.MainId} + //主单止盈成交 + if err := db.Model(&DbModels.LinePreOrder{}).Where("id IN ?", ids).Update("status", 9).Error; err != nil { + logger.Errorf("主单止盈成功修改主单状态失败 订单号:%s:", err) + } +} + +// 清除合约缓存 +func removeFutLossAndAddPosition(preOrder *DbModels.LinePreOrder) { + stoplossKey := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE) + stoplossVal, _ := helper.DefaultRedis.GetAllList(stoplossKey) + addPositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) + addPositionVal, _ := helper.DefaultRedis.GetAllList(addPositionKey) + reduceKey := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) + reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) + stoploss := dto.StopLossRedisList{} + addPosition := AddPositionList{} + reduce := ReduceListItem{} + + //止损缓存 + for _, v := range stoplossVal { + sonic.Unmarshal([]byte(v), &stoploss) + if stoploss.MainId == preOrder.MainId { + _, err := helper.DefaultRedis.LRem(stoplossKey, v) + + if err != nil { + logger.Errorf("订单回调失败, 回调订单号:%s 删除止损缓存失败:%v", preOrder.OrderSn, err) + } } - } else { - //对冲单止盈成交 - if err := db.Model(&DbModels.LinePreOrder{}).Where("pid = ? AND order_category = 2 AND order_type =0 AND status < 9", preOrder.Pid).Update("status", 9).Error; err != nil { - logger.Errorf("对冲止盈成功修改主单状态失败 订单号:%s:", err) + } + + //加仓缓存 + for _, v := range addPositionVal { + sonic.Unmarshal([]byte(v), &addPosition) + if addPosition.MainId == preOrder.MainId { + _, err := helper.DefaultRedis.LRem(addPositionKey, v) + + if err != nil { + logger.Errorf("订单回调失败, 回调订单号:%s 删除加仓缓存失败:%v", preOrder.OrderSn, err) + } + } + } + + //减仓缓存 + for _, v := range reduceVal { + sonic.Unmarshal([]byte(v), &reduce) + if reduce.MainId == preOrder.MainId { + _, err := helper.DefaultRedis.LRem(reduceKey, v) + + if err != nil { + logger.Errorf("订单回调失败, 回调订单号:%s 删除减仓缓存失败:%v", preOrder.OrderSn, err) + } } } } @@ -479,13 +368,6 @@ func handleTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder) { // preOrder 主单 func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder) { orders := []models.LinePreOrder{} - if err := db.Model(&DbModels.LinePreOrder{}).Where("pid = ? AND order_type > 0 AND status = '0' ", preOrder.Id).Find(&orders).Error; err != nil { - logger.Error("订单回调查询止盈止损单失败:", err) - return - } - - futApi := FutRestApi{} - num, _ := decimal.NewFromString(preOrder.Num) tradeSet, _ := GetTradeSet(preOrder.Symbol, 0) if tradeSet.Coin == "" { @@ -493,6 +375,21 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder) { return } + if err := db.Model(&DbModels.LinePreOrder{}). + Where("pid = ? AND order_type > 0 AND status = '0' ", preOrder.Id). + Find(&orders).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Error("订单回调查询止盈止损单失败:", err) + return + } else if len(orders) == 0 && preOrder.OrderCategory == 3 { + orders, err = makeFuturesTakeAndReduce(preOrder, db, tradeSet, orders) + if err != nil { + return + } + } + + futApi := FutRestApi{} + num, _ := decimal.NewFromString(preOrder.Num) + for i, order := range orders { if i >= 2 { // 最多处理 2 个订单 break @@ -511,14 +408,122 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder) { case 2: // 止损 processFutStopLossOrder(db, order, price, num) case 4: //减仓 - processFutReduceOrder(db, order, price, num) + processFutReduceOrder(order, price, num) } } } +// 构建合约止盈、减仓单 +func makeFuturesTakeAndReduce(preOrder *DbModels.LinePreOrder, db *gorm.DB, tradeSet models2.TradeSet, orders []DbModels.LinePreOrder) ([]DbModels.LinePreOrder, error) { + ext := models.LinePreOrderExt{} + apiInfo, err := GetApiInfo(preOrder.ApiId) + price := utility.StrToDecimal(preOrder.Price) + + if apiInfo.Id == 0 { + logger.Error("订单回调查询apiuserinfo失败 err:", err) + return nil, errors.New("订单回调查询apiuserinfo失败") + } + + if err := db.Model(&ext).Where("order_id = ?", preOrder.Id).First(&ext).Error; err != nil { + logger.Error("订单回调查询止盈止损单扩展表失败:", err) + return nil, errors.New("订单回调查询止盈止损单扩展表失败") + } + + totalLossAmountU, _ := GetTotalLossAmount(db, preOrder.MainId) + num, err := getFuturesPositionNum(apiInfo, preOrder, tradeSet) + + if err != nil { + logger.Error("订单回调查询持仓数量失败:", err) + return nil, errors.New("订单回调查询持仓数量失败") + } + + //止盈单 + if ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 { + profitOrder := models.LinePreOrder{} + copier.Copy(&profitOrder, preOrder) + var rate decimal.Decimal + + profitOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10) + profitOrder.Pid = preOrder.Id + profitOrder.OrderType = 1 + profitOrder.Status = 0 + profitOrder.MainId = ext.MainOrderId + profitOrder.Num = num.String() + + //止盈需要累加之前的亏损 + if totalLossAmountU.Cmp(decimal.Zero) > 0 { + rate = totalLossAmountU.Div(num).Div(price).Sub(decimal.NewFromInt(1)).Abs().Add(ext.TakeProfitRatio).Truncate(2) + } else { + rate = ext.TakeProfitRatio + } + + profitOrder.Rate = rate.String() + + orders = append(orders, profitOrder) + } + + //减仓单 + if ext.ReducePriceRatio.Cmp(decimal.Zero) > 0 { + stopOrder := models.LinePreOrder{} + copier.Copy(&stopOrder, preOrder) + + stopOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10) + stopOrder.Pid = preOrder.Id + stopOrder.MainId = ext.MainOrderId + stopOrder.OrderType = 4 + stopOrder.Status = 0 + stopOrder.Rate = ext.ReducePriceRatio.String() + stopOrder.Num = num.String() + + orders = append(orders, stopOrder) + } + + for index := range orders { + orderRate := utility.StrToDecimal(orders[index].Rate) + if strings.ToUpper(preOrder.Site) == "BUY" { + orders[index].Site = "SELL" + orders[index].Price = utility.StrToDecimal(preOrder.Price).Mul(decimal.NewFromInt(1).Sub(orderRate.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() + } else { + orders[index].Site = "BUY" + orders[index].Price = utility.StrToDecimal(preOrder.Price).Mul(decimal.NewFromInt(1).Add(orderRate.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() + } + } + + if len(orders) > 0 { + if err := db.Create(&orders).Error; err != nil { + logger.Error("主单回调,创建止盈、减仓单失败") + return orders, errors.New("主单回调,创建止盈、减仓单失败") + } + } + return orders, nil +} + // 减仓单 -func processFutReduceOrder(db *gorm.DB, order DbModels.LinePreOrder, price, num decimal.Decimal) { - // key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) +func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decimal) { + key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) + item := ReduceListItem{ + Id: order.Id, + ApiId: order.ApiId, + Pid: order.Pid, + MainId: order.MainId, + Price: price, + Num: num, + Side: order.Site, + Symbol: order.Symbol, + OrderSn: order.OrderSn, + } + + itemVal, err := sonic.MarshalString(&item) + + if err != nil { + logger.Errorf("序列化失败 err:%v", err) + return + } + + if err := helper.DefaultRedis.RPushList(key, itemVal); err != nil { + logger.Errorf("减仓单写入缓存失败 err:%v", err) + return + } } // 处理止盈订单 diff --git a/services/binanceservice/models.go b/services/binanceservice/models.go index 3f25b0d..c2d04a5 100644 --- a/services/binanceservice/models.go +++ b/services/binanceservice/models.go @@ -188,6 +188,7 @@ type OpenOrders struct { // 待触发加仓单 type AddPositionList struct { + Id int `json:"id"` //订单id Pid int `json:"pid"` //父级id MainId int `json:"mainId"` //主单Id ApiId int `json:"apiId"` //触发账户id @@ -195,6 +196,7 @@ type AddPositionList struct { Price decimal.Decimal `json:"price"` //触发价 Side string `json:"side"` //买卖方向 SymbolType int `json:"type" comment:"交易对类别 1-现货 2-合约"` + OrderSn string `json:"prderSn"` } // SpotAccountInfo 现货账户信息 @@ -225,3 +227,15 @@ type SpotAccountInfo struct { Permissions []string `json:"permissions"` Uid int `json:"uid"` } + +type ReduceListItem struct { + Id int `json:"id"` + ApiId int `json:"apiId"` + MainId int `json:"mainId"` + Pid int `json:"pid"` + Symbol string `json:"symbol"` + Price decimal.Decimal `json:"price"` + Side string `json:"side"` + Num decimal.Decimal `json:"num"` + OrderSn string `json:"orderSn"` +} diff --git a/services/binanceservice/orderservice.go b/services/binanceservice/orderservice.go index a52283e..89df671 100644 --- a/services/binanceservice/orderservice.go +++ b/services/binanceservice/orderservice.go @@ -4,6 +4,7 @@ import ( "go-admin/app/admin/models" DbModels "go-admin/app/admin/models" + "github.com/go-admin-team/go-admin-core/logger" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -112,3 +113,18 @@ func GetOrderExts(db *gorm.DB, mainId int) ([]models.LinePreOrderExt, error) { return result, nil } + +// 获取主单累计亏损 +func GetTotalLossAmount(db *gorm.DB, mainId int) (decimal.Decimal, error) { + var totalLossAmountU decimal.Decimal + + if err := db.Model(&DbModels.LinePreOrder{}). + Where("main_id =? AND order_type =0", mainId). + Select("sum(loss_amount)"). + Find(&totalLossAmountU).Error; err != nil { + logger.Error("查询主订单的实际亏损总金额失败:", err) + return totalLossAmountU, err + } + + return totalLossAmountU, nil +} diff --git a/services/binanceservice/spotjudgeservice.go b/services/binanceservice/spotjudgeservice.go new file mode 100644 index 0000000..98bd908 --- /dev/null +++ b/services/binanceservice/spotjudgeservice.go @@ -0,0 +1,469 @@ +package binanceservice + +import ( + "context" + "errors" + "fmt" + DbModels "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/common/const/rediskey" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/models" + "go-admin/pkg/utility" + "strings" + "time" + + "github.com/bytedance/sonic" + log "github.com/go-admin-team/go-admin-core/logger" + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +/* +判断是否触发 +*/ +func JudgeSpotPrice(trade models.TradeSet) { + key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) + preOrderVal, _ := helper.DefaultRedis.GetAllList(key) + db := GetDBConnection() + + if len(preOrderVal) == 0 { + return + } + + spotApi := SpotRestApi{} + for _, item := range preOrderVal { + preOrder := dto.PreOrderRedisList{} + if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if preOrder.Symbol == trade.Coin+trade.Currency { + orderPrice, _ := decimal.NewFromString(preOrder.Price) + tradePrice, _ := decimal.NewFromString(trade.LastPrice) + //买入 + if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { + SpotOrderLock(db, &preOrder, item, spotApi) + } + } + } +} + +// 分布式锁下单 +// v 预下单信息 +// item 预下单源文本 +func SpotOrderLock(db *gorm.DB, v *dto.PreOrderRedisList, item string, spotApi SpotRestApi) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Error("获取锁失败", err) + return + } else if ok { + defer lock.Release() + + key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) + preOrder := DbModels.LinePreOrder{} + if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil { + log.Error("获取预下单失败", err) + + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Error("不存在待触发主单", item) + helper.DefaultRedis.LRem(key, item) + } + + return + } + + hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item) + + if !hasrecord { + log.Error("不存在待触发主单", item) + return + } + + price, _ := decimal.NewFromString(v.Price) + num, _ := decimal.NewFromString(preOrder.Num) + params := OrderPlacementService{ + ApiId: v.ApiId, + Symbol: v.Symbol, + Side: v.Site, + Type: preOrder.MainOrderType, + TimeInForce: "GTC", + Price: price, + Quantity: num, + NewClientOrderId: v.OrderSn, + } + preOrderVal, _ := sonic.MarshalString(&v) + + if err := spotApi.OrderPlace(db, params); err != nil { + log.Error("下单失败", v.Symbol, " err:", err) + err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status =0", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error + + if err != nil { + log.Error("下单失败后修改订单失败") + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + return + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil { + log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1") + } + return + } else { + log.Error("获取锁失败") + return + } +} + +// 判断是否触发止损 +func JudgeSpotStopLoss(trade models.TradeSet) { + key := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE) + stopLossVal, _ := helper.DefaultRedis.GetAllList(key) + + if len(stopLossVal) == 0 { + return + } + + db := GetDBConnection() + spotApi := SpotRestApi{} + setting, err := GetSystemSetting(db) + + if err != nil { + log.Error("获取系统设置失败") + return + } + + tradeSet, err := GetTradeSet(trade.Coin+trade.Currency, 0) + + if err != nil { + log.Error("获取交易设置失败") + return + } + + for _, item := range stopLossVal { + stopOrder := dto.StopLossRedisList{} + if err := sonic.Unmarshal([]byte(item), &stopOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if stopOrder.Symbol == trade.Coin+trade.Currency { + orderPrice := stopOrder.Price + tradePrice, _ := decimal.NewFromString(trade.LastPrice) + //买入 + if strings.ToUpper(stopOrder.Site) == "SELL" && + orderPrice.Cmp(tradePrice) >= 0 && + orderPrice.Cmp(decimal.Zero) > 0 && + tradePrice.Cmp(decimal.Zero) > 0 { + + SpotStopLossTrigger(db, stopOrder, spotApi, setting, tradeSet, key, item) + } + } + } +} + +// 触发现货止损 +func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi SpotRestApi, setting DbModels.LineSystemSetting, tradeSet models.TradeSet, key string, item string) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, stopOrder.ApiId, stopOrder.Symbol), 20, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Error("获取锁失败", err) + return + } else if ok { + defer lock.Release() + takeOrder := DbModels.LinePreOrder{} + if err := db.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type =1", stopOrder.PId).Find(&takeOrder).Error; err != nil { + log.Error("查询止盈单失败") + return + } + + apiInfo, _ := GetApiInfo(takeOrder.ApiId) + + if apiInfo.Id == 0 { + log.Error("现货止损 查询api用户不存在") + return + } + + var err error + for x := 1; x <= 4; x++ { + err = spotApi.CancelOpenOrderByOrderSn(apiInfo, takeOrder.Symbol, takeOrder.OrderSn) + + if err == nil { + break + } + + } + + if err != nil { + log.Error("现货止损撤单失败", err) + return + } + stopPreOrder, _ := GetOrderById(db, stopOrder.Id) + price := stopOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.StopLossPremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) + num := utility.StrToDecimal(takeOrder.Num).Truncate(int32(tradeSet.AmountDigit)) + + params := OrderPlacementService{ + ApiId: takeOrder.ApiId, + Side: takeOrder.Site, + Type: "LIMIT", + TimeInForce: "GTC", + Symbol: takeOrder.Symbol, + Price: price, + Quantity: num, + NewClientOrderId: stopPreOrder.OrderSn, + } + + if err := spotApi.OrderPlace(db, params); err != nil { + log.Errorf("现货止损挂单失败 id:%s err:%v", stopOrder.Id, err) + } + + if _, err := helper.DefaultRedis.LRem(key, item); err != nil { + log.Errorf("现货止损 删除缓存失败 id:%v err:%v", stopOrder.Id, err) + } + } else { + log.Error("获取锁失败") + } + +} + +// 判断是否触发现货减仓 +func JudgeSpotReduce(trade models.TradeSet) { + key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) + reduceVal, _ := helper.DefaultRedis.GetAllList(key) + + if len(reduceVal) == 0 { + return + } + + db := GetDBConnection() + spotApi := SpotRestApi{} + setting, err := GetSystemSetting(db) + + if err != nil { + log.Error("获取系统设置失败") + return + } + + tradeSet, err := GetTradeSet(trade.Coin+trade.Currency, 0) + + if err != nil { + log.Error("获取交易设置失败") + return + } + + for _, item := range reduceVal { + reduceOrder := ReduceListItem{} + if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if reduceOrder.Symbol == trade.Coin+trade.Currency { + orderPrice := reduceOrder.Price + tradePrice, _ := decimal.NewFromString(trade.LastPrice) + //买入 + if strings.ToUpper(reduceOrder.Side) == "SELL" && + orderPrice.Cmp(tradePrice) >= 0 && + orderPrice.Cmp(decimal.Zero) > 0 && + tradePrice.Cmp(decimal.Zero) > 0 { + + SpotReduceTrigger(db, reduceOrder, spotApi, setting, tradeSet, key, item) + } + } + } +} + +// 触发现货减仓 +func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, tradeSet models.TradeSet, key, item string) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Error("获取锁失败", err) + return + } else if ok { + defer lock.Release() + takeOrder := DbModels.LinePreOrder{} + if err := db.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type =1", reduceOrder.Pid).Find(&takeOrder).Error; err != nil { + log.Error("查询止盈单失败") + return + } + + apiInfo, _ := GetApiInfo(takeOrder.ApiId) + + if apiInfo.Id == 0 { + log.Error("现货减仓 查询api用户不存在") + return + } + + var err error + for x := 1; x <= 4; x++ { + err = spotApi.CancelOpenOrderByOrderSn(apiInfo, takeOrder.Symbol, takeOrder.OrderSn) + + if err == nil { + break + } + + } + + if err != nil { + log.Error("现货止盈撤单失败", err) + return + } + price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) + num := utility.StrToDecimal(takeOrder.Num).Truncate(int32(tradeSet.AmountDigit)) + + params := OrderPlacementService{ + ApiId: reduceOrder.ApiId, + Side: reduceOrder.Side, + Type: "LIMIT", + TimeInForce: "GTC", + Symbol: reduceOrder.Symbol, + Price: price, + Quantity: num, + NewClientOrderId: reduceOrder.OrderSn, + } + + if err := spotApi.OrderPlace(db, params); err != nil { + log.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err) + } + + if _, err := helper.DefaultRedis.LRem(key, item); err != nil { + log.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err) + } + } else { + log.Error("获取锁失败") + } +} + +// 判断现货加仓 +func JudgeSpotAddPosition(trade models.TradeSet) { + key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) + preOrderVal, _ := helper.DefaultRedis.GetAllList(key) + db := GetDBConnection() + + if len(preOrderVal) == 0 { + return + } + + spotApi := SpotRestApi{} + for _, item := range preOrderVal { + preOrder := AddPositionList{} + if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { + log.Error("反序列化失败") + continue + } + + if preOrder.Symbol == trade.Coin+trade.Currency { + orderPrice := preOrder.Price + tradePrice, _ := decimal.NewFromString(trade.LastPrice) + //买入 + if strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { + SpotAddPositionTrigger(db, &preOrder, item, spotApi) + } + } + } +} + +func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotApi SpotRestApi) { + lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) + + if ok, err := lock.AcquireWait(context.Background()); err != nil { + log.Error("获取锁失败", err) + return + } else if ok { + defer lock.Release() + + setting, _ := GetSystemSetting(db) + tradeSet, _ := GetTradeSet(v.Symbol, 0) + + if tradeSet.LastPrice == "" { + log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) + return + } + + key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) + preOrder := DbModels.LinePreOrder{} + if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil { + log.Error("获取预下单失败", err) + + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Error("不存在待触发加仓主单", item) + helper.DefaultRedis.LRem(key, item) + } + + return + } + + hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item) + + if !hasrecord { + log.Error("不存在待触发加仓主单", item) + return + } + + price := v.Price + num, _ := decimal.NewFromString(preOrder.Num) + + if setting.AddPositionPremium.Cmp(decimal.Zero) > 0 { + price = price.Mul(decimal.NewFromInt(1).Sub(setting.AddPositionPremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) + } + + params := OrderPlacementService{ + ApiId: v.ApiId, + Symbol: v.Symbol, + Side: v.Side, + Type: preOrder.MainOrderType, + TimeInForce: "GTC", + Price: price, + Quantity: num, + NewClientOrderId: v.OrderSn, + } + preOrderVal, _ := sonic.MarshalString(&v) + + if err := spotApi.OrderPlace(db, params); err != nil { + log.Error("下单失败", v.Symbol, " err:", err) + err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status =0", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error + + if err != nil { + log.Error("下单失败后修改订单失败") + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + return + } + + if preOrderVal != "" { + if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil { + log.Error("删除redis 预下单失败:", err) + } + } + + if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil { + log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1") + } + return + } else { + log.Error("获取锁失败") + return + } +} diff --git a/services/binanceservice/spotreset.go b/services/binanceservice/spotreset.go index c30eeab..a0c2fef 100644 --- a/services/binanceservice/spotreset.go +++ b/services/binanceservice/spotreset.go @@ -10,6 +10,7 @@ import ( "go-admin/common/const/rediskey" "go-admin/common/global" "go-admin/common/helper" + models2 "go-admin/models" "go-admin/pkg/utility" "go-admin/pkg/utility/snowflakehelper" "strconv" @@ -157,6 +158,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { } price := utility.StrToDecimal(preOrder.Price) + parentOrder, _ := GetOrderById(db, preOrder.Pid) orders := make([]models.LinePreOrder, 0) rate := utility.StringAsFloat(preOrder.Rate) ext := models.LinePreOrderExt{} @@ -165,34 +167,11 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { // 不是100%减仓 就需要挂止盈止损 if rate < 100 { - client := GetClient(&apiUserInfo) - - resp, _, err := client.SendSpotAuth("/api/v3/account", "GET", map[string]interface{}{ - "omitZeroBalances": true, - }) - + num, err := getSpotPositionNum(apiUserInfo, preOrder, tradeSet) if err != nil { - logger.Errorf("api_id:%d 交易对:%s 查询用户信息失败:%v", apiUserInfo.Id, preOrder.Symbol, err) return } - var balanceInfo SpotAccountInfo - sonic.Unmarshal(resp, &balanceInfo) - suffix := utility.ReplaceSuffix(preOrder.Symbol, preOrder.QuoteSymbol, "") - var num decimal.Decimal - - for _, item := range balanceInfo.Balances { - if item.Asset == suffix { - if utility.StrToDecimal(item.Free).Cmp(decimal.Zero) <= 0 { - logger.Error("handleMainReduceFilled 账户余额不足,交易对:%s 订单号:%s", preOrder.Symbol, preOrder.OrderSn) - return - } - - num = utility.StrToDecimal(item.Free).Mul(decimal.NewFromFloat(0.998)).Truncate(int32(tradeSet.AmountDigit)) - break - } - } - takeProfitOrder := models.LinePreOrder{} copier.Copy(&takeProfitOrder, &preOrder) takeProfitOrder.Id = 0 @@ -253,6 +232,17 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { } } + //计算实际亏损 + if parentOrder.Price != "" { + parentPrice := utility.StrToDecimal(parentOrder.Price) + reduceNum := utility.StrToDecimal(preOrder.Num) + lossAmountU := price.Sub(parentPrice).Abs().Mul(reduceNum).Truncate(2) + + if err := db.Model(&parentOrder).Update("loss_amount", lossAmountU).Error; err != nil { + logger.Errorf("修改主单实际亏损失败 订单号:%s err:%v", parentOrder.OrderSn, err) + } + } + //加仓待触发 addPositionOrder := DbModels.LinePreOrder{} @@ -285,6 +275,38 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { } } +// 获取现货持仓数量 +func getSpotPositionNum(apiUserInfo DbModels.LineApiUser, preOrder *DbModels.LinePreOrder, tradeSet models2.TradeSet) (decimal.Decimal, error) { + client := GetClient(&apiUserInfo) + + resp, _, err := client.SendSpotAuth("/api/v3/account", "GET", map[string]interface{}{ + "omitZeroBalances": true, + }) + + if err != nil { + logger.Errorf("api_id:%d 交易对:%s 查询用户信息失败:%v", apiUserInfo.Id, preOrder.Symbol, err) + return decimal.Decimal{}, err + } + + var balanceInfo SpotAccountInfo + sonic.Unmarshal(resp, &balanceInfo) + suffix := utility.ReplaceSuffix(preOrder.Symbol, preOrder.QuoteSymbol, "") + var num decimal.Decimal + + for _, item := range balanceInfo.Balances { + if item.Asset == suffix { + if utility.StrToDecimal(item.Free).Cmp(decimal.Zero) <= 0 { + logger.Error("handleMainReduceFilled 账户余额不足,交易对:%s 订单号:%s", preOrder.Symbol, preOrder.OrderSn) + return decimal.Decimal{}, errors.New("账户余额不足") + } + + num = utility.StrToDecimal(item.Free).Mul(decimal.NewFromFloat(0.998)).Truncate(int32(tradeSet.AmountDigit)) + break + } + } + return num, nil +} + // 主单取消 func handleMainOrderCancel(preOrder *DbModels.LinePreOrder) { preOrderKey := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) @@ -359,9 +381,13 @@ func removeSpotLossAndAddPosition(preOrder *DbModels.LinePreOrder) { stoplossVal, _ := helper.DefaultRedis.GetAllList(stoplossKey) addPositionKey := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) addPositionVal, _ := helper.DefaultRedis.GetAllList(addPositionKey) + reduceKey := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) + reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) stoploss := dto.StopLossRedisList{} addPosition := AddPositionList{} + reduce := ReduceListItem{} + //止损缓存 for _, v := range stoplossVal { sonic.Unmarshal([]byte(v), &stoploss) if stoploss.MainId == preOrder.MainId { @@ -373,6 +399,7 @@ func removeSpotLossAndAddPosition(preOrder *DbModels.LinePreOrder) { } } + //加仓缓存 for _, v := range addPositionVal { sonic.Unmarshal([]byte(v), &addPosition) if addPosition.MainId == preOrder.MainId { @@ -383,6 +410,18 @@ func removeSpotLossAndAddPosition(preOrder *DbModels.LinePreOrder) { } } } + + //减仓缓存 + for _, v := range reduceVal { + sonic.Unmarshal([]byte(v), &reduce) + if reduce.MainId == preOrder.MainId { + _, err := helper.DefaultRedis.LRem(reduceKey, v) + + if err != nil { + logger.Errorf("订单回调失败, 回调订单号:%s 删除减仓缓存失败:%v", preOrder.OrderSn, err) + } + } + } } // 主单成交 @@ -517,14 +556,28 @@ func updateOrderStatus(db *gorm.DB, preOrder *models.LinePreOrder, status int, r // preOrder 主单 func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder) { orders := []models.LinePreOrder{} - if err := db.Model(&DbModels.LinePreOrder{}).Where("pid = ? AND order_category = 1 AND order_type > 0 AND status = '0' ", preOrder.Id).Find(&orders).Error; err != nil { + tradeSet, _ := GetTradeSet(preOrder.Symbol, 0) + + if tradeSet.Coin == "" { + logger.Error("获取交易对失败") + return + } + + if err := db.Model(&DbModels.LinePreOrder{}). + Where("pid = ? AND order_category = 1 AND order_type > 0 AND status = '0' ", preOrder.Id). + Find(&orders).Error; err != nil && errors.Is(err, gorm.ErrRecordNotFound) { logger.Error("订单回调查询止盈止损单失败:", err) return + } else if len(orders) == 0 && preOrder.OrderCategory == 3 { + orders, err = makeSpotTakeAndReduce(preOrder, db, tradeSet, orders) + + if err != nil { + return + } } spotApi := SpotRestApi{} num, _ := decimal.NewFromString(preOrder.Num) - // num = num.Mul(decimal.NewFromFloat(0.998)) for i, order := range orders { if i >= 2 { // 最多处理 2 个订单 @@ -540,10 +593,123 @@ func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrd processTakeProfitOrder(db, spotApi, order, num) case 2: // 止损 processStopLossOrder(order) + case 4: //减仓 + processSpotReduceOrder(order, num) + } } } +// 构建现货止盈、减仓单 +func makeSpotTakeAndReduce(preOrder *DbModels.LinePreOrder, db *gorm.DB, tradeSet models2.TradeSet, orders []DbModels.LinePreOrder) ([]DbModels.LinePreOrder, error) { + ext := models.LinePreOrderExt{} + apiInfo, err := GetApiInfo(preOrder.ApiId) + price := utility.StrToDecimal(preOrder.Price) + + if apiInfo.Id == 0 { + logger.Error("订单回调查询apiuserinfo失败 err:", err) + return nil, errors.New("订单回调查询apiuserinfo失败") + } + + if err := db.Model(&ext).Where("order_id = ?", preOrder.Id).First(&ext).Error; err != nil { + logger.Error("订单回调查询止盈止损单扩展表失败:", err) + return nil, errors.New("订单回调查询止盈止损单扩展表失败") + } + + totalLossAmountU, _ := GetTotalLossAmount(db, preOrder.MainId) + num, err := getSpotPositionNum(apiInfo, preOrder, tradeSet) + + if err != nil { + logger.Error("订单回调查询持仓数量失败:", err) + return nil, errors.New("订单回调查询持仓数量失败") + } + + //止盈单 + if ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 { + profitOrder := models.LinePreOrder{} + copier.Copy(&profitOrder, preOrder) + + profitOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10) + profitOrder.Pid = preOrder.Id + profitOrder.OrderType = 1 + profitOrder.Status = 0 + profitOrder.Rate = ext.TakeProfitRatio.String() + profitOrder.MainId = ext.MainOrderId + profitOrder.Num = num.String() + + //止盈需要累加之前的亏损 + if totalLossAmountU.Cmp(decimal.Zero) > 0 { + profitOrder.Rate = totalLossAmountU.Div(num).Div(price).Sub(decimal.NewFromInt(1)).Abs().Add(ext.TakeProfitRatio).Truncate(2).String() + } else { + profitOrder.Rate = ext.TakeProfitRatio.String() + } + + orders = append(orders, profitOrder) + } + + //减仓单 + if ext.ReducePriceRatio.Cmp(decimal.Zero) > 0 { + stopOrder := models.LinePreOrder{} + copier.Copy(&stopOrder, preOrder) + + stopOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10) + stopOrder.Pid = preOrder.Id + stopOrder.MainId = ext.MainOrderId + stopOrder.OrderType = 4 + stopOrder.Status = 0 + stopOrder.Rate = ext.ReducePriceRatio.String() + stopOrder.Num = num.String() + + orders = append(orders, stopOrder) + } + + for index := range orders { + if strings.ToUpper(preOrder.Site) == "BUY" { + orders[index].Site = "SELL" + orders[index].Price = utility.StrToDecimal(preOrder.Price).Mul(decimal.NewFromInt(1).Sub(ext.ReducePriceRatio.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() + } else { + orders[index].Site = "BUY" + orders[index].Price = utility.StrToDecimal(preOrder.Price).Mul(decimal.NewFromInt(1).Add(ext.ReducePriceRatio.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String() + } + } + + if len(orders) > 0 { + if err := db.Create(&orders).Error; err != nil { + logger.Error("主单回调,创建止盈、减仓单失败") + return orders, errors.New("主单回调,创建止盈、减仓单失败") + } + } + return orders, nil +} + +// 现货减仓 +func processSpotReduceOrder(preOrder DbModels.LinePreOrder, num decimal.Decimal) { + key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) + item := ReduceListItem{ + Id: preOrder.Id, + ApiId: preOrder.ApiId, + Pid: preOrder.Pid, + MainId: preOrder.MainId, + Price: utility.StrToDecimal(preOrder.Price), + Num: num, + Side: preOrder.Site, + Symbol: preOrder.Symbol, + OrderSn: preOrder.OrderSn, + } + + itemVal, err := sonic.MarshalString(&item) + + if err != nil { + logger.Errorf("序列化失败 err:%v", err) + return + } + + if err := helper.DefaultRedis.RPushList(key, itemVal); err != nil { + logger.Errorf("减仓单写入缓存失败 err:%v", err) + return + } +} + // 处理止盈订单 func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder, num decimal.Decimal) { tradeSet, _ := GetTradeSet(order.Symbol, 0) diff --git a/services/futureservice/binancemarket.go b/services/futureservice/binancemarket.go index 72ea0e3..00b71da 100644 --- a/services/futureservice/binancemarket.go +++ b/services/futureservice/binancemarket.go @@ -153,8 +153,10 @@ func handleTickerAllMessage(msg []byte) { for index := range trades { //主单触发 utility.SafeGoParam(binanceservice.JudgeFuturesPrice, trades[index]) - //止损信息 - // utility.SafeGoParam(binanceservice.JudgeFuturesStoplossPrice, trades[index]) + //减仓 + utility.SafeGoParam(binanceservice.JudgeFuturesReduce, trades[index]) + //加仓 + utility.SafeGoParam(binanceservice.JudgeFutAddPosition, trades[index]) } } } diff --git a/services/spotservice/binancemarket.go b/services/spotservice/binancemarket.go index 9481dc4..093790a 100644 --- a/services/spotservice/binancemarket.go +++ b/services/spotservice/binancemarket.go @@ -246,6 +246,12 @@ func handleTickerMessage(msg []byte) { // 止损单 utility.SafeGoParam(binanceservice.JudgeSpotStopLoss, trades[index]) + + //减仓 + utility.SafeGoParam(binanceservice.JudgeSpotReduce, trades[index]) + + //加仓 + utility.SafeGoParam(binanceservice.JudgeFutAddPosition, trades[index]) } } }