Files
coin/internal/logic/spot_binance_close.go

104 lines
2.8 KiB
Go
Raw Normal View History

2026-05-07 09:56:21 +08:00
package logic
import (
"context"
"log"
"strconv"
"git.apinb.com/quant/coin/internal/models"
"github.com/adshao/go-binance/v2"
"github.com/shopspring/decimal"
)
// trySpotRallySell 跟踪止盈浮盈≥profitArmPct 后记录阶段最高价,不在该涨幅直接平仓;
// 仅当现价从本轮高点回撤≥trailPullbackPct 时市价卖出全部可用基础资产。
func trySpotRallySell(ctx context.Context, client *binance.Client, w spotSymbol, price float64, st *models.SpotPosition, free float64) error {
cost := st.AvgCostUSDT
if cost <= 0 {
return nil
}
armLine := cost * (1 + profitArmPct)
if !st.TrailArmed {
if price >= armLine {
st.TrailArmed = true
st.TrailPeakUSDT = price
}
return nil
}
if price > st.TrailPeakUSDT {
st.TrailPeakUSDT = price
}
sellLine := st.TrailPeakUSDT * (1 - trailPullbackPct)
if price >= sellLine {
return nil
}
qtyStr, ok, err := formatQtyToLotStep(free, spotLotStep(w.Symbol))
if err != nil {
return err
}
2026-05-08 23:37:34 +08:00
if !ok {
log.Printf("logic: %s 跟踪止盈回撤触发但可卖数量按 LOT 步长取整为 0free=%.12f解除跟踪dust 请手工或下轮余额变化后再管", w.Symbol, free)
resetSpotTrail(st)
markSpotPortfolioDirty()
2026-05-07 09:56:21 +08:00
return nil
}
2026-05-08 23:37:34 +08:00
nominal := parseFloat(qtyStr) * price
if nominal < minSellNotional {
log.Printf("logic: %s 跟踪止盈:卖出名义约 %.4f USDT 低于参考门槛 %.2f,仍尝试市价可卖尽卖", w.Symbol, nominal, minSellNotional)
}
2026-05-07 09:56:21 +08:00
order, err := client.NewCreateOrderService().
Symbol(w.Symbol).
Side(binance.SideTypeSell).
Type(binance.OrderTypeMarket).
Quantity(qtyStr).
NewOrderRespType(binance.NewOrderRespTypeFULL).
Do(ctx)
if err != nil {
return err
}
sold, _ := strconv.ParseFloat(order.ExecutedQuantity, 64)
st.Quantity = free - sold
if st.Quantity < 0 {
st.Quantity = 0
}
peak := st.TrailPeakUSDT
resetSpotTrail(st)
2026-05-08 23:37:34 +08:00
markSpotPortfolioDirty()
2026-05-07 09:56:21 +08:00
log.Printf("logic: %s 从跟踪高点 %.6f 回撤≥%.1f%% 全平, 卖出数量 %s", w.Symbol, peak, trailPullbackPct*100, qtyStr)
return nil
}
// spotLotStep 返回交易对 LOT_SIZE 步长;未加载 exchangeInfo 时用保守默认。
func spotLotStep(symbol string) string {
if s := stepSizes[symbol]; s != "" {
return s
}
return "0.00001"
}
// formatQtyToLotStep 将数量按 stepSize 向下取整,满足 Binance LOT_SIZE买卖共用过小则返回 ok=false。
func formatQtyToLotStep(qty float64, stepSize string) (string, bool, error) {
if qty <= 0 {
return "", false, nil
}
step, err := decimal.NewFromString(stepSize)
if err != nil {
return "", false, err
}
q := decimal.NewFromFloat(qty)
n := q.Div(step).Floor()
out := n.Mul(step)
if out.LessThanOrEqual(decimal.Zero) {
return "", false, nil
}
return out.String(), true, nil
}
func parseFloat(s string) float64 {
v, _ := strconv.ParseFloat(s, 64)
return v
}