mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 15:57:01 +08:00
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:
945
trader/auto_trader.go
Normal file
945
trader/auto_trader.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user