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] =?UTF-8?q?fix(trader+decision):=20prevent=20quantity=3D0?= =?UTF-8?q?=20error=20with=20min=20notional=20checks=20User=20encountered?= =?UTF-8?q?=20API=20error=20when=20opening=20BTC=20position:=20-=20Account?= =?UTF-8?q?=20equity:=209.20=20USDT=20-=20AI=20suggested:=20~7.36=20USDT?= =?UTF-8?q?=20position=20-=20Error:=20`code=3D-4003,=20msg=3DQuantity=20le?= =?UTF-8?q?ss=20than=20or=20equal=20to=20zero.`=20```=20quantity=20=3D=207?= =?UTF-8?q?.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).