126 lines
4.2 KiB
Go
126 lines
4.2 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"strconv"
|
||
|
||
"git.apinb.com/quant/coin/internal/models"
|
||
"github.com/adshao/go-binance/v2"
|
||
)
|
||
|
||
// trySpotInitialEntry 视为空仓时按配置 OrderQtyUsdt(USDT 名义)市价买入,数量按现价换算后按 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()
|
||
}
|