Files
coin/internal/logic/boot.go
2026-05-10 23:23:38 +08:00

164 lines
4.8 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"
"errors"
"log"
"strings"
"sync"
"time"
"git.apinb.com/quant/coin/internal/config"
"git.apinb.com/quant/coin/internal/models"
"github.com/adshao/go-binance/v2"
)
var (
BinanceClient *binance.Client
Symbols []string
InvestUsdt map[string]float64 = make(map[string]float64)
SymbolInfos map[string]*binance.Symbol = make(map[string]*binance.Symbol)
accountMu sync.Mutex // 保护 account 与数据库写入,避免并发轮询(若以后拆协程)
account map[string]float64 = make(map[string]float64)
portfolioMu sync.Mutex // 保护 portfolio 与数据库写入,避免并发轮询(若以后拆协程)
portfolio = models.NewSpotPortfolioSnapshot()
)
// Boot 在独立协程中运行 Binance 现货轮询策略(反弹/布林开仓、分档加仓、跟踪止盈全平);内部为死循环不返回。
func Boot() {
// 检查参数
if err := CheckArgs(); err != nil {
log.Fatal("logic: 参数检查失败: " + err.Error())
}
// 读取交易对参数
if err := InitSymbolInfos(); err != nil {
log.Fatal("logic: 读取交易对参数失败: " + err.Error())
}
// 启动现货策略
runSpotStrategy()
}
func CheckArgs() error {
ctx := context.Background()
if len(config.Spec.SpotWatchList) == 0 {
return errors.New("SpotWatchList 未配置或无效,跳过现货策略")
}
key := config.Spec.BinanceApiKey
secret := config.Spec.BinanceApiSecret
if key == "" || secret == "" {
return errors.New("未配置 BinanceApiKey 或 BinanceApiSecret")
}
client := binance.NewClient(key, secret)
if err := client.NewPingService().Do(ctx); err != nil {
return errors.New("Binance Ping 失败: " + err.Error())
}
acct, err := client.NewGetAccountService().Do(ctx)
if err != nil {
return errors.New("Binance 账户校验失败: " + err.Error())
}
if !acct.CanTrade {
return errors.New("Binance 账户未开启现货交易权限")
}
if len(spotWatchesFromConfig()) == 0 {
return errors.New("SpotWatchList 未配置或无效,跳过现货策略")
}
log.Printf("logic: Binance 已连接CanTrade=%v", acct.CanTrade)
BinanceClient = client
return nil
}
// InitSymbolInfos 从 exchangeInfo 拉取规则,填充 SymbolInfos 与 stepSizes供下单数量取整
func InitSymbolInfos() error {
ctx := context.Background()
Symbols = nil
for _, item := range config.Spec.SpotWatchList {
s := strings.ToUpper(strings.TrimSpace(item.Symbol))
if s != "" {
Symbols = append(Symbols, s)
InvestUsdt[s] = item.OrderQtyUsdt
}
}
if len(Symbols) == 0 {
return errors.New("SpotWatchList 中无有效 Symbol")
}
info, err := BinanceClient.NewExchangeInfoService().Symbols(Symbols...).Do(ctx)
if err != nil {
return err
}
for i := range info.Symbols {
s := &info.Symbols[i]
SymbolInfos[s.Symbol] = s
if lot := s.LotSizeFilter(); lot != nil && lot.StepSize != "" {
stepSizes[s.Symbol] = lot.StepSize
}
var cfgUsdt float64
for _, it := range config.Spec.SpotWatchList {
if strings.ToUpper(strings.TrimSpace(it.Symbol)) == s.Symbol {
cfgUsdt = it.OrderQtyUsdt
break
}
}
minQty, step, marketMin, minNotional := "-", "-", "-", "-"
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 配置OrderQtyUsdt=%.2f USDT | LOT最小量=%s 步长=%s | 市价单最小基础量=%s | minNotional=%s",
s.Symbol, cfgUsdt, minQty, step, marketMin, minNotional)
}
return nil
}
func getSymbolInfo(symbol string) *binance.Symbol {
return SymbolInfos[symbol]
}
// runBinanceSpotStrategy 入口:校验配置 → 连通性 → 死循环定时执行 spotTick。
// 若 Key/Secret 为空或 API 失败会直接 return不再占用协程由 Boot 调用方决定是否在 goroutine 里跑)。
func runSpotStrategy() {
if err := RefreshAccount(); err != nil {
log.Printf("logic: 刷新账户失败: %v", err)
}
if err := loadPortfolio(); err != nil {
log.Printf("logic: 从数据库加载现货持仓失败(将使用空档): %v", err)
}
portfolio := GetPortfolio()
for _, item := range config.Spec.SpotWatchList {
base := spotBaseFromSymbol(strings.ToUpper(strings.TrimSpace(item.Symbol)))
st := portfolio[base]
if st == nil || (st.Quantity <= 0 && st.AvgCostUSDT <= 0) {
log.Printf("logic: %s 无有效持仓记录,尝试建仓", item.Symbol)
CreateNewSpotPosition(item.Symbol)
}
}
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for {
ctx := context.Background()
if err := Run(ctx); err != nil {
log.Printf("logic: 现货策略轮询错误: %v", err)
}
<-ticker.C
}
}