From 44ba8bfbf15cd59bc05ca967ee72e2b61de8d5fb Mon Sep 17 00:00:00 2001 From: hucan <951870319@qq.com> Date: Mon, 21 Apr 2025 17:32:57 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=B3=A2=E6=AE=B5=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/models/line_strategy_template.go | 2 +- app/admin/service/dto/line_pre_order.go | 2 +- .../service/dto/line_strategy_template.go | 28 +-- app/admin/service/line_pre_order.go | 94 ++++++++- app/jobs/jobbase.go | 6 +- app/jobs/strategy_job_test.go | 22 +++ common/const/rediskey/redis_key.go | 4 +- common/helper/redislock.go | 10 + .../binanceservice/strategy_order_service.go | 186 +++++++++++++----- .../strategy_order_service_test.go | 26 +++ services/cacheservice/config_service.go | 5 + 11 files changed, 314 insertions(+), 71 deletions(-) create mode 100644 app/jobs/strategy_job_test.go create mode 100644 services/binanceservice/strategy_order_service_test.go diff --git a/app/admin/models/line_strategy_template.go b/app/admin/models/line_strategy_template.go index 3b83c12..7f6f937 100644 --- a/app/admin/models/line_strategy_template.go +++ b/app/admin/models/line_strategy_template.go @@ -14,7 +14,7 @@ type LineStrategyTemplate struct { Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"` CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "` TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"` - TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"` + // TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"` 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 1868464..5952222 100644 --- a/app/admin/service/dto/line_pre_order.go +++ b/app/admin/service/dto/line_pre_order.go @@ -197,7 +197,7 @@ type LineAddPreOrderReq struct { Site string `json:"site" ` //购买方向 BuyPrice string `json:"buy_price" vd:"$>0"` //购买金额 U PricePattern string `json:"price_pattern"` //价格模式 - Price string `json:"price" vd:"$>0"` //下单价百分比 + Price string `json:"price"` //下单价百分比 Profit string `json:"profit" vd:"$>0"` //止盈价 ProfitNumRatio decimal.Decimal `json:"profit_num_ratio"` //止盈数量百分比 ProfitTpTpPriceRatio decimal.Decimal `json:"profit_tp_tp_price_ratio"` //止盈后止盈价百分比 diff --git a/app/admin/service/dto/line_strategy_template.go b/app/admin/service/dto/line_strategy_template.go index 6b0cb4f..c1e8dc5 100644 --- a/app/admin/service/dto/line_strategy_template.go +++ b/app/admin/service/dto/line_strategy_template.go @@ -41,8 +41,8 @@ type LineStrategyTemplateInsertReq struct { Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"` Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"` CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "` - TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"` - TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"` + TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"` + // TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"` common.ControlBy } @@ -63,13 +63,13 @@ func (s *LineStrategyTemplateInsertReq) Valid() error { return errors.New("比较类型不合法") } - if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 { + if s.TimeSlotStart < 0 { return errors.New("时间段不合法") } - if s.TimeSlotEnd < s.TimeSlotStart { - return errors.New("时间段不合法") - } + // if s.TimeSlotEnd < s.TimeSlotStart { + // return errors.New("时间段不合法") + // } return nil } @@ -83,7 +83,7 @@ func (s *LineStrategyTemplateInsertReq) Generate(model *models.LineStrategyTempl model.Percentag = s.Percentag model.CompareType = s.CompareType model.TimeSlotStart = s.TimeSlotStart - model.TimeSlotEnd = s.TimeSlotEnd + // model.TimeSlotEnd = s.TimeSlotEnd model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 } @@ -97,8 +97,8 @@ type LineStrategyTemplateUpdateReq struct { Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"` Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"` CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "` - TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"` - TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"` + TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"` + // TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"` common.ControlBy } @@ -120,13 +120,13 @@ func (s *LineStrategyTemplateUpdateReq) Valid() error { return errors.New("比较类型不合法") } - if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 { + if s.TimeSlotStart < 0 { return errors.New("时间段不合法") } - if s.TimeSlotEnd < s.TimeSlotStart { - return errors.New("时间段不合法") - } + // if s.TimeSlotEnd < s.TimeSlotStart { + // return errors.New("时间段不合法") + // } return nil } @@ -139,7 +139,7 @@ func (s *LineStrategyTemplateUpdateReq) Generate(model *models.LineStrategyTempl model.Percentag = s.Percentag model.CompareType = s.CompareType model.TimeSlotStart = s.TimeSlotStart - model.TimeSlotEnd = s.TimeSlotEnd + // model.TimeSlotEnd = s.TimeSlotEnd model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 } diff --git a/app/admin/service/line_pre_order.go b/app/admin/service/line_pre_order.go index e4ca38c..8b49be6 100644 --- a/app/admin/service/line_pre_order.go +++ b/app/admin/service/line_pre_order.go @@ -279,6 +279,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey) spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey) spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey) + spotStrategyMap := e.GetStrategyOrderListMap(1) + futStrategyMap := e.GetStrategyOrderListMap(2) for _, v := range futAddPositionVal { sonic.Unmarshal([]byte(v), &addPosition) @@ -337,8 +339,14 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi if val, ok := spotRedces[order.Id]; ok { helper.DefaultRedis.LRem(spotReduceKey, val) } + var tradedSetKey string + if order.SymbolType == 1 { + tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol) + } else { + tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol) + } - tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)) + tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey) redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String() marshal, _ := sonic.Marshal(redisList) if order.SymbolType == 1 { @@ -349,6 +357,13 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi helper.DefaultRedis.LRem(listKey, string(marshal)) } + switch { + case order.StrategyTemplateType == 1 && order.SymbolType == 1: + e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap) + case order.StrategyTemplateType == 1 && order.SymbolType == 2: + e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap) + } + //会影响持仓的 removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType) @@ -908,7 +923,6 @@ func saveOrderCache(req *dto.LineAddPreOrderReq, AddOrder models.LinePreOrder, l list.Percentag = linestrategyTemplate.Percentag list.CompareType = linestrategyTemplate.CompareType list.TimeSlotStart = linestrategyTemplate.TimeSlotStart - list.TimeSlotEnd = linestrategyTemplate.TimeSlotEnd marshal, _ = sonic.Marshal(&list) if AddOrder.SymbolType == global.SYMBOL_SPOT { @@ -1995,6 +2009,8 @@ func (e *LinePreOrder) ClearUnTriggered() error { var orderLists []models.LinePreOrder positions := map[string]positiondto.LinePreOrderPositioinDelReq{} e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{}) + spotStrategyMap := e.GetStrategyOrderListMap(1) + futStrategyMap := e.GetStrategyOrderListMap(2) for _, order := range orderLists { redisList := dto.PreOrderRedisList{ @@ -2006,17 +2022,35 @@ func (e *LinePreOrder) ClearUnTriggered() error { OrderSn: order.OrderSn, QuoteSymbol: order.QuoteSymbol, } - tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)) + + var tradedSetKey string + if order.SymbolType == 1 { + tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol) + } else { + tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol) + } + + tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey) redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String() marshal, _ := sonic.Marshal(redisList) + if order.SymbolType == 1 { key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType) + helper.DefaultRedis.LRem(key, string(marshal)) } else { key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType) + helper.DefaultRedis.LRem(key, string(marshal)) } + switch { + case order.StrategyTemplateType == 1 && order.SymbolType == 1: + e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap) + case order.StrategyTemplateType == 1 && order.SymbolType == 2: + e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap) + } + //会影响持仓的 removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType) @@ -2056,6 +2090,60 @@ func (e *LinePreOrder) ClearUnTriggered() error { return nil } +// 移除待策略待触发单 +func (e *LinePreOrder) RemoveStrategyOrderCache(orderId int, symbolType int, exchangeType string, caches *map[string][]dto.StrategyOrderRedisList) { + strategys, _ := (*caches)[exchangeType] + var strategyListKey string + + if symbolType == 1 { + strategyListKey = fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType) + } else { + strategyListKey = fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType) + } + + for _, strategy := range strategys { + if strategy.Id == orderId { + strategyVal, _ := sonic.MarshalString(strategy) + helper.DefaultRedis.LRem(strategyListKey, strategyVal) + } + } +} + +// 获取策略订单缓存列表 +// symbolType 1现货 2合约 +func (e *LinePreOrder) GetStrategyOrderListMap(symbolType int) map[string][]dto.StrategyOrderRedisList { + result := make(map[string][]dto.StrategyOrderRedisList) + var key string + exchanges := []string{global.EXCHANGE_BINANCE} + + if symbolType == 1 { + key = rediskey.StrategySpotOrderList + } else { + key = rediskey.StrategyFutOrderList + } + + for _, exchange := range exchanges { + newKey := fmt.Sprintf(key, exchange) + vals, _ := helper.DefaultRedis.GetAllList(newKey) + itemData := make([]dto.StrategyOrderRedisList, 0) + item := dto.StrategyOrderRedisList{} + + for _, v := range vals { + sonic.Unmarshal([]byte(v), &item) + + if item.Id > 0 { + itemData = append(itemData, item) + } + } + + if len(itemData) > 0 { + result[exchange] = itemData + } + } + + return result +} + func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) { var apiUserInfo models.LineApiUser e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo) diff --git a/app/jobs/jobbase.go b/app/jobs/jobbase.go index 66e5afc..74c7007 100644 --- a/app/jobs/jobbase.go +++ b/app/jobs/jobbase.go @@ -3,6 +3,7 @@ package jobs import ( "fmt" models2 "go-admin/app/jobs/models" + "runtime" "time" log "github.com/go-admin-team/go-admin-core/logger" @@ -43,7 +44,10 @@ type ExecJob struct { func (e *ExecJob) Run() { defer func() { if err := recover(); err != nil { - log.Errorf("脚本任务失败:%v", err) + // 获取调用栈信息 + buf := make([]byte, 1<<16) // 64KB 缓冲区 + n := runtime.Stack(buf, false) + log.Errorf("脚本任务失败: %v\n%s", err, buf[:n]) } }() diff --git a/app/jobs/strategy_job_test.go b/app/jobs/strategy_job_test.go new file mode 100644 index 0000000..463acda --- /dev/null +++ b/app/jobs/strategy_job_test.go @@ -0,0 +1,22 @@ +package jobs + +import ( + "go-admin/common/helper" + "testing" + + "github.com/go-admin-team/go-admin-core/sdk" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestStrategyJob(t *testing.T) { + dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" + db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + sdk.Runtime.SetDb("default", db) + helper.InitDefaultRedis("127.0.0.1:6379", "", 2) + helper.InitLockRedisConn("127.0.0.1:6379", "", "2") + + job := StrategyJob{} + + job.Exec([]string{}) +} diff --git a/common/const/rediskey/redis_key.go b/common/const/rediskey/redis_key.go index dbd70f9..cdf35f2 100644 --- a/common/const/rediskey/redis_key.go +++ b/common/const/rediskey/redis_key.go @@ -46,9 +46,9 @@ const ( SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol} FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol} - //波段现货触发{apiuserid|symbol} + //波段现货触发{apiuserid|ordersn} StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s" - //波段合约触发{apiuserid|symbol} + //波段合约触发{apiuserid|ordersn} StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s" //减仓波段合约触发 {apiuserid|symbol} diff --git a/common/helper/redislock.go b/common/helper/redislock.go index 1716794..a2bef22 100644 --- a/common/helper/redislock.go +++ b/common/helper/redislock.go @@ -106,6 +106,9 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) { baseInterval = time.Second } + if baseInterval <= 0 { + baseInterval = time.Millisecond * 100 // 至少 100ms + } // 随机退避 retryInterval := time.Duration(rand.Int63n(int64(baseInterval))) // 随机退避 if retryInterval < time.Millisecond*100 { @@ -129,6 +132,13 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) { return false, ErrFailed } +func safeRandomDuration(max time.Duration) time.Duration { + if max <= 0 { + return 100 * time.Millisecond // fallback default + } + return time.Duration(rand.Int63n(int64(max))) +} + // Release 释放锁 func (rl *RedisLock) Release() (bool, error) { return rl.releaseCtx(context.Background()) diff --git a/services/binanceservice/strategy_order_service.go b/services/binanceservice/strategy_order_service.go index 7b61bdc..d45cf27 100644 --- a/services/binanceservice/strategy_order_service.go +++ b/services/binanceservice/strategy_order_service.go @@ -24,22 +24,29 @@ import ( type BinanceStrategyOrderService struct { service.Service + Debug bool } // 判断是否触发波段订单 func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) { //现货 - orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)) - e.DoJudge(orderStrs, 1, exchangeType) + orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType)) + if len(orderStrs) > 0 { + e.DoJudge(orderStrs, 1, exchangeType) + } //合约 futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)) - e.DoJudge(futOrdedrStrs, 2, exchangeType) + + if len(futOrdedrStrs) > 0 { + e.DoJudge(futOrdedrStrs, 2, exchangeType) + } } // 判断是否符合条件 func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) { db := GetDBConnection() + // setting, _ := cacheservice.GetSystemSetting(db) for _, orderStr := range orderStrs { var lockKey string @@ -56,7 +63,7 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int lockKey = rediskey.StrategyFutTriggerLock } - lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.Symbol), 200, 50, 100*time.Millisecond) + lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.OrderSn), 60, 20, 300*time.Millisecond) if ok, err := lock.AcquireWait(context.Background()); err != nil { e.Log.Debug("获取锁失败", err) @@ -65,13 +72,13 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int defer lock.Release() //判断是否符合条件 - success, err := e.JudgeStrategy(orderItem, 1, exchangeType) + success, err := e.JudgeStrategy(orderItem, symbolType, exchangeType) if err != nil { e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err) } - if success { + if e.Debug || success { e.TriggerOrder(db, orderStr, orderItem, symbolType) } } @@ -121,22 +128,26 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis } percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6) - fmt.Printf("百分比:%s", percentag.Mul(decimal.NewFromInt(100)).String()) + logger.Infof("百分比:%s", percentag.Mul(decimal.NewFromInt(100)).String()) //价格没有变动 if percentag.Cmp(decimal.Zero) == 0 { return result, nil } //满足条件 - switch order.CompareType { - case 1: - result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0 - case 2: - result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0 - case 5: - result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0 - default: - return result, errors.New("没有对应的类型") + switch { + //涨价格大于0.5% 跌价格小于-0.5% + case order.CompareType == 1 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0, + order.CompareType == 1 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0: + result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0 + + case order.CompareType == 2 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0, + order.CompareType == 2 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0: + result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0 + + case order.CompareType == 5 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0, + order.CompareType == 5 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0: + result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0 } return result, nil @@ -145,7 +156,7 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis // 触发委托单 func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string, order dto.StrategyOrderRedisList, symbolType int) error { orders := make([]models.LinePreOrder, 0) - if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =?", order.Id).Find(&orders).Error; err != nil { + if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =? or id =?", order.Id, order.Id).Find(&orders).Error; err != nil { e.Log.Errorf("order_id:%d 获取委托单失败:%s", order.Id, err.Error()) return err } @@ -156,7 +167,16 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string, return errors.New("获取系统设置失败") } - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, symbolType) + var tradeSet models2.TradeSet + + switch symbolType { + case 1: + tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0) + case 2: + tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) + default: + return errors.New("获取交易对行情失败,交易对类型错误") + } if tradeSet.Coin == "" { return errors.New("获取交易对行情失败") @@ -177,11 +197,36 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string, } } + if mainOrder.Id == 0 { + return errors.New("获取主单失败") + } + GetOrderByPid(&mainOrder, orders, mainOrder.Id) e.RecalculateOrder(tradeSet, &mainOrder, setting) - if err := e.Orm.Save(&mainOrder).Error; err != nil { + //事务保存 + err := e.Orm.Transaction(func(tx *gorm.DB) error { + if err1 := tx.Save(&mainOrder).Error; err1 != nil { + return err1 + } + + for _, v := range mainOrder.Childs { + if err1 := tx.Save(&v).Error; err1 != nil { + return err1 + } + + for _, v2 := range v.Childs { + if err1 := tx.Save(&v2).Error; err1 != nil { + return err1 + } + } + } + + return nil + }) + + if err != nil { e.Log.Errorf("order_id:%d 波段触发保存委托单失败:%s", mainOrder.Id, err.Error()) return err } @@ -224,16 +269,29 @@ func (e *BinanceStrategyOrderService) StrategyOrderPlace(db *gorm.DB, cacheVal s if err := futApi.OrderPlaceLoop(db, params, 3); err != nil { logger.Error("下单失败", mainOrder.Symbol, " err:", err) + if _, err := helper.DefaultRedis.LRem(key, cacheVal); err != nil { + logger.Error("删除redis 预下单失败:", err) + } + err := db.Model(&models.LinePreOrder{}).Where("id =? and status='0'", mainOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error if err != nil { logger.Error("更新预下单状态失败") } + return + } else { if _, err := helper.DefaultRedis.LRem(key, cacheVal); err != nil { logger.Error("删除redis 预下单失败:", err) } - return + } + + if err := db.Model(&models.LinePreOrder{}).Where("id =? ", mainOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil { + logger.Error("更新预下单状态失败 ordersn:", mainOrder.OrderSn, " status:1") + } + + if err := db.Model(&models.LinePreOrder{}).Where("id =? AND status ='0'", mainOrder.Id).Updates(map[string]interface{}{"status": "1"}).Error; err != nil { + logger.Error("更新预下单状态失败 ordersn:", mainOrder.OrderSn, " status:1") } } @@ -247,9 +305,10 @@ func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet return errors.New("获取拓展信息失败") } + var newPrice decimal.Decimal lastPrice := utility.StrToDecimal(tradeSet.LastPrice) rate := utility.StrToDecimal(mainOrder.Rate) - newPrice := lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit)) + newPrice = lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit)) buyPrice := utility.StrToDecimal(mainOrder.BuyPrice) totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit)) @@ -257,51 +316,60 @@ func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet mainOrder.Price = newPrice.String() mainOrder.Num = totalNum.String() remainQuantity := totalNum + var totalLossAmount decimal.Decimal + prePrice := lastPrice for index := range mainOrder.Childs { var ext models.LinePreOrderExt + extOrderId := mainOrder.Childs[index].Id + takeStopArray := []int{1, 2} + + //止盈止损 ext拓展id为 pid + if utility.ContainsInt(takeStopArray, mainOrder.Childs[index].OrderType) { + extOrderId = mainOrder.Childs[index].Pid + } + for _, v := range exts { - if v.OrderId == mainOrder.Child[index].Id { + if v.OrderId == extOrderId { ext = v break } } if ext.Id <= 0 { - logger.Errorf("子订单ext不存在 id:%d", mainOrder.Child[index].Id) + logger.Errorf("子订单ext不存在 id:%d", mainOrder.Childs[index].Id) continue } //主单止盈、止损 - if mainOrder.Child[index].Pid == mainOrder.Child[index].MainId && (mainOrder.Child[index].OrderType == 1 || mainOrder.Child[index].OrderType == 2) { + if mainOrder.Childs[index].Pid == mainOrder.Childs[index].MainId && (mainOrder.Childs[index].OrderType == 1 || mainOrder.Childs[index].OrderType == 2) { var percent decimal.Decimal switch { // 加价 - case mainOrder.Child[index].OrderType == 1 && mainOrder.Site == "BUY", mainOrder.Child[index].OrderType == 2 && mainOrder.Site == "SELL": + case mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "SELL": percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio) //减价 - case mainOrder.Child[index].OrderType == 2 && mainOrder.Site == "BUY", mainOrder.Child[index].OrderType == 1 && mainOrder.Site == "SELL": + case mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "SELL": percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio) } childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit)) - mainOrder.Child[index].Price = childPrice.String() - mainOrder.Child[index].Num = totalNum.String() + mainOrder.Childs[index].Price = childPrice.String() + mainOrder.Childs[index].Num = totalNum.String() } else { - //todo 重新计算 lastNum := remainQuantity //过期时间 if ext.ExpirateHour <= 0 { - mainOrder.Child[index].ExpireTime = time.Now().AddDate(10, 0, 0) + mainOrder.Childs[index].ExpireTime = time.Now().AddDate(10, 0, 0) } else { - mainOrder.Child[index].ExpireTime = time.Now().Add(time.Hour * time.Duration(ext.ExpirateHour)) + mainOrder.Childs[index].ExpireTime = time.Now().Add(time.Hour * time.Duration(ext.ExpirateHour)) } switch { //加仓单 - case mainOrder.Child[index].OrderType == 1 && mainOrder.Child[index].OrderCategory == 3: + case mainOrder.Childs[index].OrderType == 1 && mainOrder.Childs[index].OrderCategory == 3: var percentage decimal.Decimal if mainOrder.Site == "BUY" { @@ -311,24 +379,29 @@ func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet } dataPrice := utility.StrToDecimal(mainOrder.Price).Mul(percentage).Truncate(int32(tradeSet.PriceDigit)) - mainOrder.Child[index].Price = dataPrice.String() + mainOrder.Childs[index].Price = dataPrice.String() + + priceDiff := dataPrice.Sub(prePrice).Abs() + totalLossAmount = totalLossAmount.Add(lastNum.Mul(priceDiff)) if ext.AddPositionType == 1 { buyPrice := utility.StrToDecimal(mainOrder.BuyPrice).Mul(utility.SafeDiv(ext.AddPositionVal, decimal.NewFromInt(100))).Truncate(2) - mainOrder.Child[index].Num = utility.SafeDiv(buyPrice, dataPrice).Truncate(int32(tradeSet.AmountDigit)).String() + mainOrder.Childs[index].Num = utility.SafeDiv(buyPrice, dataPrice).Truncate(int32(tradeSet.AmountDigit)).String() } else { - mainOrder.Child[index].Num = utility.SafeDiv(ext.AddPositionVal.Truncate(2), dataPrice).Truncate(int32(tradeSet.AmountDigit)).String() + mainOrder.Childs[index].Num = utility.SafeDiv(ext.AddPositionVal.Truncate(2), dataPrice).Truncate(int32(tradeSet.AmountDigit)).String() } //加库存 - lastNum = lastNum.Add(utility.StrToDecimal(mainOrder.Child[index].Num)) + lastNum = lastNum.Add(utility.StrToDecimal(mainOrder.Childs[index].Num)) // 计算子订单 - if len(mainOrder.Child[index].Child) > 0 { - calculateChildOrder(&mainOrder.Child, &tradeSet, ext, lastNum, dataPrice, false) + if len(mainOrder.Childs[index].Childs) > 0 { + calculateChildOrder(&mainOrder.Childs[index].Childs, &tradeSet, ext, lastNum, dataPrice, totalLossAmount, false) } + //覆盖最近的订单价 + prePrice = dataPrice //减仓单 - case mainOrder.Child[index].OrderType == 4: + case mainOrder.Childs[index].OrderType == 4: percentage := decimal.NewFromInt(1) if mainOrder.Site == "BUY" && ext.PriceRatio.Cmp(decimal.Zero) > 0 { @@ -338,22 +411,26 @@ func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet } dataPrice := utility.StrToDecimal(mainOrder.Price).Mul(percentage).Truncate(int32(tradeSet.PriceDigit)) - mainOrder.Child[index].Price = dataPrice.String() + mainOrder.Childs[index].Price = dataPrice.String() + priceDiff := dataPrice.Sub(prePrice).Abs() + totalLossAmount = totalLossAmount.Add(lastNum.Mul(priceDiff)) //百分比减仓 if ext.AddPositionType == 1 { - mainOrder.Child[index].Num = lastNum.Mul(ext.AddPositionVal.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit)).String() + mainOrder.Childs[index].Num = lastNum.Mul(ext.AddPositionVal.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit)).String() } else { logger.Error("减仓不能是固定数值") } //减库存 - lastNum = lastNum.Sub(utility.StrToDecimal(mainOrder.Child[index].Num)) + lastNum = lastNum.Sub(utility.StrToDecimal(mainOrder.Childs[index].Num)) // 计算子订单 - if len(mainOrder.Child[index].Child) > 0 { - calculateChildOrder(&mainOrder.Child, &tradeSet, ext, lastNum, dataPrice, false) + if len(mainOrder.Childs[index].Childs) > 0 { + calculateChildOrder(&mainOrder.Childs[index].Childs, &tradeSet, ext, lastNum, dataPrice, totalLossAmount, false) } + //覆盖最近的订单价 + prePrice = dataPrice } } } @@ -362,7 +439,8 @@ func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet // 计算子订单信息 // isTpTp 是否是止盈后止损止盈 -func calculateChildOrder(orders *[]models.LinePreOrder, tradeSet *models2.TradeSet, ext models.LinePreOrderExt, lastNum decimal.Decimal, price decimal.Decimal, isTpTp bool) error { +// totalLossAmount 累计亏损金额 +func calculateChildOrder(orders *[]models.LinePreOrder, tradeSet *models2.TradeSet, ext models.LinePreOrderExt, lastNum decimal.Decimal, price decimal.Decimal, totalLossAmount decimal.Decimal, isTpTp bool) error { for index := range *orders { orderQuantity := lastNum.Truncate(int32(tradeSet.AmountDigit)) percentage := decimal.NewFromInt(1) @@ -389,11 +467,21 @@ func calculateChildOrder(orders *[]models.LinePreOrder, tradeSet *models2.TradeS switch { //做多止盈、做空止损 case (*orders)[index].OrderType == 1 && (*orders)[index].Site == "SELL", (*orders)[index].OrderType == 2 && (*orders)[index].Site == "BUY": - percentage = decimal.NewFromInt(100).Add(addPercentage).Div(decimal.NewFromInt(100)) + percentage = decimal.NewFromInt(100).Add(addPercentage) //做多止损、做空止盈 case (*orders)[index].OrderType == 2 && (*orders)[index].Site == "SELL", (*orders)[index].OrderType == 1 && (*orders)[index].Site == "BUY": - percentage = decimal.NewFromInt(100).Sub(addPercentage).Div(decimal.NewFromInt(100)) + percentage = decimal.NewFromInt(100).Sub(addPercentage) + } + + //止盈亏损回本百分比 + if (*orders)[index].OrderType == 1 && totalLossAmount.Cmp(decimal.Zero) > 0 { + lossPercent := totalLossAmount.Div(lastNum).Mul(decimal.NewFromInt(100)).Truncate(2) + percentage = percentage.Add(lossPercent) + } + + if percentage.Cmp(decimal.Zero) > 0 { + percentage = percentage.Div(decimal.NewFromInt(100)) } orderPrice := price.Mul(percentage).Truncate(int32(tradeSet.PriceDigit)) @@ -402,8 +490,8 @@ func calculateChildOrder(orders *[]models.LinePreOrder, tradeSet *models2.TradeS lastOrderQuantity := lastNum.Sub(orderQuantity).Truncate(int32(tradeSet.AmountDigit)) //止盈后止盈、止盈后止损 - if len((*orders)[index].Child) > 0 && lastOrderQuantity.Cmp(decimal.Zero) > 0 { - calculateChildOrder(&(*orders)[index].Child, tradeSet, ext, lastOrderQuantity, orderPrice, true) + if len((*orders)[index].Childs) > 0 && lastOrderQuantity.Cmp(decimal.Zero) > 0 { + calculateChildOrder(&(*orders)[index].Childs, tradeSet, ext, lastOrderQuantity, orderPrice, decimal.Zero, true) } } diff --git a/services/binanceservice/strategy_order_service_test.go b/services/binanceservice/strategy_order_service_test.go new file mode 100644 index 0000000..b271afe --- /dev/null +++ b/services/binanceservice/strategy_order_service_test.go @@ -0,0 +1,26 @@ +package binanceservice + +import ( + "go-admin/common/global" + "go-admin/common/helper" + "testing" + + "github.com/go-admin-team/go-admin-core/sdk" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// 测试策略 触发单 +func TestTriggerOrder(t *testing.T) { + service := BinanceStrategyOrderService{} + dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" + db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + helper.InitDefaultRedis("127.0.0.1:6379", "", 2) + helper.InitLockRedisConn("127.0.0.1:6379", "", "2") + sdk.Runtime.SetDb("default", db) + + service.Orm = db + service.Debug = true + + service.TriggerStrategyOrder(global.EXCHANGE_BINANCE) +} diff --git a/services/cacheservice/config_service.go b/services/cacheservice/config_service.go index ab80e4d..e0db3cc 100644 --- a/services/cacheservice/config_service.go +++ b/services/cacheservice/config_service.go @@ -66,9 +66,11 @@ func GetConfigCacheByKey(db *gorm.DB, key string) models.SysConfig { // 获取缓存交易对 // symbolType 0-现货 1-合约 func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) { + // 定义返回结果和val变量 result := models2.TradeSet{} val := "" + // 根据交易对类型选择不同的key switch symbolType { case 0: key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol) @@ -78,14 +80,17 @@ func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.Tr val, _ = helper.DefaultRedis.GetString(key) } + // 如果val不为空,则解析val为TradeSet结构体 if val != "" { if err := sonic.Unmarshal([]byte(val), &result); err != nil { return result, err } } else { + // 如果val为空,则返回错误信息 return result, errors.New("未找到交易对信息") } + // 返回结果 return result, nil }