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
|
|
|
|
}
|