164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
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
|
||
}
|
||
}
|