Files
coin/internal/logic/spot_binance_open.go

123 lines
3.9 KiB
Go
Raw Normal View History

2026-05-07 09:56:21 +08:00
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
2026-05-08 23:37:34 +08:00
markSpotPortfolioDirty()
2026-05-07 09:56:21 +08:00
}