Initial commit: NOFX AI Trading System

- Multi-AI competition mode (Qwen vs DeepSeek)
- Binance Futures integration
- AI self-learning mechanism
- Professional web dashboard
- Complete risk management system
This commit is contained in:
tinkle-community
2025-10-28 15:47:34 +08:00
parent 976d6d74f6
commit 8d3981fd3e
12369 changed files with 1739210 additions and 0 deletions

945
trader/auto_trader.go Normal file
View File

@@ -0,0 +1,945 @@
package trader
import (
"encoding/json"
"fmt"
"log"
"nofx/logger"
"nofx/market"
"nofx/pool"
"strings"
"time"
)
// AutoTraderConfig 自动交易配置(简化版 - AI全权决策
type AutoTraderConfig struct {
// Trader标识
ID string // Trader唯一标识用于日志目录等
Name string // Trader显示名称
AIModel string // AI模型: "qwen" 或 "deepseek"
// API配置
BinanceAPIKey string
BinanceSecretKey string
CoinPoolAPIURL string
// AI配置
UseQwen bool
DeepSeekKey string
QwenKey string
// 扫描配置
ScanInterval time.Duration // 扫描间隔建议3分钟
// 账户配置
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
// 风险控制仅作为提示AI可自主决定
MaxDailyLoss float64 // 最大日亏损百分比(提示)
MaxDrawdown float64 // 最大回撤百分比(提示)
StopTradingTime time.Duration // 触发风控后暂停时长
}
// AutoTrader 自动交易器
type AutoTrader struct {
id string // Trader唯一标识
name string // Trader显示名称
aiModel string // AI模型名称
config AutoTraderConfig
trader *FuturesTrader
decisionLogger *logger.DecisionLogger // 决策日志记录器
initialBalance float64
dailyPnL float64
lastResetTime time.Time
stopUntil time.Time
isRunning bool
startTime time.Time // 系统启动时间
callCount int // AI调用次数
}
// NewAutoTrader 创建自动交易器
func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
// 设置默认值
if config.ID == "" {
config.ID = "default_trader"
}
if config.Name == "" {
config.Name = "Default Trader"
}
if config.AIModel == "" {
if config.UseQwen {
config.AIModel = "qwen"
} else {
config.AIModel = "deepseek"
}
}
// 初始化AI
if config.UseQwen {
market.SetQwenAPIKey(config.QwenKey, "")
log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name)
} else {
market.SetDeepSeekAPIKey(config.DeepSeekKey)
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
}
// 初始化币种池API
if config.CoinPoolAPIURL != "" {
pool.SetCoinPoolAPI(config.CoinPoolAPIURL)
}
// 初始化币安合约交易器
trader := NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
// 验证初始金额配置
if config.InitialBalance <= 0 {
return nil, fmt.Errorf("初始金额必须大于0请在配置中设置InitialBalance")
}
// 初始化决策日志记录器使用trader ID创建独立目录
logDir := fmt.Sprintf("decision_logs/%s", config.ID)
decisionLogger := logger.NewDecisionLogger(logDir)
return &AutoTrader{
id: config.ID,
name: config.Name,
aiModel: config.AIModel,
config: config,
trader: trader,
decisionLogger: decisionLogger,
initialBalance: config.InitialBalance,
lastResetTime: time.Now(),
startTime: time.Now(),
callCount: 0,
isRunning: false,
}, nil
}
// Run 运行自动交易主循环
func (at *AutoTrader) Run() error {
at.isRunning = true
log.Println("🚀 AI驱动自动交易系统启动")
log.Printf("💰 初始余额: %.2f USDT", at.initialBalance)
log.Printf("⚙️ 扫描间隔: %v", at.config.ScanInterval)
log.Println("🤖 AI将全权决定杠杆、仓位大小、止损止盈等参数")
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
// 首次立即执行
if err := at.runCycle(); err != nil {
log.Printf("❌ 执行失败: %v", err)
}
for at.isRunning {
select {
case <-ticker.C:
if err := at.runCycle(); err != nil {
log.Printf("❌ 执行失败: %v", err)
}
}
}
return nil
}
// Stop 停止自动交易
func (at *AutoTrader) Stop() {
at.isRunning = false
log.Println("⏹ 自动交易系统停止")
}
// runCycle 运行一个交易周期使用AI全权决策
func (at *AutoTrader) runCycle() error {
at.callCount++
log.Printf("\n" + strings.Repeat("=", 70))
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
log.Printf(strings.Repeat("=", 70))
// 创建决策记录
record := &logger.DecisionRecord{
ExecutionLog: []string{},
Success: true,
}
// 1. 检查是否需要停止交易
if time.Now().Before(at.stopUntil) {
remaining := at.stopUntil.Sub(time.Now())
log.Printf("⏸ 风险控制:暂停交易中,剩余 %.0f 分钟", remaining.Minutes())
record.Success = false
record.ErrorMessage = fmt.Sprintf("风险控制暂停中,剩余 %.0f 分钟", remaining.Minutes())
at.decisionLogger.LogDecision(record)
return nil
}
// 2. 重置日盈亏(每天重置)
if time.Since(at.lastResetTime) > 24*time.Hour {
at.dailyPnL = 0
at.lastResetTime = time.Now()
log.Println("📅 日盈亏已重置")
}
// 3. 收集交易上下文
ctx, err := at.buildTradingContext()
if err != nil {
record.Success = false
record.ErrorMessage = fmt.Sprintf("构建交易上下文失败: %v", err)
at.decisionLogger.LogDecision(record)
return fmt.Errorf("构建交易上下文失败: %w", err)
}
// 保存账户状态快照
record.AccountState = logger.AccountSnapshot{
TotalBalance: ctx.Account.TotalEquity,
AvailableBalance: ctx.Account.AvailableBalance,
TotalUnrealizedProfit: ctx.Account.TotalPnL,
PositionCount: ctx.Account.PositionCount,
MarginUsedPct: ctx.Account.MarginUsedPct,
}
// 保存持仓快照
for _, pos := range ctx.Positions {
record.Positions = append(record.Positions, logger.PositionSnapshot{
Symbol: pos.Symbol,
Side: pos.Side,
PositionAmt: pos.Quantity,
EntryPrice: pos.EntryPrice,
MarkPrice: pos.MarkPrice,
UnrealizedProfit: pos.UnrealizedPnL,
Leverage: float64(pos.Leverage),
LiquidationPrice: pos.LiquidationPrice,
})
}
// 保存候选币种列表
for _, coin := range ctx.CandidateCoins {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
}
log.Printf("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d",
ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount)
// 4. 调用AI获取完整决策
log.Println("🤖 正在请求AI分析并决策...")
decision, err := market.GetFullTradingDecision(ctx)
// 即使有错误也保存思维链和决策用于debug
if decision != nil {
record.CoTTrace = decision.CoTTrace
if len(decision.Decisions) > 0 {
decisionJSON, _ := json.MarshalIndent(decision.Decisions, "", " ")
record.DecisionJSON = string(decisionJSON)
}
}
if err != nil {
record.Success = false
record.ErrorMessage = fmt.Sprintf("获取AI决策失败: %v", err)
// 打印AI思维链即使有错误
if decision != nil && decision.CoTTrace != "" {
log.Printf("\n" + strings.Repeat("-", 70))
log.Println("💭 AI思维链分析错误情况:")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Printf(strings.Repeat("-", 70) + "\n")
}
at.decisionLogger.LogDecision(record)
return fmt.Errorf("获取AI决策失败: %w", err)
}
// 5. 打印AI思维链
log.Printf("\n" + strings.Repeat("-", 70))
log.Println("💭 AI思维链分析:")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Printf(strings.Repeat("-", 70) + "\n")
// 6. 打印AI决策
log.Printf("📋 AI决策列表 (%d 个):\n", len(decision.Decisions))
for i, d := range decision.Decisions {
log.Printf(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning)
if d.Action == "open_long" || d.Action == "open_short" {
log.Printf(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f",
d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit)
}
}
log.Println()
// 7. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
sortedDecisions := sortDecisionsByPriority(decision.Decisions)
log.Println("🔄 执行顺序(已优化): 先平仓→后开仓")
for i, d := range sortedDecisions {
log.Printf(" [%d] %s %s", i+1, d.Symbol, d.Action)
}
log.Println()
// 执行决策并记录结果
for _, d := range sortedDecisions {
actionRecord := logger.DecisionAction{
Action: d.Action,
Symbol: d.Symbol,
Quantity: 0,
Leverage: d.Leverage,
Price: 0,
Timestamp: time.Now(),
Success: false,
}
if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil {
log.Printf("❌ 执行决策失败 (%s %s): %v", d.Symbol, d.Action, err)
actionRecord.Error = err.Error()
record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("❌ %s %s 失败: %v", d.Symbol, d.Action, err))
} else {
actionRecord.Success = true
record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("✓ %s %s 成功", d.Symbol, d.Action))
// 成功执行后短暂延迟
time.Sleep(1 * time.Second)
}
record.Decisions = append(record.Decisions, actionRecord)
}
// 8. 保存决策记录
if err := at.decisionLogger.LogDecision(record); err != nil {
log.Printf("⚠ 保存决策记录失败: %v", err)
}
return nil
}
// buildTradingContext 构建交易上下文
func (at *AutoTrader) buildTradingContext() (*market.TradingContext, error) {
// 1. 获取账户信息
balance, err := at.trader.GetBalance()
if err != nil {
return nil, fmt.Errorf("获取账户余额失败: %w", err)
}
// 获取账户字段
totalWalletBalance := 0.0
totalUnrealizedProfit := 0.0
availableBalance := 0.0
if wallet, ok := balance["totalWalletBalance"].(float64); ok {
totalWalletBalance = wallet
}
if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok {
totalUnrealizedProfit = unrealized
}
if avail, ok := balance["availableBalance"].(float64); ok {
availableBalance = avail
}
// Total Equity = 钱包余额 + 未实现盈亏
totalEquity := totalWalletBalance + totalUnrealizedProfit
// 2. 获取持仓信息
positions, err := at.trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
var positionInfos []market.PositionInfo
totalMarginUsed := 0.0
for _, pos := range positions {
symbol := pos["symbol"].(string)
side := pos["side"].(string)
entryPrice := pos["entryPrice"].(float64)
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity // 空仓数量为负,转为正数
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
liquidationPrice := pos["liquidationPrice"].(float64)
// 计算盈亏百分比
pnlPct := 0.0
if side == "long" {
pnlPct = ((markPrice - entryPrice) / entryPrice) * 100
} else {
pnlPct = ((entryPrice - markPrice) / entryPrice) * 100
}
// 计算占用保证金(估算)
leverage := 10 // 默认值,实际应该从持仓信息获取
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
totalMarginUsed += marginUsed
positionInfos = append(positionInfos, market.PositionInfo{
Symbol: symbol,
Side: side,
EntryPrice: entryPrice,
MarkPrice: markPrice,
Quantity: quantity,
Leverage: leverage,
UnrealizedPnL: unrealizedPnl,
UnrealizedPnLPct: pnlPct,
LiquidationPrice: liquidationPrice,
MarginUsed: marginUsed,
})
}
// 3. 获取合并的候选币种池AI500 + OI Top去重
// 无论有没有持仓都分析相同数量的币种让AI看到所有好机会
// AI会根据保证金使用率和现有持仓情况自己决定是否要换仓
const ai500Limit = 20 // AI500取前20个评分最高的币种
// 获取合并后的币种池AI500 + OI Top
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
if err != nil {
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
}
// 构建候选币种列表(包含来源信息)
var candidateCoins []market.CandidateCoin
for _, symbol := range mergedPool.AllSymbols {
sources := mergedPool.SymbolSources[symbol]
candidateCoins = append(candidateCoins, market.CandidateCoin{
Symbol: symbol,
Sources: sources, // "ai500" 和/或 "oi_top"
})
}
log.Printf("📋 合并币种池: AI500前%d + OI_Top20 = 总计%d个候选币种",
ai500Limit, len(candidateCoins))
// 4. 计算总盈亏
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
if at.initialBalance > 0 {
totalPnLPct = (totalPnL / at.initialBalance) * 100
}
marginUsedPct := 0.0
if totalEquity > 0 {
marginUsedPct = (totalMarginUsed / totalEquity) * 100
}
// 5. 分析历史表现最近20个周期
performance, err := at.decisionLogger.AnalyzePerformance(20)
if err != nil {
log.Printf("⚠️ 分析历史表现失败: %v", err)
// 不影响主流程,继续执行
}
// 6. 构建上下文
ctx := &market.TradingContext{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
CallCount: at.callCount,
Account: market.AccountInfo{
TotalEquity: totalEquity,
AvailableBalance: availableBalance,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
MarginUsed: totalMarginUsed,
MarginUsedPct: marginUsedPct,
PositionCount: len(positionInfos),
},
Positions: positionInfos,
CandidateCoins: candidateCoins,
Performance: performance, // 添加历史表现分析
}
return ctx, nil
}
// executeDecision 执行AI决策
func (at *AutoTrader) executeDecision(decision *market.TradingDecision) error {
switch decision.Action {
case "open_long":
return at.executeOpenLong(decision)
case "open_short":
return at.executeOpenShort(decision)
case "close_long":
return at.executeCloseLong(decision)
case "close_short":
return at.executeCloseShort(decision)
case "hold", "wait":
// 无需执行,仅记录
return nil
default:
return fmt.Errorf("未知的action: %s", decision.Action)
}
}
// executeDecisionWithRecord 执行AI决策并记录详细信息
func (at *AutoTrader) executeDecisionWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
switch decision.Action {
case "open_long":
return at.executeOpenLongWithRecord(decision, actionRecord)
case "open_short":
return at.executeOpenShortWithRecord(decision, actionRecord)
case "close_long":
return at.executeCloseLongWithRecord(decision, actionRecord)
case "close_short":
return at.executeCloseShortWithRecord(decision, actionRecord)
case "hold", "wait":
// 无需执行,仅记录
return nil
default:
return fmt.Errorf("未知的action: %s", decision.Action)
}
}
// executeOpenLong 执行开多仓
func (at *AutoTrader) executeOpenLong(decision *market.TradingDecision) error {
log.Printf(" 📈 开多仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量:仓位大小(USD) / 当前价格
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
log.Printf(" ✓ 开仓成功订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeOpenShort 执行开空仓
func (at *AutoTrader) executeOpenShort(decision *market.TradingDecision) error {
log.Printf(" 📉 开空仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
log.Printf(" ✓ 开仓成功订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeCloseLong 执行平多仓
func (at *AutoTrader) executeCloseLong(decision *market.TradingDecision) error {
log.Printf(" 🔄 平多仓: %s", decision.Symbol)
_, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
log.Printf(" ✓ 平仓成功")
return nil
}
// executeCloseShort 执行平空仓
func (at *AutoTrader) executeCloseShort(decision *market.TradingDecision) error {
log.Printf(" 🔄 平空仓: %s", decision.Symbol)
_, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
log.Printf(" ✓ 平仓成功")
return nil
}
// executeOpenLongWithRecord 执行开多仓并记录详细信息
func (at *AutoTrader) executeOpenLongWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 📈 开多仓: %s", decision.Symbol)
// ⚠️ 关键:检查是否已有同币种同方向持仓,如果有则拒绝开仓(防止仓位叠加超限)
positions, err := at.trader.GetPositions()
if err == nil {
for _, pos := range positions {
if pos["symbol"] == decision.Symbol && pos["side"] == "long" {
return fmt.Errorf("❌ %s 已有多仓,拒绝开仓以防止仓位叠加超限。如需换仓,请先给出 close_long 决策", decision.Symbol)
}
}
}
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 开仓成功订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeOpenShortWithRecord 执行开空仓并记录详细信息
func (at *AutoTrader) executeOpenShortWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 📉 开空仓: %s", decision.Symbol)
// ⚠️ 关键:检查是否已有同币种同方向持仓,如果有则拒绝开仓(防止仓位叠加超限)
positions, err := at.trader.GetPositions()
if err == nil {
for _, pos := range positions {
if pos["symbol"] == decision.Symbol && pos["side"] == "short" {
return fmt.Errorf("❌ %s 已有空仓,拒绝开仓以防止仓位叠加超限。如需换仓,请先给出 close_short 决策", decision.Symbol)
}
}
}
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 开仓成功订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeCloseLongWithRecord 执行平多仓并记录详细信息
func (at *AutoTrader) executeCloseLongWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🔄 平多仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 平仓
order, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 平仓成功")
return nil
}
// executeCloseShortWithRecord 执行平空仓并记录详细信息
func (at *AutoTrader) executeCloseShortWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🔄 平空仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 平仓
order, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 平仓成功")
return nil
}
// GetID 获取trader ID
func (at *AutoTrader) GetID() string {
return at.id
}
// GetName 获取trader名称
func (at *AutoTrader) GetName() string {
return at.name
}
// GetAIModel 获取AI模型
func (at *AutoTrader) GetAIModel() string {
return at.aiModel
}
// GetDecisionLogger 获取决策日志记录器
func (at *AutoTrader) GetDecisionLogger() *logger.DecisionLogger {
return at.decisionLogger
}
// GetStatus 获取系统状态用于API
func (at *AutoTrader) GetStatus() map[string]interface{} {
aiProvider := "DeepSeek"
if at.config.UseQwen {
aiProvider = "Qwen"
}
return map[string]interface{}{
"trader_id": at.id,
"trader_name": at.name,
"ai_model": at.aiModel,
"is_running": at.isRunning,
"start_time": at.startTime.Format(time.RFC3339),
"runtime_minutes": int(time.Since(at.startTime).Minutes()),
"call_count": at.callCount,
"initial_balance": at.initialBalance,
"scan_interval": at.config.ScanInterval.String(),
"stop_until": at.stopUntil.Format(time.RFC3339),
"last_reset_time": at.lastResetTime.Format(time.RFC3339),
"ai_provider": aiProvider,
}
}
// GetAccountInfo 获取账户信息用于API
func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) {
balance, err := at.trader.GetBalance()
if err != nil {
return nil, fmt.Errorf("获取余额失败: %w", err)
}
// 获取账户字段
totalWalletBalance := 0.0
totalUnrealizedProfit := 0.0
availableBalance := 0.0
if wallet, ok := balance["totalWalletBalance"].(float64); ok {
totalWalletBalance = wallet
}
if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok {
totalUnrealizedProfit = unrealized
}
if avail, ok := balance["availableBalance"].(float64); ok {
availableBalance = avail
}
// Total Equity = 钱包余额 + 未实现盈亏
totalEquity := totalWalletBalance + totalUnrealizedProfit
// 获取持仓计算总保证金
positions, err := at.trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
totalMarginUsed := 0.0
totalUnrealizedPnL := 0.0
for _, pos := range positions {
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
totalUnrealizedPnL += unrealizedPnl
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
totalMarginUsed += marginUsed
}
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
if at.initialBalance > 0 {
totalPnLPct = (totalPnL / at.initialBalance) * 100
}
marginUsedPct := 0.0
if totalEquity > 0 {
marginUsedPct = (totalMarginUsed / totalEquity) * 100
}
return map[string]interface{}{
// 核心字段
"total_equity": totalEquity, // 账户净值 = wallet + unrealized
"wallet_balance": totalWalletBalance, // 钱包余额(不含未实现盈亏)
"unrealized_profit": totalUnrealizedProfit, // 未实现盈亏从API
"available_balance": availableBalance, // 可用余额
// 盈亏统计
"total_pnl": totalPnL, // 总盈亏 = equity - initial
"total_pnl_pct": totalPnLPct, // 总盈亏百分比
"total_unrealized_pnl": totalUnrealizedPnL, // 未实现盈亏(从持仓计算)
"initial_balance": at.initialBalance, // 初始余额
"daily_pnl": at.dailyPnL, // 日盈亏
// 持仓信息
"position_count": len(positions), // 持仓数量
"margin_used": totalMarginUsed, // 保证金占用
"margin_used_pct": marginUsedPct, // 保证金使用率
}, nil
}
// GetPositions 获取持仓列表用于API
func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) {
positions, err := at.trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
var result []map[string]interface{}
for _, pos := range positions {
symbol := pos["symbol"].(string)
side := pos["side"].(string)
entryPrice := pos["entryPrice"].(float64)
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
liquidationPrice := pos["liquidationPrice"].(float64)
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
pnlPct := 0.0
if side == "long" {
pnlPct = ((markPrice - entryPrice) / entryPrice) * 100
} else {
pnlPct = ((entryPrice - markPrice) / entryPrice) * 100
}
marginUsed := (quantity * markPrice) / float64(leverage)
result = append(result, map[string]interface{}{
"symbol": symbol,
"side": side,
"entry_price": entryPrice,
"mark_price": markPrice,
"quantity": quantity,
"leverage": leverage,
"unrealized_pnl": unrealizedPnl,
"unrealized_pnl_pct": pnlPct,
"liquidation_price": liquidationPrice,
"margin_used": marginUsed,
})
}
return result, nil
}
// sortDecisionsByPriority 对决策排序先平仓再开仓最后hold/wait
// 这样可以避免换仓时仓位叠加超限
func sortDecisionsByPriority(decisions []market.TradingDecision) []market.TradingDecision {
if len(decisions) <= 1 {
return decisions
}
// 定义优先级
getActionPriority := func(action string) int {
switch action {
case "close_long", "close_short":
return 1 // 最高优先级:先平仓
case "open_long", "open_short":
return 2 // 次优先级:后开仓
case "hold", "wait":
return 3 // 最低优先级:观望
default:
return 999 // 未知动作放最后
}
}
// 复制决策列表
sorted := make([]market.TradingDecision, len(decisions))
copy(sorted, decisions)
// 按优先级排序
for i := 0; i < len(sorted)-1; i++ {
for j := i + 1; j < len(sorted); j++ {
if getActionPriority(sorted[i].Action) > getActionPriority(sorted[j].Action) {
sorted[i], sorted[j] = sorted[j], sorted[i]
}
}
}
return sorted
}

564
trader/binance_futures.go Normal file
View File

@@ -0,0 +1,564 @@
package trader
import (
"context"
"fmt"
"log"
"strconv"
"time"
"github.com/adshao/go-binance/v2/futures"
)
// FuturesTrader 币安合约交易器
type FuturesTrader struct {
client *futures.Client
}
// NewFuturesTrader 创建合约交易器
func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader {
client := futures.NewClient(apiKey, secretKey)
return &FuturesTrader{
client: client,
}
}
// GetBalance 获取账户余额
func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
log.Printf("🔄 正在调用币安API获取账户余额...")
account, err := t.client.NewGetAccountService().Do(context.Background())
if err != nil {
log.Printf("❌ 币安API调用失败: %v", err)
return nil, fmt.Errorf("获取账户信息失败: %w", err)
}
result := make(map[string]interface{})
result["totalWalletBalance"], _ = strconv.ParseFloat(account.TotalWalletBalance, 64)
result["availableBalance"], _ = strconv.ParseFloat(account.AvailableBalance, 64)
result["totalUnrealizedProfit"], _ = strconv.ParseFloat(account.TotalUnrealizedProfit, 64)
log.Printf("✓ 币安API返回: 总余额=%s, 可用=%s, 未实现盈亏=%s",
account.TotalWalletBalance,
account.AvailableBalance,
account.TotalUnrealizedProfit)
return result, nil
}
// GetPositions 获取所有持仓
func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
positions, err := t.client.NewGetPositionRiskService().Do(context.Background())
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
var result []map[string]interface{}
for _, pos := range positions {
posAmt, _ := strconv.ParseFloat(pos.PositionAmt, 64)
if posAmt == 0 {
continue // 跳过无持仓的
}
posMap := make(map[string]interface{})
posMap["symbol"] = pos.Symbol
posMap["positionAmt"], _ = strconv.ParseFloat(pos.PositionAmt, 64)
posMap["entryPrice"], _ = strconv.ParseFloat(pos.EntryPrice, 64)
posMap["markPrice"], _ = strconv.ParseFloat(pos.MarkPrice, 64)
posMap["unRealizedProfit"], _ = strconv.ParseFloat(pos.UnRealizedProfit, 64)
posMap["leverage"], _ = strconv.ParseFloat(pos.Leverage, 64)
posMap["liquidationPrice"], _ = strconv.ParseFloat(pos.LiquidationPrice, 64)
// 判断方向
if posAmt > 0 {
posMap["side"] = "long"
} else {
posMap["side"] = "short"
}
result = append(result, posMap)
}
return result, nil
}
// SetLeverage 设置杠杆(智能判断+冷却期)
func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
// 先尝试获取当前杠杆(从持仓信息)
currentLeverage := 0
positions, err := t.GetPositions()
if err == nil {
for _, pos := range positions {
if pos["symbol"] == symbol {
if lev, ok := pos["leverage"].(float64); ok {
currentLeverage = int(lev)
break
}
}
}
}
// 如果当前杠杆已经是目标杠杆,跳过
if currentLeverage == leverage && currentLeverage > 0 {
log.Printf(" ✓ %s 杠杆已是 %dx无需切换", symbol, leverage)
return nil
}
// 切换杠杆
_, err = t.client.NewChangeLeverageService().
Symbol(symbol).
Leverage(leverage).
Do(context.Background())
if err != nil {
// 如果错误信息包含"No need to change",说明杠杆已经是目标值
if contains(err.Error(), "No need to change") {
log.Printf(" ✓ %s 杠杆已是 %dx", symbol, leverage)
return nil
}
return fmt.Errorf("设置杠杆失败: %w", err)
}
log.Printf(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
// 切换杠杆后等待5秒避免冷却期错误
log.Printf(" ⏱ 等待5秒冷却期...")
time.Sleep(5 * time.Second)
return nil
}
// SetMarginType 设置保证金模式
func (t *FuturesTrader) SetMarginType(symbol string, marginType futures.MarginType) error {
err := t.client.NewChangeMarginTypeService().
Symbol(symbol).
MarginType(marginType).
Do(context.Background())
if err != nil {
// 如果已经是该模式,不算错误
if contains(err.Error(), "No need to change") {
log.Printf(" ✓ %s 保证金模式已是 %s", symbol, marginType)
return nil
}
return fmt.Errorf("设置保证金模式失败: %w", err)
}
log.Printf(" ✓ %s 保证金模式已切换为 %s", symbol, marginType)
// 切换保证金模式后等待3秒避免冷却期错误
log.Printf(" ⏱ 等待3秒冷却期...")
time.Sleep(3 * time.Second)
return nil
}
// OpenLong 开多仓
func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先取消该币种的所有委托单(清理旧的止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
}
// 设置杠杆
if err := t.SetLeverage(symbol, leverage); err != nil {
return nil, err
}
// 设置逐仓模式
if err := t.SetMarginType(symbol, futures.MarginTypeIsolated); err != nil {
return nil, err
}
// 格式化数量到正确精度
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价买入订单
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeBuy).
PositionSide(futures.PositionSideTypeLong).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("开多仓失败: %w", err)
}
log.Printf("✓ 开多仓成功: %s 数量: %s", symbol, quantityStr)
log.Printf(" 订单ID: %d", order.OrderID)
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// OpenShort 开空仓
func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先取消该币种的所有委托单(清理旧的止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
}
// 设置杠杆
if err := t.SetLeverage(symbol, leverage); err != nil {
return nil, err
}
// 设置逐仓模式
if err := t.SetMarginType(symbol, futures.MarginTypeIsolated); err != nil {
return nil, err
}
// 格式化数量到正确精度
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价卖出订单
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeSell).
PositionSide(futures.PositionSideTypeShort).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("开空仓失败: %w", err)
}
log.Printf("✓ 开空仓成功: %s 数量: %s", symbol, quantityStr)
log.Printf(" 订单ID: %d", order.OrderID)
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// CloseLong 平多仓
func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
// 如果数量为0获取当前持仓数量
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "long" {
quantity = pos["positionAmt"].(float64)
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("没有找到 %s 的多仓", symbol)
}
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价卖出订单(平多)
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeSell).
PositionSide(futures.PositionSideTypeLong).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("平多仓失败: %w", err)
}
log.Printf("✓ 平多仓成功: %s 数量: %s", symbol, quantityStr)
// 平仓后取消该币种的所有挂单(止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消挂单失败: %v", err)
}
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// CloseShort 平空仓
func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
// 如果数量为0获取当前持仓数量
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "short" {
quantity = -pos["positionAmt"].(float64) // 空仓数量是负的,取绝对值
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("没有找到 %s 的空仓", symbol)
}
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价买入订单(平空)
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeBuy).
PositionSide(futures.PositionSideTypeShort).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("平空仓失败: %w", err)
}
log.Printf("✓ 平空仓成功: %s 数量: %s", symbol, quantityStr)
// 平仓后取消该币种的所有挂单(止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消挂单失败: %v", err)
}
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// CancelAllOrders 取消该币种的所有挂单
func (t *FuturesTrader) CancelAllOrders(symbol string) error {
err := t.client.NewCancelAllOpenOrdersService().
Symbol(symbol).
Do(context.Background())
if err != nil {
return fmt.Errorf("取消挂单失败: %w", err)
}
log.Printf(" ✓ 已取消 %s 的所有挂单", symbol)
return nil
}
// GetMarketPrice 获取市场价格
func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) {
prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background())
if err != nil {
return 0, fmt.Errorf("获取价格失败: %w", err)
}
if len(prices) == 0 {
return 0, fmt.Errorf("未找到价格")
}
price, err := strconv.ParseFloat(prices[0].Price, 64)
if err != nil {
return 0, err
}
return price, nil
}
// CalculatePositionSize 计算仓位大小
func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float64, leverage int) float64 {
riskAmount := balance * (riskPercent / 100.0)
positionValue := riskAmount * float64(leverage)
quantity := positionValue / price
return quantity
}
// SetStopLoss 设置止损单
func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
var side futures.SideType
var posSide futures.PositionSideType
if positionSide == "LONG" {
side = futures.SideTypeSell
posSide = futures.PositionSideTypeLong
} else {
side = futures.SideTypeBuy
posSide = futures.PositionSideTypeShort
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return err
}
_, err = t.client.NewCreateOrderService().
Symbol(symbol).
Side(side).
PositionSide(posSide).
Type(futures.OrderTypeStopMarket).
StopPrice(fmt.Sprintf("%.8f", stopPrice)).
Quantity(quantityStr).
WorkingType(futures.WorkingTypeContractPrice).
ClosePosition(true).
Do(context.Background())
if err != nil {
return fmt.Errorf("设置止损失败: %w", err)
}
log.Printf(" 止损价设置: %.4f", stopPrice)
return nil
}
// SetTakeProfit 设置止盈单
func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
var side futures.SideType
var posSide futures.PositionSideType
if positionSide == "LONG" {
side = futures.SideTypeSell
posSide = futures.PositionSideTypeLong
} else {
side = futures.SideTypeBuy
posSide = futures.PositionSideTypeShort
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return err
}
_, err = t.client.NewCreateOrderService().
Symbol(symbol).
Side(side).
PositionSide(posSide).
Type(futures.OrderTypeTakeProfitMarket).
StopPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
Quantity(quantityStr).
WorkingType(futures.WorkingTypeContractPrice).
ClosePosition(true).
Do(context.Background())
if err != nil {
return fmt.Errorf("设置止盈失败: %w", err)
}
log.Printf(" 止盈价设置: %.4f", takeProfitPrice)
return nil
}
// GetSymbolPrecision 获取交易对的数量精度
func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())
if err != nil {
return 0, fmt.Errorf("获取交易规则失败: %w", err)
}
for _, s := range exchangeInfo.Symbols {
if s.Symbol == symbol {
// 从LOT_SIZE filter获取精度
for _, filter := range s.Filters {
if filter["filterType"] == "LOT_SIZE" {
stepSize := filter["stepSize"].(string)
precision := calculatePrecision(stepSize)
log.Printf(" %s 数量精度: %d (stepSize: %s)", symbol, precision, stepSize)
return precision, nil
}
}
}
}
log.Printf(" ⚠ %s 未找到精度信息使用默认精度3", symbol)
return 3, nil // 默认精度为3
}
// calculatePrecision 从stepSize计算精度
func calculatePrecision(stepSize string) int {
// 去除尾部的0
stepSize = trimTrailingZeros(stepSize)
// 查找小数点
dotIndex := -1
for i := 0; i < len(stepSize); i++ {
if stepSize[i] == '.' {
dotIndex = i
break
}
}
// 如果没有小数点或小数点在最后精度为0
if dotIndex == -1 || dotIndex == len(stepSize)-1 {
return 0
}
// 返回小数点后的位数
return len(stepSize) - dotIndex - 1
}
// trimTrailingZeros 去除尾部的0
func trimTrailingZeros(s string) string {
// 如果没有小数点,直接返回
if !stringContains(s, ".") {
return s
}
// 从后向前遍历去除尾部的0
for len(s) > 0 && s[len(s)-1] == '0' {
s = s[:len(s)-1]
}
// 如果最后一位是小数点,也去掉
if len(s) > 0 && s[len(s)-1] == '.' {
s = s[:len(s)-1]
}
return s
}
// FormatQuantity 格式化数量到正确的精度
func (t *FuturesTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
precision, err := t.GetSymbolPrecision(symbol)
if err != nil {
// 如果获取失败,使用默认格式
return fmt.Sprintf("%.3f", quantity), nil
}
format := fmt.Sprintf("%%.%df", precision)
return fmt.Sprintf(format, quantity), nil
}
// 辅助函数
func contains(s, substr string) bool {
return len(s) >= len(substr) && stringContains(s, substr)
}
func stringContains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}