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 } if !ok { log.Printf("logic: %s 跟踪止盈回撤触发但可卖数量按 LOT 步长取整为 0(free=%.12f),解除跟踪;dust 请手工或下轮余额变化后再管", w.Symbol, free) resetSpotTrail(st) markSpotPortfolioDirty() return nil } nominal := parseFloat(qtyStr) * price if nominal < minSellNotional { log.Printf("logic: %s 跟踪止盈:卖出名义约 %.4f USDT 低于参考门槛 %.2f,仍尝试市价可卖尽卖", w.Symbol, nominal, minSellNotional) } 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) markSpotPortfolioDirty() 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 }