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

126 lines
4.2 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 视为空仓时按配置 OrderQtyUsdtUSDT 名义)市价买入,数量按现价换算后按 LOT_SIZE 向下取整。
func trySpotInitialEntry(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, st *models.SpotPosition, price float64) error {
wantUsdt := w.OrderQtyUsdt
order, qtyStr, err := executeSpotMarketBuy(ctx, client, balances, w, price)
if err != nil {
return err
}
applyBuyFill(st, order)
log.Printf("logic: %s 反弹≥%.2f%% 后初始建仓数量 %s (配置 %.2f USDT), 成交均价约 %.4f", w.Symbol, buyReboundPct*100, qtyStr, wantUsdt, st.AvgCostUSDT)
return nil
}
// trySpotDipAdd 相对当前均价达到第 (DipAddsDone+1) 档跌幅5%/15%/30%/50%)且未锁波段时,先等反弹 buyReboundPct 再按 OrderQtyUsdt 买入;最多加仓 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
}
wantUsdt := w.OrderQtyUsdt
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 (配置 %.2f USDT), 新成本约 %.4f", w.Symbol, st.DipAddsDone, draw*100, buyReboundPct*100, qtyStr, wantUsdt, 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, price)
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 将 OrderQtyUsdt 按现价换为基础币数量,再按 LOT_SIZE 向下取整为下单字符串want 为换算后的基础币数量(取整前)。
func spotBuyQtyString(w spotSymbol, price float64) (qtyStr string, want float64, err error) {
if price <= 0 {
return "", 0, fmt.Errorf("%s: 现价无效,无法由 OrderQtyUsdt 换算数量", w.Symbol)
}
want = w.OrderQtyUsdt / price
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: OrderQtyUsdt=%g USDT 按现价与 LOT_SIZE(%s) 取整后为 0请调大 OrderQtyUsdt≥%.0f)或检查交易对", w.Symbol, w.OrderQtyUsdt, step, minOrderQtyUsdt)
}
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()
}