From 1cb5c268c529cf98dcd244868bd368632e4f3256 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:13:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(trader+decision):=20prevent=20quantity?= =?UTF-8?q?=3D0=20error=20with=20min=20notional=20checks=20User=20encounte?= =?UTF-8?q?red=20API=20error=20when=20opening=20BTC=20position:=20-=20Acco?= =?UTF-8?q?unt=20equity:=209.20=20USDT=20-=20AI=20suggested:=20~7.36=20USD?= =?UTF-8?q?T=20position=20-=20Error:=20`code=3D-4003,=20msg=3DQuantity=20l?= =?UTF-8?q?ess=20than=20or=20equal=20to=20zero.`=20```=20quantity=20=3D=20?= =?UTF-8?q?7.36=20/=20101808.2=20=E2=89=88=200.00007228=20BTC=20formatted?= =?UTF-8?q?=20(%.3f)=20=E2=86=92=20"0.000"=20=E2=9D=8C=20Rounded=20down=20?= =?UTF-8?q?to=200!=20```=20BTCUSDT=20precision=20is=203=20decimals=20(step?= =?UTF-8?q?Size=3D0.001),=20causing=20small=20quantities=20to=20round=20to?= =?UTF-8?q?=200.=20-=20=E2=9C=85=20CloseLong()=20and=20CloseShort()=20have?= =?UTF-8?q?=20CheckMinNotional()=20-=20=E2=9D=8C=20OpenLong()=20and=20Open?= =?UTF-8?q?Short()=20**missing**=20CheckMinNotional()=20-=20AI=20could=20s?= =?UTF-8?q?uggest=20position=5Fsize=5Fusd=20<=20minimum=20notional=20value?= =?UTF-8?q?=20-=20No=20validation=20prevented=20tiny=20positions=20that=20?= =?UTF-8?q?would=20fail=20---=20**OpenLong()=20and=20OpenShort()**=20-=20A?= =?UTF-8?q?dded=20two=20checks:=20```go=20//=20=E2=9C=85=20Check=20if=20fo?= =?UTF-8?q?rmatted=20quantity=20became=200=20(rounding=20issue)=20quantity?= =?UTF-8?q?Float,=20=5F=20:=3D=20strconv.ParseFloat(quantityStr,=2064)=20i?= =?UTF-8?q?f=20quantityFloat=20<=3D=200=20{=20=20=20=20=20return=20error("?= =?UTF-8?q?Quantity=20too=20small,=20formatted=20to=200...")=20}=20//=20?= =?UTF-8?q?=E2=9C=85=20Check=20minimum=20notional=20value=20(Binance=20req?= =?UTF-8?q?uires=20=E2=89=A510=20USDT)=20if=20err=20:=3D=20t.CheckMinNotio?= =?UTF-8?q?nal(symbol,=20quantityFloat);=20err=20!=3D=20nil=20{=20=20=20?= =?UTF-8?q?=20=20return=20err=20}=20```=20**Impact**:=20Prevents=20API=20e?= =?UTF-8?q?rrors=20by=20catching=20invalid=20quantities=20before=20submiss?= =?UTF-8?q?ion.=20---=20Added=20minimum=20position=20size=20validation:=20?= =?UTF-8?q?```go=20const=20minPositionSizeGeneral=20=3D=2015.0=20=20=20//?= =?UTF-8?q?=20Altcoins=20const=20minPositionSizeBTCETH=20=3D=20100.0=20=20?= =?UTF-8?q?=20//=20BTC/ETH=20(high=20price=20+=20precision=20limits)=20if?= =?UTF-8?q?=20symbol=20=3D=3D=20BTC/ETH=20&&=20position=5Fsize=5Fusd=20数量)\n") - sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n", - accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage)) - sb.WriteString("4. 保证金: 总使用率 ≤ 90%\n\n") + sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n", + accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) + sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆** (⚠️ 严格执行,不可超过)\n", altcoinLeverage, btcEthLeverage)) + sb.WriteString("5. 保证金: 总使用率 ≤ 90%\n") + sb.WriteString("6. 最小开仓金额: **BTC/ETH ≥100 USDT | 山寨币 ≥15 USDT** (⚠️ 低于此金额会因精度问题导致开仓失败)\n\n") // 3. 输出格式 - 动态生成 sb.WriteString("#输出格式\n\n") @@ -532,6 +534,22 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi if d.PositionSizeUSD <= 0 { return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) } + + // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) + // Binance 最小名义价值 10 USDT + 安全边际 + const minPositionSizeGeneral = 15.0 + const minPositionSizeBTCETH = 100.0 // BTC/ETH 因价格高和精度限制需要更大金额 + + if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { + if d.PositionSizeUSD < minPositionSizeBTCETH { + return fmt.Errorf("%s 开仓金额过小(%.2f USDT),必须≥%.2f USDT(因价格高且精度限制,避免数量四舍五入为0)", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH) + } + } else { + if d.PositionSizeUSD < minPositionSizeGeneral { + return fmt.Errorf("开仓金额过小(%.2f USDT),必须≥%.2f USDT(Binance 最小名义价值要求)", d.PositionSizeUSD, minPositionSizeGeneral) + } + } + // 验证仓位价值上限(加1%容差以避免浮点数精度问题) tolerance := maxPositionValue * 0.01 // 1%容差 if d.PositionSizeUSD > maxPositionValue+tolerance { diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..833826d2 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -237,6 +237,17 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) return nil, err } + // ✅ 检查格式化后的数量是否为 0(防止四舍五入导致的错误) + quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64) + if parseErr != nil || quantityFloat <= 0 { + return nil, fmt.Errorf("开倉數量過小,格式化後為 0 (原始: %.8f → 格式化: %s)。建議增加開倉金額或選擇價格更低的幣種", quantity, quantityStr) + } + + // ✅ 检查最小名义价值(Binance 要求至少 10 USDT) + if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { + return nil, err + } + // 创建市价买入订单 order, err := t.client.NewCreateOrderService(). Symbol(symbol). @@ -280,6 +291,17 @@ func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) return nil, err } + // ✅ 检查格式化后的数量是否为 0(防止四舍五入导致的错误) + quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64) + if parseErr != nil || quantityFloat <= 0 { + return nil, fmt.Errorf("开倉數量過小,格式化後為 0 (原始: %.8f → 格式化: %s)。建議增加開倉金額或選擇價格更低的幣種", quantity, quantityStr) + } + + // ✅ 检查最小名义价值(Binance 要求至少 10 USDT) + if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { + return nil, err + } + // 创建市价卖出订单 order, err := t.client.NewCreateOrderService(). Symbol(symbol). From 7027d7a2e1f500853c758d99fc348fd50858ee76 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:16:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor(decision):=20relax=20minimum=20pos?= =?UTF-8?q?ition=20size=20constraints=20for=20flexibility=20##=20Changes?= =?UTF-8?q?=20###=20Prompt=20Layer=20(Soft=20Guidance)=20**Before**:=20-?= =?UTF-8?q?=20BTC/ETH=20=E2=89=A5100=20USDT=20|=20=E5=B1=B1=E5=AF=A8?= =?UTF-8?q?=E5=B8=81=20=E2=89=A515=20USDT=20(=E7=A1=AC=E6=80=A7=E8=A6=81?= =?UTF-8?q?=E6=B1=82)=20**After**:=20-=20=E7=BB=9F=E4=B8=80=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=20=E2=89=A512=20USDT=20(=E8=BD=AF=E6=80=A7=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE)=20-=20=E6=9B=B4=E7=AE=80=E6=B4=81=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E5=B8=81=E7=A7=8D=20-=20=E7=BB=99=20AI=20?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E5=86=B3=E7=AD=96=E7=A9=BA=E9=97=B4=20###=20?= =?UTF-8?q?Validation=20Layer=20(Lower=20Thresholds)=20**Before**:=20-=20B?= =?UTF-8?q?TC/ETH:=20100=20USDT=20(=E7=A1=AC=E6=80=A7)=20-=20=E5=B1=B1?= =?UTF-8?q?=E5=AF=A8=E5=B8=81:=2015=20USDT=20(=E7=A1=AC=E6=80=A7)=20**Afte?= =?UTF-8?q?r**:=20-=20BTC/ETH:=2060=20USDT=20(-40%,=20=E6=9B=B4=E7=81=B5?= =?UTF-8?q?=E6=B4=BB)=20-=20=E5=B1=B1=E5=AF=A8=E5=B8=81:=2012=20USDT=20(-2?= =?UTF-8?q?0%,=20=E6=9B=B4=E5=90=88=E7=90=86)=20##=20Rationale=20###=20Why?= =?UTF-8?q?=20Relax=3F=201.=20**Previous=20was=20too=20strict**:=20=20=20?= =?UTF-8?q?=20-=20100=20USDT=20for=20BTC=20hardcoded=20at=20current=20pric?= =?UTF-8?q?e=20(~101k)=20=20=20=20-=20If=20BTC=20drops=20to=2060k,=20only?= =?UTF-8?q?=20needs=2060=20USDT=20=20=20=20-=2015=20USDT=20for=20altcoins?= =?UTF-8?q?=20=3D=2050%=20safety=20margin=20(too=20conservative)=202.=20**?= =?UTF-8?q?Three-layer=20defense=20is=20sufficient**:=20=20=20=20-=20Layer?= =?UTF-8?q?=201=20(Prompt):=20Soft=20suggestion=20(=E2=89=A512=20USDT)=20?= =?UTF-8?q?=20=20=20-=20Layer=202=20(Validation):=20Medium=20threshold=20(?= =?UTF-8?q?BTC=2060=20/=20Alt=2012)=20=20=20=20-=20Layer=203=20(API):=20Fi?= =?UTF-8?q?nal=20check=20(quantity=20!=3D=200=20+=20CheckMinNotional)=203.?= =?UTF-8?q?=20**User=20feedback**:=20Original=20constraints=20too=20restri?= =?UTF-8?q?ctive=20###=20Safety=20Preserved=20=E2=9C=85=20API=20layer=20st?= =?UTF-8?q?ill=20prevents:=20-=20quantity=20=3D=200=20errors=20(formatted?= =?UTF-8?q?=20precision=20check)=20-=20Below=20min=20notional=20(CheckMinN?= =?UTF-8?q?otional)=20=E2=9C=85=20Validation=20still=20blocks=20obviously?= =?UTF-8?q?=20small=20amounts=20=E2=9C=85=20Prompt=20guides=20AI=20toward?= =?UTF-8?q?=20safe=20amounts=20##=20Testing=20|=20Symbol=20|=20Amount=20|?= =?UTF-8?q?=20Old=20|=20New=20|=20Result=20|=20|--------|--------|-----|--?= =?UTF-8?q?---|--------|=20|=20BTCUSDT=20|=2050=20USDT=20|=20=E2=9D=8C=20R?= =?UTF-8?q?ejected=20|=20=E2=9D=8C=20Rejected=20|=20=E2=9C=85=20Correct=20?= =?UTF-8?q?(too=20small)=20|=20|=20BTCUSDT=20|=2070=20USDT=20|=20=E2=9D=8C?= =?UTF-8?q?=20Rejected=20|=20=E2=9C=85=20Pass=20|=20=E2=9C=85=20More=20fle?= =?UTF-8?q?xible=20|=20|=20ADAUSDT=20|=2011=20USDT=20|=20=E2=9D=8C=20Rejec?= =?UTF-8?q?ted=20|=20=E2=9D=8C=20Rejected=20|=20=E2=9C=85=20Correct=20(too?= =?UTF-8?q?=20small)=20|=20|=20ADAUSDT=20|=2013=20USDT=20|=20=E2=9D=8C=20R?= =?UTF-8?q?ejected=20|=20=E2=9C=85=20Pass=20|=20=E2=9C=85=20More=20flexibl?= =?UTF-8?q?e=20|=20##=20Impact=20-=20=E2=9C=85=20More=20flexible=20for=20p?= =?UTF-8?q?rice=20fluctuations=20-=20=E2=9C=85=20Better=20user=20experienc?= =?UTF-8?q?e=20for=20small=20accounts=20-=20=E2=9C=85=20Still=20prevents?= =?UTF-8?q?=20API=20errors=20-=20=E2=9C=85=20AI=20has=20more=20decision=20?= =?UTF-8?q?space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index ebe49fff..17120d0d 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -268,7 +268,7 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆** (⚠️ 严格执行,不可超过)\n", altcoinLeverage, btcEthLeverage)) sb.WriteString("5. 保证金: 总使用率 ≤ 90%\n") - sb.WriteString("6. 最小开仓金额: **BTC/ETH ≥100 USDT | 山寨币 ≥15 USDT** (⚠️ 低于此金额会因精度问题导致开仓失败)\n\n") + sb.WriteString("6. 开仓金额: 建议 **≥12 USDT** (交易所最小名义价值 10 USDT + 安全边际)\n\n") // 3. 输出格式 - 动态生成 sb.WriteString("#输出格式\n\n") @@ -537,8 +537,8 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) // Binance 最小名义价值 10 USDT + 安全边际 - const minPositionSizeGeneral = 15.0 - const minPositionSizeBTCETH = 100.0 // BTC/ETH 因价格高和精度限制需要更大金额 + const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 + const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.PositionSizeUSD < minPositionSizeBTCETH { From aecca7fc8c70e3b915de8a900f539a99fdd5b1c8 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:20:02 +0800 Subject: [PATCH 3/3] fix(trader): add missing GetMinNotional and CheckMinNotional methods These methods are required by the OpenLong/OpenShort validation but were missing from upstream/dev. Adds: - GetMinNotional(): Returns minimum notional value (10 USDT default) - CheckMinNotional(): Validates order meets minimum notional requirement --- trader/binance_futures.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 833826d2..0698fd2d 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -550,6 +550,32 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti return nil } +// GetMinNotional 获取最小名义价值(Binance要求) +func (t *FuturesTrader) GetMinNotional(symbol string) float64 { + // 使用保守的默认值 10 USDT,确保订单能够通过交易所验证 + return 10.0 +} + +// CheckMinNotional 检查订单是否满足最小名义价值要求 +func (t *FuturesTrader) CheckMinNotional(symbol string, quantity float64) error { + price, err := t.GetMarketPrice(symbol) + if err != nil { + return fmt.Errorf("获取市价失败: %w", err) + } + + notionalValue := quantity * price + minNotional := t.GetMinNotional(symbol) + + if notionalValue < minNotional { + return fmt.Errorf( + "订单金额 %.2f USDT 低于最小要求 %.2f USDT (数量: %.4f, 价格: %.4f)", + notionalValue, minNotional, quantity, price, + ) + } + + return nil +} + // GetSymbolPrecision 获取交易对的数量精度 func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) { exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())