deving
This commit is contained in:
@@ -18,9 +18,7 @@ BinanceApiSecret: "YqTpRybnBWllS0fA1yk0T1MEx0RxRazc2bH2iZuPEI8QJKesUueq3saCDdDj7
|
|||||||
# 现货轮询标的:Symbol 为交易对,OrderQty 为每笔开仓/加仓买入的基础币数量(列表为空则不调现货策略)
|
# 现货轮询标的:Symbol 为交易对,OrderQty 为每笔开仓/加仓买入的基础币数量(列表为空则不调现货策略)
|
||||||
SpotWatchList:
|
SpotWatchList:
|
||||||
- Symbol: BTCUSDT
|
- Symbol: BTCUSDT
|
||||||
OrderQty: 0.001
|
OrderQty: 0.00001
|
||||||
- Symbol: ETHUSDT
|
|
||||||
OrderQty: 0.01
|
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
Log:
|
Log:
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ Cache: redis://null:Weidong2023~!@139.224.247.176:19379/1
|
|||||||
BinanceApiKey: "qZrLXzykvb7w7uKVJ45rXomp94BiJnk4a4tsaduAHKMYlljVcwWsZzOA3pYdGnqo"
|
BinanceApiKey: "qZrLXzykvb7w7uKVJ45rXomp94BiJnk4a4tsaduAHKMYlljVcwWsZzOA3pYdGnqo"
|
||||||
BinanceApiSecret: "YqTpRybnBWllS0fA1yk0T1MEx0RxRazc2bH2iZuPEI8QJKesUueq3saCDdDj7hpU"
|
BinanceApiSecret: "YqTpRybnBWllS0fA1yk0T1MEx0RxRazc2bH2iZuPEI8QJKesUueq3saCDdDj7hpU"
|
||||||
|
|
||||||
|
# 现货轮询标的:Symbol 为交易对,OrderQty 为每笔开仓/加仓买入的基础币数量(列表为空则不调现货策略)
|
||||||
SpotWatchList:
|
SpotWatchList:
|
||||||
- Symbol: BTCUSDT
|
- Symbol: BTCUSDT
|
||||||
OrderQty: 0.001
|
OrderQty: 0.0001
|
||||||
- Symbol: ETHUSDT
|
|
||||||
OrderQty: 0.01
|
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
Log:
|
Log:
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ Cache: redis://null:Weidong2023~!@139.224.247.176:19379/1
|
|||||||
BinanceApiKey: "qZrLXzykvb7w7uKVJ45rXomp94BiJnk4a4tsaduAHKMYlljVcwWsZzOA3pYdGnqo"
|
BinanceApiKey: "qZrLXzykvb7w7uKVJ45rXomp94BiJnk4a4tsaduAHKMYlljVcwWsZzOA3pYdGnqo"
|
||||||
BinanceApiSecret: "YqTpRybnBWllS0fA1yk0T1MEx0RxRazc2bH2iZuPEI8QJKesUueq3saCDdDj7hpU"
|
BinanceApiSecret: "YqTpRybnBWllS0fA1yk0T1MEx0RxRazc2bH2iZuPEI8QJKesUueq3saCDdDj7hpU"
|
||||||
|
|
||||||
|
# 现货轮询标的:Symbol 为交易对,OrderQty 为每笔开仓/加仓买入的基础币数量(列表为空则不调现货策略)
|
||||||
SpotWatchList:
|
SpotWatchList:
|
||||||
- Symbol: BTCUSDT
|
- Symbol: BTCUSDT
|
||||||
OrderQty: 0.001
|
OrderQty: 0.00001
|
||||||
- Symbol: ETHUSDT
|
|
||||||
OrderQty: 0.01
|
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
Log:
|
Log:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package logic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,7 +27,7 @@ const (
|
|||||||
// trailPullbackPct 从跟踪期内最高价回撤本比例则触发市价全仓卖出
|
// trailPullbackPct 从跟踪期内最高价回撤本比例则触发市价全仓卖出
|
||||||
trailPullbackPct = 0.005
|
trailPullbackPct = 0.005
|
||||||
|
|
||||||
// minHoldUSDT 持仓市值低于此值(USDT)视为「无仓」,会按配置 OrderQty 市价买基础币建首笔
|
// minHoldUSDT 持仓市值默认「无仓」判断的全局名义下限(USDT);单笔 OrderQty×价 接近该值时会用 spotHoldingThresholdUSDT 放宽,避免 LOT 取整后略低于本值被误判空仓。
|
||||||
minHoldUSDT = 10.0
|
minHoldUSDT = 10.0
|
||||||
// minSellNotional 单笔卖出名义价值下限,低于则不下单(贴近交易所 minNotional)
|
// minSellNotional 单笔卖出名义价值下限,低于则不下单(贴近交易所 minNotional)
|
||||||
minSellNotional = 10.0
|
minSellNotional = 10.0
|
||||||
@@ -35,6 +36,10 @@ const (
|
|||||||
dipRecoverPct = 0.03
|
dipRecoverPct = 0.03
|
||||||
// buyReboundPct 开仓/加仓前须相对阶段低点反弹超过本比例才市价买入(持续创新低则不买)
|
// buyReboundPct 开仓/加仓前须相对阶段低点反弹超过本比例才市价买入(持续创新低则不买)
|
||||||
buyReboundPct = 0.0039
|
buyReboundPct = 0.0039
|
||||||
|
|
||||||
|
// spotImmediateInitialOpen 为 true 时,仅当策略侧从未有过持仓(成本与数量均为 0)时跳过「先锚定再等反弹」,
|
||||||
|
// 启动后首轮即可市价开首仓;曾经建仓后又全平的标的仍须等反弹后再进,避免刚卖立刻买回。
|
||||||
|
spotImmediateInitialOpen = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// spotSymbol 单标的运行时视图:基础资产、交易对、每笔买入的基础币数量(来自配置 SpotWatchList)。
|
// spotSymbol 单标的运行时视图:基础资产、交易对、每笔买入的基础币数量(来自配置 SpotWatchList)。
|
||||||
@@ -79,8 +84,15 @@ var (
|
|||||||
portfolio = models.NewSpotPortfolioSnapshot()
|
portfolio = models.NewSpotPortfolioSnapshot()
|
||||||
// stepSizes 各交易对 LOT_SIZE 的 stepSize,用于卖出数量按交易所步长向下取整
|
// stepSizes 各交易对 LOT_SIZE 的 stepSize,用于卖出数量按交易所步长向下取整
|
||||||
stepSizes = map[string]string{}
|
stepSizes = map[string]string{}
|
||||||
|
// spotTickDirty 本轮是否发生过市价成交(首仓/加仓/全平)并改动了需持久化的字段;无成交则不写库。
|
||||||
|
// 无成交时的 trail/反弹锚点等仅驻内存,进程异常退出可能丢失该段进度(下轮仍可从账户与价格继续推演)。
|
||||||
|
spotTickDirty bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func markSpotPortfolioDirty() {
|
||||||
|
spotTickDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
// runBinanceSpotStrategy 入口:校验配置 → 连通性 → 死循环定时执行 spotTick。
|
// runBinanceSpotStrategy 入口:校验配置 → 连通性 → 死循环定时执行 spotTick。
|
||||||
// 若 Key/Secret 为空或 API 失败会直接 return,不再占用协程(由 Boot 调用方决定是否在 goroutine 里跑)。
|
// 若 Key/Secret 为空或 API 失败会直接 return,不再占用协程(由 Boot 调用方决定是否在 goroutine 里跑)。
|
||||||
func runBinanceSpotStrategy() {
|
func runBinanceSpotStrategy() {
|
||||||
@@ -114,8 +126,12 @@ func runBinanceSpotStrategy() {
|
|||||||
if err := loadPortfolio(); err != nil {
|
if err := loadPortfolio(); err != nil {
|
||||||
log.Printf("logic: 从数据库加载现货持仓失败(将使用空档): %v", err)
|
log.Printf("logic: 从数据库加载现货持仓失败(将使用空档): %v", err)
|
||||||
}
|
}
|
||||||
|
if err := startupSyncSpotHoldingFromBinance(ctx, client, acct.Balances); err != nil {
|
||||||
|
log.Printf("logic: 启动时与交易所同步持仓失败: %v", err)
|
||||||
|
}
|
||||||
if err := refreshStepSizes(ctx, client); err != nil {
|
if err := refreshStepSizes(ctx, client); err != nil {
|
||||||
log.Printf("logic: 加载交易对精度失败: %v", err)
|
log.Printf("logic: 加载交易对精度失败: %v", err)
|
||||||
|
logSpotWatchConfigOnly()
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(pollInterval)
|
ticker := time.NewTicker(pollInterval)
|
||||||
@@ -156,10 +172,20 @@ func spotTick(ctx context.Context, client *binance.Client) error {
|
|||||||
}
|
}
|
||||||
priceBySymbol[p.Symbol] = v
|
priceBySymbol[p.Symbol] = v
|
||||||
}
|
}
|
||||||
|
parts := make([]string, 0, len(watch))
|
||||||
|
for _, w := range watch {
|
||||||
|
if px, ok := priceBySymbol[w.Symbol]; ok {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s=%.8f", w.Symbol, px))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, w.Symbol+"=?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("logic: 现货轮询现价 %s", strings.Join(parts, ", "))
|
||||||
|
|
||||||
portfolioMu.Lock()
|
portfolioMu.Lock()
|
||||||
defer portfolioMu.Unlock()
|
defer portfolioMu.Unlock()
|
||||||
|
|
||||||
|
spotTickDirty = false
|
||||||
for _, w := range watch {
|
for _, w := range watch {
|
||||||
px, ok := priceBySymbol[w.Symbol]
|
px, ok := priceBySymbol[w.Symbol]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -169,6 +195,9 @@ func spotTick(ctx context.Context, client *binance.Client) error {
|
|||||||
log.Printf("logic: %s 处理失败: %v", w.Symbol, err)
|
log.Printf("logic: %s 处理失败: %v", w.Symbol, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !spotTickDirty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return savePortfolioLocked()
|
return savePortfolioLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +214,24 @@ func getOrCreateState(base, symbol string) *models.SpotPosition {
|
|||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spotHoldingThresholdUSDT 判断是否视为「有仓」的名义市值下限(USDT)。
|
||||||
|
// 若仅用固定 minHoldUSDT,而配置 OrderQty×现价 与 10U 接近,LOT 向下取整后实际成交量略小(如 0.0001→0.0000999),
|
||||||
|
// 名义可能略低于 10U,会被误判为空仓并重复首仓;对在全局门槛附近的单笔配置名义降低阈值并留松弛。
|
||||||
|
func spotHoldingThresholdUSDT(w spotSymbol, price float64) float64 {
|
||||||
|
t := minHoldUSDT
|
||||||
|
nominal := w.OrderQty * price
|
||||||
|
if nominal >= minHoldUSDT*0.85 && nominal <= minHoldUSDT*1.25 {
|
||||||
|
relaxed := nominal * 0.965
|
||||||
|
if relaxed < t {
|
||||||
|
t = relaxed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t < 0.3 {
|
||||||
|
t = 0.3
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
// processOne 单标的一轮决策:无仓建仓 → 有仓则判超跌买 / 冲高卖,并维护锁与成本。
|
// processOne 单标的一轮决策:无仓建仓 → 有仓则判超跌买 / 冲高卖,并维护锁与成本。
|
||||||
func processOne(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, price float64) error {
|
func processOne(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, price float64) error {
|
||||||
free, err := balanceFree(balances, w.Base)
|
free, err := balanceFree(balances, w.Base)
|
||||||
@@ -193,17 +240,21 @@ func processOne(ctx context.Context, client *binance.Client, balances []binance.
|
|||||||
}
|
}
|
||||||
st := getOrCreateState(w.Base, w.Symbol)
|
st := getOrCreateState(w.Base, w.Symbol)
|
||||||
positionUSDT := free * price
|
positionUSDT := free * price
|
||||||
|
holdTh := spotHoldingThresholdUSDT(w, price)
|
||||||
|
|
||||||
if positionUSDT >= minHoldUSDT {
|
if positionUSDT >= holdTh {
|
||||||
st.OpenReboundLow = 0
|
st.OpenReboundLow = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if positionUSDT < minHoldUSDT {
|
if positionUSDT < holdTh {
|
||||||
st.DipAddsDone = 0
|
st.DipAddsDone = 0
|
||||||
st.DipLegLocked = false
|
st.DipLegLocked = false
|
||||||
st.RallyLegLocked = false
|
st.RallyLegLocked = false
|
||||||
resetSpotTrail(st)
|
resetSpotTrail(st)
|
||||||
if !spotReboundReady(&st.OpenReboundLow, price) {
|
neverEntered := st.AvgCostUSDT <= 0 && st.Quantity <= 0
|
||||||
|
ready := (spotImmediateInitialOpen && neverEntered) || spotReboundReady(&st.OpenReboundLow, price)
|
||||||
|
if !ready {
|
||||||
|
log.Printf("logic: %s 空仓 现价=%.8f 阶段低=%.8f 待反弹≥%.3f%% 才首仓 (持仓≈%.2f USDT)", w.Symbol, price, st.OpenReboundLow, buyReboundPct*100, positionUSDT)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err = trySpotInitialEntry(ctx, client, balances, w, st, price)
|
err = trySpotInitialEntry(ctx, client, balances, w, st, price)
|
||||||
|
|||||||
@@ -2,14 +2,24 @@ package logic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.apinb.com/quant/coin/internal/impl"
|
"git.apinb.com/quant/coin/internal/impl"
|
||||||
"git.apinb.com/quant/coin/internal/models"
|
"git.apinb.com/quant/coin/internal/models"
|
||||||
"github.com/adshao/go-binance/v2"
|
"github.com/adshao/go-binance/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// refreshStepSizes 从 exchangeInfo 拉取各 symbol 的 LOT_SIZE.stepSize,供卖出数量格式化。
|
// logSpotWatchConfigOnly 在 exchangeInfo 拉取失败时仍打印 yaml 中的标的与 OrderQty。
|
||||||
|
func logSpotWatchConfigOnly() {
|
||||||
|
for _, w := range spotWatchesFromConfig() {
|
||||||
|
log.Printf("logic: 现货标的(仅配置,未取到交易所规则) %s 每笔OrderQty=%.8f", w.Symbol, w.OrderQty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshStepSizes 从 exchangeInfo 拉取各 symbol 的 LOT_SIZE.stepSize,供卖出数量格式化;
|
||||||
|
// 成功后按标的打印配置 OrderQty 与交易所最小量、步长、最小名义等。
|
||||||
func refreshStepSizes(ctx context.Context, client *binance.Client) error {
|
func refreshStepSizes(ctx context.Context, client *binance.Client) error {
|
||||||
watch := spotWatchesFromConfig()
|
watch := spotWatchesFromConfig()
|
||||||
syms := make([]string, 0, len(watch))
|
syms := make([]string, 0, len(watch))
|
||||||
@@ -20,12 +30,42 @@ func refreshStepSizes(ctx context.Context, client *binance.Client) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, s := range info.Symbols {
|
bySymbol := make(map[string]binance.Symbol, len(info.Symbols))
|
||||||
|
for i := range info.Symbols {
|
||||||
|
s := info.Symbols[i]
|
||||||
|
bySymbol[s.Symbol] = s
|
||||||
lot := s.LotSizeFilter()
|
lot := s.LotSizeFilter()
|
||||||
if lot == nil || lot.StepSize == "" {
|
if lot != nil && lot.StepSize != "" {
|
||||||
|
stepSizes[s.Symbol] = lot.StepSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("logic: 现货 SpotWatchList 与交易所最小交易量(exchangeInfo)")
|
||||||
|
for _, w := range watch {
|
||||||
|
minQty, step := "-", "-"
|
||||||
|
marketMin := "-"
|
||||||
|
minNotional := "-"
|
||||||
|
s, ok := bySymbol[w.Symbol]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("logic: %s 配置OrderQty=%.8f | 交易所未返回该交易对规则", w.Symbol, w.OrderQty)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stepSizes[s.Symbol] = lot.StepSize
|
if lot := s.LotSizeFilter(); lot != nil {
|
||||||
|
if lot.MinQuantity != "" {
|
||||||
|
minQty = lot.MinQuantity
|
||||||
|
}
|
||||||
|
if lot.StepSize != "" {
|
||||||
|
step = lot.StepSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mls := s.MarketLotSizeFilter(); mls != nil && mls.MinQuantity != "" {
|
||||||
|
marketMin = mls.MinQuantity
|
||||||
|
}
|
||||||
|
if nf := s.NotionalFilter(); nf != nil && nf.MinNotional != "" {
|
||||||
|
minNotional = nf.MinNotional
|
||||||
|
}
|
||||||
|
log.Printf("logic: %s 配置OrderQty=%.8f | LOT最小量=%s 步长=%s | 市价单最小基础量=%s | minNotional=%s",
|
||||||
|
w.Symbol, w.OrderQty, minQty, step, marketMin, minNotional)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -77,3 +117,93 @@ func balanceFree(balances []binance.Balance, asset string) (float64, error) {
|
|||||||
}
|
}
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// balanceLocked 解析某资产下单冻结数量(Locked)。
|
||||||
|
func balanceLocked(balances []binance.Balance, asset string) float64 {
|
||||||
|
for _, b := range balances {
|
||||||
|
if b.Asset == asset {
|
||||||
|
v, err := strconv.ParseFloat(b.Locked, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// startupSyncSpotHoldingFromBinance 启动时用已拉取的账户余额与 ListPrices 现价,按 SpotWatchList 将实际可用持仓与空仓/有仓状态对齐到内存并写库(与 processOne 一致以 Free 为可交易数量)。
|
||||||
|
func startupSyncSpotHoldingFromBinance(ctx context.Context, client *binance.Client, balances []binance.Balance) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 45*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
watch := spotWatchesFromConfig()
|
||||||
|
if len(watch) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
symbols := make([]string, len(watch))
|
||||||
|
for i, w := range watch {
|
||||||
|
symbols[i] = w.Symbol
|
||||||
|
}
|
||||||
|
prices, err := client.NewListPricesService().Symbols(symbols).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
priceBySymbol := make(map[string]float64, len(prices))
|
||||||
|
for _, p := range prices {
|
||||||
|
v, err := strconv.ParseFloat(p.Price, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
priceBySymbol[p.Symbol] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
portfolioMu.Lock()
|
||||||
|
defer portfolioMu.Unlock()
|
||||||
|
|
||||||
|
for _, w := range watch {
|
||||||
|
px, ok := priceBySymbol[w.Symbol]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("logic: 启动同步持仓 %s 未取到现价,跳过该标的", w.Symbol)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st := getOrCreateState(w.Base, w.Symbol)
|
||||||
|
st.Symbol = w.Symbol
|
||||||
|
|
||||||
|
free, err := balanceFree(balances, w.Base)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
locked := balanceLocked(balances, w.Base)
|
||||||
|
st.Quantity = free
|
||||||
|
|
||||||
|
positionUSDT := free * px
|
||||||
|
holdTh := spotHoldingThresholdUSDT(w, px)
|
||||||
|
if positionUSDT < holdTh {
|
||||||
|
st.DipAddsDone = 0
|
||||||
|
st.DipLegLocked = false
|
||||||
|
st.RallyLegLocked = false
|
||||||
|
resetSpotTrail(st)
|
||||||
|
st.OpenReboundLow = 0
|
||||||
|
st.DipReboundLow = 0
|
||||||
|
st.AvgCostUSDT = 0
|
||||||
|
log.Printf("logic: 启动持仓同步 %s 可用=%.8f 冻结=%.8f 名义≈%.4f USDT(<%.4f USDT 视为空仓)已对齐库", w.Symbol, free, locked, positionUSDT, holdTh)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
st.OpenReboundLow = 0
|
||||||
|
if st.AvgCostUSDT <= 0 {
|
||||||
|
st.AvgCostUSDT = px
|
||||||
|
}
|
||||||
|
log.Printf("logic: 启动持仓同步 %s 可用=%.8f 冻结=%.8f 名义≈%.4f USDT(≥%.4f USDT 有仓)数量与成本已对齐库", w.Symbol, free, locked, positionUSDT, holdTh)
|
||||||
|
}
|
||||||
|
|
||||||
|
if impl.DBService == nil {
|
||||||
|
log.Printf("logic: 启动持仓同步完成(未配置数据库,仅内存)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := savePortfolioLocked(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("logic: 启动持仓同步已写入数据库")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,9 +39,16 @@ func trySpotRallySell(ctx context.Context, client *binance.Client, w spotSymbol,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !ok || parseFloat(qtyStr)*price < minSellNotional {
|
if !ok {
|
||||||
|
log.Printf("logic: %s 跟踪止盈回撤触发但可卖数量按 LOT 步长取整为 0(free=%.12f),解除跟踪;dust 请手工或下轮余额变化后再管", w.Symbol, free)
|
||||||
|
resetSpotTrail(st)
|
||||||
|
markSpotPortfolioDirty()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
nominal := parseFloat(qtyStr) * price
|
||||||
|
if nominal < minSellNotional {
|
||||||
|
log.Printf("logic: %s 跟踪止盈:卖出名义约 %.4f USDT 低于参考门槛 %.2f,仍尝试市价可卖尽卖", w.Symbol, nominal, minSellNotional)
|
||||||
|
}
|
||||||
order, err := client.NewCreateOrderService().
|
order, err := client.NewCreateOrderService().
|
||||||
Symbol(w.Symbol).
|
Symbol(w.Symbol).
|
||||||
Side(binance.SideTypeSell).
|
Side(binance.SideTypeSell).
|
||||||
@@ -59,6 +66,7 @@ func trySpotRallySell(ctx context.Context, client *binance.Client, w spotSymbol,
|
|||||||
}
|
}
|
||||||
peak := st.TrailPeakUSDT
|
peak := st.TrailPeakUSDT
|
||||||
resetSpotTrail(st)
|
resetSpotTrail(st)
|
||||||
|
markSpotPortfolioDirty()
|
||||||
log.Printf("logic: %s 从跟踪高点 %.6f 回撤≥%.1f%% 全平, 卖出数量 %s", w.Symbol, peak, trailPullbackPct*100, qtyStr)
|
log.Printf("logic: %s 从跟踪高点 %.6f 回撤≥%.1f%% 全平, 卖出数量 %s", w.Symbol, peak, trailPullbackPct*100, qtyStr)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,4 +118,5 @@ func applyBuyFill(st *models.SpotPosition, order *binance.CreateOrderResponse) {
|
|||||||
st.AvgCostUSDT = (oldQty*oldCost + quote) / newQty
|
st.AvgCostUSDT = (oldQty*oldCost + quote) / newQty
|
||||||
}
|
}
|
||||||
st.Quantity = newQty
|
st.Quantity = newQty
|
||||||
|
markSpotPortfolioDirty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ GOARCH=amd64 GOOS=linux go build -o ../builds/coin ./cmd/main/main.go
|
|||||||
|
|
||||||
BSM_RuntimeMode=prod BSM_Prefix=/data/app/ nohup ./coin > /data/app/logs/coin.log 2>&1 &
|
BSM_RuntimeMode=prod BSM_Prefix=/data/app/ nohup ./coin > /data/app/logs/coin.log 2>&1 &
|
||||||
cat /data/app/logs/coin.log
|
cat /data/app/logs/coin.log
|
||||||
|
|
||||||
|
BSM_RuntimeMode=prod BSM_Prefix=/data/app/ ./coin
|
||||||
Reference in New Issue
Block a user