Files
coin/internal/logic/spot_binance_account.go
2026-05-08 23:37:34 +08:00

210 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"log"
"strconv"
"time"
"git.apinb.com/quant/coin/internal/impl"
"git.apinb.com/quant/coin/internal/models"
"github.com/adshao/go-binance/v2"
)
// 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 {
watch := spotWatchesFromConfig()
syms := make([]string, 0, len(watch))
for _, w := range watch {
syms = append(syms, w.Symbol)
}
info, err := client.NewExchangeInfoService().Symbols(syms...).Do(ctx)
if err != nil {
return err
}
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()
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
}
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
}
// loadPortfolio 从 GORM 读入全表 spot_positions填充内存 map键为 BaseAsset
func loadPortfolio() error {
portfolioMu.Lock()
defer portfolioMu.Unlock()
if impl.DBService == nil {
portfolio = models.NewSpotPortfolioSnapshot()
return nil
}
var rows []models.SpotPosition
if err := impl.DBService.Find(&rows).Error; err != nil {
return err
}
portfolio = models.NewSpotPortfolioSnapshot()
for i := range rows {
p := new(models.SpotPosition)
*p = rows[i]
portfolio.Positions[p.BaseAsset] = p
}
return nil
}
// savePortfolioLocked 将内存中各 SpotPosition 以 Save 写回数据库(有主键则更新,无则插入)。
// 调用方必须已持有 portfolioMu。
func savePortfolioLocked() error {
if impl.DBService == nil {
return nil
}
for _, st := range portfolio.Positions {
if st == nil {
continue
}
if err := impl.DBService.Save(st).Error; err != nil {
return err
}
}
return nil
}
// balanceFree 从账户余额列表里解析某资产的可用数量Free 字段为字符串)。
func balanceFree(balances []binance.Balance, asset string) (float64, error) {
for _, b := range balances {
if b.Asset == asset {
return strconv.ParseFloat(b.Free, 64)
}
}
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
}