deving
This commit is contained in:
@@ -3,6 +3,7 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,7 +27,7 @@ const (
|
||||
// trailPullbackPct 从跟踪期内最高价回撤本比例则触发市价全仓卖出
|
||||
trailPullbackPct = 0.005
|
||||
|
||||
// minHoldUSDT 持仓市值低于此值(USDT)视为「无仓」,会按配置 OrderQty 市价买基础币建首笔
|
||||
// minHoldUSDT 持仓市值默认「无仓」判断的全局名义下限(USDT);单笔 OrderQty×价 接近该值时会用 spotHoldingThresholdUSDT 放宽,避免 LOT 取整后略低于本值被误判空仓。
|
||||
minHoldUSDT = 10.0
|
||||
// minSellNotional 单笔卖出名义价值下限,低于则不下单(贴近交易所 minNotional)
|
||||
minSellNotional = 10.0
|
||||
@@ -35,6 +36,10 @@ const (
|
||||
dipRecoverPct = 0.03
|
||||
// buyReboundPct 开仓/加仓前须相对阶段低点反弹超过本比例才市价买入(持续创新低则不买)
|
||||
buyReboundPct = 0.0039
|
||||
|
||||
// spotImmediateInitialOpen 为 true 时,仅当策略侧从未有过持仓(成本与数量均为 0)时跳过「先锚定再等反弹」,
|
||||
// 启动后首轮即可市价开首仓;曾经建仓后又全平的标的仍须等反弹后再进,避免刚卖立刻买回。
|
||||
spotImmediateInitialOpen = true
|
||||
)
|
||||
|
||||
// spotSymbol 单标的运行时视图:基础资产、交易对、每笔买入的基础币数量(来自配置 SpotWatchList)。
|
||||
@@ -79,8 +84,15 @@ var (
|
||||
portfolio = models.NewSpotPortfolioSnapshot()
|
||||
// stepSizes 各交易对 LOT_SIZE 的 stepSize,用于卖出数量按交易所步长向下取整
|
||||
stepSizes = map[string]string{}
|
||||
// spotTickDirty 本轮是否发生过市价成交(首仓/加仓/全平)并改动了需持久化的字段;无成交则不写库。
|
||||
// 无成交时的 trail/反弹锚点等仅驻内存,进程异常退出可能丢失该段进度(下轮仍可从账户与价格继续推演)。
|
||||
spotTickDirty bool
|
||||
)
|
||||
|
||||
func markSpotPortfolioDirty() {
|
||||
spotTickDirty = true
|
||||
}
|
||||
|
||||
// runBinanceSpotStrategy 入口:校验配置 → 连通性 → 死循环定时执行 spotTick。
|
||||
// 若 Key/Secret 为空或 API 失败会直接 return,不再占用协程(由 Boot 调用方决定是否在 goroutine 里跑)。
|
||||
func runBinanceSpotStrategy() {
|
||||
@@ -114,8 +126,12 @@ func runBinanceSpotStrategy() {
|
||||
if err := loadPortfolio(); err != nil {
|
||||
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 {
|
||||
log.Printf("logic: 加载交易对精度失败: %v", err)
|
||||
logSpotWatchConfigOnly()
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
@@ -156,10 +172,20 @@ func spotTick(ctx context.Context, client *binance.Client) error {
|
||||
}
|
||||
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()
|
||||
defer portfolioMu.Unlock()
|
||||
|
||||
spotTickDirty = false
|
||||
for _, w := range watch {
|
||||
px, ok := priceBySymbol[w.Symbol]
|
||||
if !ok {
|
||||
@@ -169,6 +195,9 @@ func spotTick(ctx context.Context, client *binance.Client) error {
|
||||
log.Printf("logic: %s 处理失败: %v", w.Symbol, err)
|
||||
}
|
||||
}
|
||||
if !spotTickDirty {
|
||||
return nil
|
||||
}
|
||||
return savePortfolioLocked()
|
||||
}
|
||||
|
||||
@@ -185,6 +214,24 @@ func getOrCreateState(base, symbol string) *models.SpotPosition {
|
||||
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 单标的一轮决策:无仓建仓 → 有仓则判超跌买 / 冲高卖,并维护锁与成本。
|
||||
func processOne(ctx context.Context, client *binance.Client, balances []binance.Balance, w spotSymbol, price float64) error {
|
||||
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)
|
||||
positionUSDT := free * price
|
||||
holdTh := spotHoldingThresholdUSDT(w, price)
|
||||
|
||||
if positionUSDT >= minHoldUSDT {
|
||||
if positionUSDT >= holdTh {
|
||||
st.OpenReboundLow = 0
|
||||
}
|
||||
|
||||
if positionUSDT < minHoldUSDT {
|
||||
if positionUSDT < holdTh {
|
||||
st.DipAddsDone = 0
|
||||
st.DipLegLocked = false
|
||||
st.RallyLegLocked = false
|
||||
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
|
||||
}
|
||||
err = trySpotInitialEntry(ctx, client, balances, w, st, price)
|
||||
|
||||
Reference in New Issue
Block a user