package logic import ( "context" "fmt" "log" "strconv" "git.apinb.com/quant/coin/internal/models" "github.com/adshao/go-binance/v2" ) // trySpotInitialEntry 视为空仓时按配置 OrderQty(基础币)市价买入,数量按 LOT_SIZE 向下取整。 func trySpotInitialEntry(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, st *models.SpotPosition, price float64) error { want := w.OrderQty order, qtyStr, err := executeSpotMarketBuy(ctx, client, balances, w, price) if err != nil { return err } applyBuyFill(st, order) log.Printf("logic: %s 反弹≥%.2f%% 后初始建仓数量 %s (配置 %.8f), 成交均价约 %.4f", w.Symbol, buyReboundPct*100, qtyStr, want, st.AvgCostUSDT) return nil } // trySpotDipAdd 相对当前均价达到第 (DipAddsDone+1) 档跌幅(5%/15%/30%/50%)且未锁波段时,先等反弹 buyReboundPct 再按 OrderQty 买入;最多加仓 maxDipAdds 次。 func trySpotDipAdd(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, price float64, st *models.SpotPosition) error { if st.DipAddsDone >= maxDipAdds { return nil } cost := st.AvgCostUSDT if cost <= 0 { return nil } draw := dipAddDrawdowns[st.DipAddsDone] dipLine := cost * (1 - draw) if price > dipLine { st.DipReboundLow = 0 return nil } if st.DipLegLocked { return nil } if !spotReboundReady(&st.DipReboundLow, price) { return nil } want := w.OrderQty order, qtyStr, err := executeSpotMarketBuy(ctx, client, balances, w, price) if err != nil { return err } applyBuyFill(st, order) st.DipAddsDone++ st.DipLegLocked = true st.DipReboundLow = 0 resetSpotTrail(st) log.Printf("logic: %s 第 %d 次超跌(跌幅%.0f%%)反弹≥%.2f%% 后加仓数量 %s (配置 %.8f), 新成本约 %.4f", w.Symbol, st.DipAddsDone, draw*100, buyReboundPct*100, qtyStr, want, st.AvgCostUSDT) return nil } // executeSpotMarketBuy 校验 USDT 后下市价买单(FULL),返回成交回报与已取整数量字符串。 func executeSpotMarketBuy(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, price float64) (*binance.CreateOrderResponse, string, error) { qtyStr, _, err := spotBuyQtyString(w) if err != nil { return nil, "", err } usdtFree, err := balanceFree(balances, "USDT") if err != nil { return nil, "", err } est := parseFloat(qtyStr) * price if usdtFree < est { return nil, "", fmt.Errorf("USDT 余额不足,约需 %.2f USDT(数量 %s × 价 %.6f)", est, qtyStr, price) } order, err := client.NewCreateOrderService(). Symbol(w.Symbol). Side(binance.SideTypeBuy). Type(binance.OrderTypeMarket). Quantity(qtyStr). NewOrderRespType(binance.NewOrderRespTypeFULL). Do(ctx) if err != nil { return nil, "", err } return order, qtyStr, nil } // spotBuyQtyString 将配置的 OrderQty 按交易对 LOT_SIZE 向下取整为下单字符串;want 为配置原值。 func spotBuyQtyString(w spotSymbol) (qtyStr string, want float64, err error) { want = w.OrderQty step := spotLotStep(w.Symbol) ok := false qtyStr, ok, err = formatQtyToLotStep(want, step) if err != nil { return "", want, err } if !ok { return "", want, fmt.Errorf("%s: OrderQty=%g 按 LOT_SIZE(%s) 取整后为 0,请调大数量或检查交易对", w.Symbol, want, step) } return qtyStr, want, nil } // applyBuyFill 根据市价买单成交回报更新加权成本与数量(首笔建仓与加仓共用)。 func applyBuyFill(st *models.SpotPosition, order *binance.CreateOrderResponse) { execQty, _ := strconv.ParseFloat(order.ExecutedQuantity, 64) quote, _ := strconv.ParseFloat(order.CummulativeQuoteQuantity, 64) if execQty <= 0 || quote <= 0 { return } oldQty := st.Quantity oldCost := st.AvgCostUSDT newQty := oldQty + execQty if newQty <= 0 { return } if oldQty <= 0 || oldCost <= 0 { st.AvgCostUSDT = quote / execQty } else { st.AvgCostUSDT = (oldQty*oldCost + quote) / newQty } st.Quantity = newQty }