Files
coin/internal/logic/spot_binance_open.go
2026-05-08 23:37:34 +08:00

123 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
markSpotPortfolioDirty()
}