fix(trader+decision): prevent quantity=0 error with min notional checks

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())
This commit is contained in:
ZhouYongyou
2025-11-05 01:13:06 +08:00
parent 735db88ae5
commit 1cb5c268c5
2 changed files with 43 additions and 3 deletions
+21 -3
View File
@@ -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 USDTBinance 最小名义价值要求)", d.PositionSizeUSD, minPositionSizeGeneral)
}
}
// 验证仓位价值上限(加1%容差以避免浮点数精度问题)
tolerance := maxPositionValue * 0.01 // 1%容差
if d.PositionSizeUSD > maxPositionValue+tolerance {
+22
View File
@@ -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).