From 6a66913194f5b42417eaa9ae5778f8eb9bf193a5 Mon Sep 17 00:00:00 2001 From: 0xYYBB | ZYY | Bobo <128128010+the-dev-z@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:47:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(decision):=20=E6=B7=BB=E5=8A=A0=E6=A7=93?= =?UTF-8?q?=E6=A1=BF=E8=B6=85=E9=99=90=20fallback=20=E6=A9=9F=E5=88=B6?= =?UTF-8?q?=E4=B8=A6=E6=BE=84=E6=B8=85=E7=9B=88=E8=99=A7=E8=A8=88=E7=AE=97?= =?UTF-8?q?=E8=AA=AA=E6=98=8E=20(#716)=20*=20fix(decision):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=A7=93=E6=A1=BF=E8=B6=85=E9=99=90=20fallback=20?= =?UTF-8?q?=E6=A9=9F=E5=88=B6=E4=B8=A6=E6=BE=84=E6=B8=85=E7=9B=88=E8=99=A7?= =?UTF-8?q?=E8=A8=88=E7=AE=97=E8=AA=AA=E6=98=8E=201.=20AI=20=E6=B1=BA?= =?UTF-8?q?=E7=AD=96=E8=BC=B8=E5=87=BA=E8=B6=85=E9=99=90=E6=A7=93=E6=A1=BF?= =?UTF-8?q?=E6=99=82=EF=BC=88=E5=A6=82=2020x=EF=BC=89=EF=BC=8C=E9=A9=97?= =?UTF-8?q?=E8=AD=89=E7=9B=B4=E6=8E=A5=E6=8B=92=E7=B5=95=E5=B0=8E=E8=87=B4?= =?UTF-8?q?=E6=95=B4=E5=80=8B=E4=BA=A4=E6=98=93=E9=80=B1=E6=9C=9F=E5=A4=B1?= =?UTF-8?q?=E6=95=97=202.=20Prompt=20=E6=9C=AA=E6=98=8E=E7=A2=BA=E8=AA=AA?= =?UTF-8?q?=E6=98=8E=E7=9B=88=E8=99=A7=E7=99=BE=E5=88=86=E6=AF=94=E5=B7=B2?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E6=A7=93=E6=A1=BF=E6=95=88=E6=87=89=EF=BC=8C?= =?UTF-8?q?=E5=B0=8E=E8=87=B4=20AI=20=E6=80=9D=E7=B6=AD=E9=8F=88=E4=B8=AD?= =?UTF-8?q?=E8=AA=A4=E7=94=A8=E5=83=B9=E6=A0=BC=E8=AE=8A=E5=8B=95%=20-=20*?= =?UTF-8?q?*Before**:=20=E8=B6=85=E9=99=90=E7=9B=B4=E6=8E=A5=E5=A0=B1?= =?UTF-8?q?=E9=8C=AF=20=E2=86=92=20=E6=B1=BA=E7=AD=96=E5=A4=B1=E6=95=97=20?= =?UTF-8?q?-=20**After**:=20=E8=87=AA=E5=8B=95=E9=99=8D=E7=B4=9A=E7=82=BA?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8A=E9=99=90=20=E2=86=92=20=E6=B1=BA?= =?UTF-8?q?=E7=AD=96=E7=B9=BC=E7=BA=8C=E5=9F=B7=E8=A1=8C=20-=20**=E6=95=88?= =?UTF-8?q?=E6=9E=9C**:=20SOLUSDT=2020x=20=E2=86=92=20=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=82=BA=205x=EF=BC=88=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8A=E9=99=90=EF=BC=89=20-=20=E6=98=8E=E7=A2=BA=E5=91=8A?= =?UTF-8?q?=E7=9F=A5=20AI=EF=BC=9A=E7=B3=BB=E7=B5=B1=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E7=9A=84=E3=80=8C=E7=9B=88=E8=99=A7%=E3=80=8D=E5=B7=B2?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E6=A7=93=E6=A1=BF=E6=95=88=E6=87=89=20-=20?= =?UTF-8?q?=E5=85=AC=E5=BC=8F:=20=E7=9B=88=E8=99=A7%=20=3D=20(=E6=9C=AA?= =?UTF-8?q?=E5=AF=A6=E7=8F=BE=E7=9B=88=E8=99=A7=20/=20=E4=BF=9D=E8=AD=89?= =?UTF-8?q?=E9=87=91)=20=C3=97=20100=20-=20=E7=A4=BA=E4=BE=8B:=205x=20?= =?UTF-8?q?=E6=A7=93=E6=A1=BF=EF=BC=8C=E5=83=B9=E6=A0=BC=E6=BC=B2=202%=20?= =?UTF-8?q?=3D=20=E5=AF=A6=E9=9A=9B=E7=9B=88=E5=88=A9=2010%=20-=20?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=E5=B1=B1=E5=AF=A8=E5=B9=A3=E8=B6=85=E9=99=90?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=8820x=20=E2=86=92=205x=EF=BC=89=20-=20?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=20BTC/ETH=20=E8=B6=85=E9=99=90=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=EF=BC=8820x=20=E2=86=92=2010x=EF=BC=89=20-=20?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=E6=AD=A3=E5=B8=B8=E7=AF=84=E5=9C=8D=E4=B8=8D?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20-=20=E6=B8=AC=E8=A9=A6=E7=84=A1=E6=95=88?= =?UTF-8?q?=E6=A7=93=E6=A1=BF=E6=8B=92=E7=B5=95=20```=20PASS:=20TestLevera?= =?UTF-8?q?geFallback/=E5=B1=B1=E5=AF=A8=E5=B8=81=E6=9D=A0=E6=9D=86?= =?UTF-8?q?=E8=B6=85=E9=99=90=5F=E8=87=AA=E5=8A=A8=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E4=B8=BA=E4=B8=8A=E9=99=90=20PASS:=20TestLeverageFallback/BTC?= =?UTF-8?q?=E6=9D=A0=E6=9D=86=E8=B6=85=E9=99=90=5F=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=BA=E4=B8=8A=E9=99=90=20PASS:=20TestLev?= =?UTF-8?q?erageFallback/=E6=9D=A0=E6=9D=86=E5=9C=A8=E4=B8=8A=E9=99=90?= =?UTF-8?q?=E5=86=85=5F=E4=B8=8D=E4=BF=AE=E6=AD=A3=20PASS:=20TestLeverageF?= =?UTF-8?q?allback/=E6=9D=A0=E6=9D=86=E4=B8=BA0=5F=E5=BA=94=E8=AF=A5?= =?UTF-8?q?=E6=8A=A5=E9=94=99=20```=20-=20=E2=9C=85=20=E5=90=91=E5=BE=8C?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=EF=BC=9A=E6=AD=A3=E5=B8=B8=E6=A7=93=E6=A1=BF?= =?UTF-8?q?=E7=AF=84=E5=9C=8D=E4=B8=8D=E5=8F=97=E5=BD=B1=E9=9F=BF=20-=20?= =?UTF-8?q?=E2=9C=85=20=E5=AE=B9=E9=8C=AF=E6=80=A7=E5=A2=9E=E5=BC=B7?= =?UTF-8?q?=EF=BC=9AAI=20=E8=BC=B8=E5=87=BA=E8=B6=85=E9=99=90=E6=99=82?= =?UTF-8?q?=E7=B3=BB=E7=B5=B1=E8=87=AA=E5=8B=95=E4=BF=AE=E6=AD=A3=20-=20?= =?UTF-8?q?=E2=9C=85=20=E6=B1=BA=E7=AD=96=E8=B3=AA=E9=87=8F=E6=8F=90?= =?UTF-8?q?=E5=8D=87=EF=BC=9AAI=20=E5=B0=8D=E6=A7=93=E6=A1=BF=E6=94=B6?= =?UTF-8?q?=E7=9B=8A=E6=9C=89=E6=AD=A3=E7=A2=BA=E8=AA=8D=E7=9F=A5=20Co-Aut?= =?UTF-8?q?hored-By:=20tinkle-community=20=20*=20sty?= =?UTF-8?q?le:=20apply=20go=20fmt=20after=20rebase=20Co-Authored-By:=20tin?= =?UTF-8?q?kle-community=20=20---------=20Co-authore?= =?UTF-8?q?d-by:=20ZhouYongyou=20<128128010+zhouyongyou@users.noreply.gith?= =?UTF-8?q?ub.com>=20Co-authored-by:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 10 +++- decision/validate_test.go | 100 ++++++++++++++++++++++++++++++++++++++ manager/trader_manager.go | 1 + 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 decision/validate_test.go diff --git a/decision/engine.go b/decision/engine.go index 56633b58..076a2a13 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -731,8 +731,14 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值 } - if d.Leverage <= 0 || d.Leverage > maxLeverage { - return fmt.Errorf("杠杆必须在1-%d之间(%s,当前配置上限%d倍): %d", maxLeverage, d.Symbol, maxLeverage, d.Leverage) + // ✅ Fallback 机制:杠杆超限时自动修正为上限值(而不是直接拒绝决策) + if d.Leverage <= 0 { + return fmt.Errorf("杠杆必须大于0: %d", d.Leverage) + } + if d.Leverage > maxLeverage { + log.Printf("⚠️ [Leverage Fallback] %s 杠杆超限 (%dx > %dx),自动调整为上限值 %dx", + d.Symbol, d.Leverage, maxLeverage, maxLeverage) + d.Leverage = maxLeverage // 自动修正为上限值 } if d.PositionSizeUSD <= 0 { return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) diff --git a/decision/validate_test.go b/decision/validate_test.go new file mode 100644 index 00000000..faac4fe5 --- /dev/null +++ b/decision/validate_test.go @@ -0,0 +1,100 @@ +package decision + +import ( + "testing" +) + +// TestLeverageFallback 测试杠杆超限时的自动修正功能 +func TestLeverageFallback(t *testing.T) { + tests := []struct { + name string + decision Decision + accountEquity float64 + btcEthLeverage int + altcoinLeverage int + wantLeverage int // 期望修正后的杠杆值 + wantError bool + }{ + { + name: "山寨币杠杆超限_自动修正为上限", + decision: Decision{ + Symbol: "SOLUSDT", + Action: "open_long", + Leverage: 20, // 超过上限 + PositionSizeUSD: 100, + StopLoss: 50, + TakeProfit: 200, + }, + accountEquity: 100, + btcEthLeverage: 10, + altcoinLeverage: 5, // 上限 5x + wantLeverage: 5, // 应该修正为 5 + wantError: false, + }, + { + name: "BTC杠杆超限_自动修正为上限", + decision: Decision{ + Symbol: "BTCUSDT", + Action: "open_long", + Leverage: 20, // 超过上限 + PositionSizeUSD: 1000, + StopLoss: 90000, + TakeProfit: 110000, + }, + accountEquity: 100, + btcEthLeverage: 10, // 上限 10x + altcoinLeverage: 5, + wantLeverage: 10, // 应该修正为 10 + wantError: false, + }, + { + name: "杠杆在上限内_不修正", + decision: Decision{ + Symbol: "ETHUSDT", + Action: "open_short", + Leverage: 5, // 未超限 + PositionSizeUSD: 500, + StopLoss: 4000, + TakeProfit: 3000, + }, + accountEquity: 100, + btcEthLeverage: 10, + altcoinLeverage: 5, + wantLeverage: 5, // 保持不变 + wantError: false, + }, + { + name: "杠杆为0_应该报错", + decision: Decision{ + Symbol: "SOLUSDT", + Action: "open_long", + Leverage: 0, // 无效 + PositionSizeUSD: 100, + StopLoss: 50, + TakeProfit: 200, + }, + accountEquity: 100, + btcEthLeverage: 10, + altcoinLeverage: 5, + wantLeverage: 0, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDecision(&tt.decision, tt.accountEquity, tt.btcEthLeverage, tt.altcoinLeverage) + + // 检查错误状态 + if (err != nil) != tt.wantError { + t.Errorf("validateDecision() error = %v, wantError %v", err, tt.wantError) + return + } + + // 如果不应该报错,检查杠杆是否被正确修正 + if !tt.wantError && tt.decision.Leverage != tt.wantLeverage { + t.Errorf("Leverage not corrected: got %d, want %d", tt.decision.Leverage, tt.wantLeverage) + } + }) + } +} diff --git a/manager/trader_manager.go b/manager/trader_manager.go index f3ead124..9e1e9fb7 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -850,6 +850,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin // - database: 数据库实例 // - userID: 用户ID // - traderID: 交易员ID +// // 返回: // - error: 如果交易员不存在、配置无效或加载失败则返回错误 func (tm *TraderManager) LoadTraderByID(database *config.Database, userID, traderID string) error {