mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 16:26:57 +08:00
Merge pull request #462 from zhouyongyou/fix/quantity-zero-min-notional
fix(trader+decision): prevent quantity=0 error with minimum notional validation
This commit is contained in:
@@ -309,9 +309,11 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in
|
|||||||
sb.WriteString("# 硬约束(风险控制)\n\n")
|
sb.WriteString("# 硬约束(风险控制)\n\n")
|
||||||
sb.WriteString("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n")
|
sb.WriteString("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n")
|
||||||
sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\n")
|
sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\n")
|
||||||
sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n",
|
sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n",
|
||||||
accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage))
|
accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10))
|
||||||
sb.WriteString("4. 保证金: 总使用率 ≤ 90%\n\n")
|
sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆** (⚠️ 严格执行,不可超过)\n", altcoinLeverage, btcEthLeverage))
|
||||||
|
sb.WriteString("5. 保证金: 总使用率 ≤ 90%\n")
|
||||||
|
sb.WriteString("6. 开仓金额: 建议 **≥12 USDT** (交易所最小名义价值 10 USDT + 安全边际)\n\n")
|
||||||
|
|
||||||
// 3. 输出格式 - 动态生成
|
// 3. 输出格式 - 动态生成
|
||||||
sb.WriteString("#输出格式\n\n")
|
sb.WriteString("#输出格式\n\n")
|
||||||
@@ -670,6 +672,22 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
|
|||||||
if d.PositionSizeUSD <= 0 {
|
if d.PositionSizeUSD <= 0 {
|
||||||
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
|
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 验证最小开仓金额(防止数量格式化为 0 的错误)
|
||||||
|
// Binance 最小名义价值 10 USDT + 安全边际
|
||||||
|
const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际
|
||||||
|
const minPositionSizeBTCETH = 60.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%容差以避免浮点数精度问题)
|
// 验证仓位价值上限(加1%容差以避免浮点数精度问题)
|
||||||
tolerance := maxPositionValue * 0.01 // 1%容差
|
tolerance := maxPositionValue * 0.01 // 1%容差
|
||||||
if d.PositionSizeUSD > maxPositionValue+tolerance {
|
if d.PositionSizeUSD > maxPositionValue+tolerance {
|
||||||
|
|||||||
@@ -279,6 +279,17 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int)
|
|||||||
return nil, err
|
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().
|
order, err := t.client.NewCreateOrderService().
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
@@ -322,6 +333,17 @@ func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int)
|
|||||||
return nil, err
|
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().
|
order, err := t.client.NewCreateOrderService().
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
@@ -748,6 +770,32 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti
|
|||||||
return nil
|
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 获取交易对的数量精度
|
// GetSymbolPrecision 获取交易对的数量精度
|
||||||
func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
|
func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
|
||||||
exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())
|
exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())
|
||||||
|
|||||||
Reference in New Issue
Block a user