From 194453477a9f758b750c123b251bd41226c421f2 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] fix(trader+decision): prevent quantity=0 error with min notional checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User encountered API error when opening BTC position: - Account equity: 9.20 USDT - AI suggested: ~7.36 USDT position - Error: `code=-4003, msg=Quantity less than or equal to zero.` ``` quantity = 7.36 / 101808.2 ≈ 0.00007228 BTC formatted (%.3f) → "0.000" ❌ Rounded down to 0! ``` BTCUSDT precision is 3 decimals (stepSize=0.001), causing small quantities to round to 0. - ✅ CloseLong() and CloseShort() have CheckMinNotional() - ❌ OpenLong() and OpenShort() **missing** CheckMinNotional() - AI could suggest position_size_usd < minimum notional value - No validation prevented tiny positions that would fail --- **OpenLong() and OpenShort()** - Added two checks: ```go // ✅ Check if formatted quantity became 0 (rounding issue) quantityFloat, _ := strconv.ParseFloat(quantityStr, 64) if quantityFloat <= 0 { return error("Quantity too small, formatted to 0...") } // ✅ Check minimum notional value (Binance requires ≥10 USDT) if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { return err } ``` **Impact**: Prevents API errors by catching invalid quantities before submission. --- Added minimum position size validation: ```go const minPositionSizeGeneral = 15.0 // Altcoins const minPositionSizeBTCETH = 100.0 // BTC/ETH (high price + precision limits) if symbol == BTC/ETH && position_size_usd < 100 { return error("BTC/ETH requires ≥100 USDT to avoid rounding to 0") } if position_size_usd < 15 { return error("Position size must be ≥15 USDT (min notional requirement)") } ``` **Impact**: Rejects invalid decisions before execution, saving API calls. --- Updated hard constraints in AI prompt: ``` 6. 最小开仓金额: **BTC/ETH ≥100 USDT | 山寨币 ≥15 USDT** (⚠️ 低于此金额会因精度问题导致开仓失败) ``` **Impact**: AI proactively avoids suggesting too-small positions. --- - ❌ User equity 9.20 USDT → suggested 7.36 USDT BTC position → **FAIL** - ❌ No validation, error only at API level - ✅ AI validation rejects position_size_usd < 100 for BTC - ✅ Binance trader checks quantity != 0 before submission - ✅ Clear error: "BTC/ETH requires ≥100 USDT..." | Symbol | position_size_usd | Price | quantity | Formatted | Result | |--------|-------------------|-------|----------|-----------|--------| | BTCUSDT | 7.36 | 101808.2 | 0.00007228 | "0.000" | ❌ Rejected (validation) | | BTCUSDT | 150 | 101808.2 | 0.00147 | "0.001" | ✅ Pass | | ADAUSDT | 15 | 1.2 | 12.5 | "12.500" | ✅ Pass | --- **Immediate**: - ✅ Prevents quantity=0 API errors - ✅ Clear error messages guide users - ✅ Saves wasted API calls **Long-term**: - ✅ AI learns minimum position sizes - ✅ Better user experience for small accounts - ✅ Prevents confusion from cryptic API errors --- - Diagnostic report: /tmp/quantity_zero_diagnosis.md - Binance min notional: 10 USDT (hardcoded in GetMinNotional()) --- decision/engine.go | 24 +++++++++++++++++++++--- trader/binance_futures.go | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index df48d534..ebe49fff 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -264,9 +264,11 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in sb.WriteString("# 硬约束(风险控制)\n\n") sb.WriteString("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\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 e17e7d90572b2b8f9dde9514360572c2e1b2864b 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] refactor(decision): relax minimum position size constraints for flexibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes ### Prompt Layer (Soft Guidance) **Before**: - BTC/ETH ≥100 USDT | 山寨币 ≥15 USDT (硬性要求) **After**: - 统一建议 ≥12 USDT (软性建议) - 更简洁,不区分币种 - 给 AI 更多决策空间 ### Validation Layer (Lower Thresholds) **Before**: - BTC/ETH: 100 USDT (硬性) - 山寨币: 15 USDT (硬性) **After**: - BTC/ETH: 60 USDT (-40%, 更灵活) - 山寨币: 12 USDT (-20%, 更合理) ## Rationale ### Why Relax? 1. **Previous was too strict**: - 100 USDT for BTC hardcoded at current price (~101k) - If BTC drops to 60k, only needs 60 USDT - 15 USDT for altcoins = 50% safety margin (too conservative) 2. **Three-layer defense is sufficient**: - Layer 1 (Prompt): Soft suggestion (≥12 USDT) - Layer 2 (Validation): Medium threshold (BTC 60 / Alt 12) - Layer 3 (API): Final check (quantity != 0 + CheckMinNotional) 3. **User feedback**: Original constraints too restrictive ### Safety Preserved ✅ API layer still prevents: - quantity = 0 errors (formatted precision check) - Below min notional (CheckMinNotional) ✅ Validation still blocks obviously small amounts ✅ Prompt guides AI toward safe amounts ## Testing | Symbol | Amount | Old | New | Result | |--------|--------|-----|-----|--------| | BTCUSDT | 50 USDT | ❌ Rejected | ❌ Rejected | ✅ Correct (too small) | | BTCUSDT | 70 USDT | ❌ Rejected | ✅ Pass | ✅ More flexible | | ADAUSDT | 11 USDT | ❌ Rejected | ❌ Rejected | ✅ Correct (too small) | | ADAUSDT | 13 USDT | ❌ Rejected | ✅ Pass | ✅ More flexible | ## Impact - ✅ More flexible for price fluctuations - ✅ Better user experience for small accounts - ✅ Still prevents API errors - ✅ AI has more decision space --- 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 dc92296c1557463a48389e2f79226129c835f29f 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())