Files
nofx/trader/auto_trader.go
ZhouYongyou d2cf05c4e1 fix(trader): 修复余额自动同步逻辑,使用净资产追踪防止误同步
## 问题背景

原有的 autoSyncBalanceIfNeeded() 函数存在严重 Bug:
1. 使用"可用余额"而非"总资产"
2. 开仓时可用余额下降,触发错误同步
3. 将 initial_balance 从 400 改为 220(可用余额)
4. 导致盈亏计算完全错误

实际案例:
- 初始余额:400 USDT
- 开仓后可用余额:220 USDT
- 错误同步:initial_balance = 220 
- 显示盈亏:+169 USDT(错误,应该是 -14 USDT)

## 解决方案:净资产追踪法

### 核心算法
```
净资产 = 总资产 - 未实现盈亏

净资产变化 = (当前净资产 - initial_balance) / initial_balance

如果:
  - 净资产变化 > 10%
  - 且净资产增加
则:
  - 同步 initial_balance = 净资产
```

### 关键改进

1. **使用总资产而非可用余额**
   - 优先使用 total_wallet_balance
   - fallback: totalWalletBalance, balance

2. **计算净资产(排除交易盈亏)**
   - 提取所有持仓的未实现盈亏
   - 净资产 = 总资产 - 未实现盈亏
   - 这个值不受开仓影响,只受充值/提现影响

3. **即使有持仓也能检测充值**
   - 场景:有持仓时充值 100 USDT
   - 旧方案:跳过检测(需要等平仓)
   - 新方案:净资产 +100,立即检测 ✓

4. **字段缺失保护**
   - 支持多种字段名:unrealizedProfit, unRealizedProfit
   - 支持 string 类型的 PNL
   - 如果无法获取 PNL,安全地跳过同步

5. **只在净资产增加时同步**
   - 充值:净资产 +100 → 同步 ✓
   - 盈利:净资产 +50 → 同步 ✓
   - 亏损:净资产 -50 → 跳过(保留原始 initial_balance)

6. **详细的日志输出**
   - 显示:总资产、未实现盈亏、净资产
   - 原因分析:"可能是用户充值"、"可能是交易盈利"

## 测试验证

### 场景 1:开仓交易(原 Bug)
```
初始:400 USDT
开仓后:
  - 总资产:385 USDT
  - 未实现盈亏:-15 USDT
  - 净资产:385 - (-15) = 400 USDT

结果:净资产不变,不触发同步 ✓
```

### 场景 2:有持仓时充值(关键改进)
```
初始:400 USDT
有持仓(浮盈 +20)时充值 100 USDT:
  - 总资产:520 USDT
  - 未实现盈亏:+20 USDT
  - 净资产:520 - 20 = 500 USDT

结果:检测到净资产 +100 (+25%),同步 ✓
日志:在有持仓的情况下净资产增加,很可能是用户充值
```

### 场景 3:交易亏损
```
初始:400 USDT
亏损 100 USDT 后平仓:
  - 总资产:300 USDT
  - 未实现盈亏:0
  - 净资产:300 USDT

结果:净资产减少,跳过同步 ✓
initial_balance 保持 400,显示正确的亏损 -100 USDT
```

## 技术细节

- 同步间隔:10 分钟
- 触发阈值:净资产变化 > 10%
- 字段支持:unrealizedProfit (float64/string), unRealizedProfit
- 错误处理:API 失败或字段缺失时安全跳过
- 日志级别:详细(便于排查问题)

## 影响范围

- 修复了 initial_balance 被错误修改的根本原因
- 支持有持仓时检测充值(之前无法检测)
- 不会因交易亏损而错误更新 initial_balance
- 向后兼容,不影响现有用户

## 后续优化建议

1. 添加手动同步按钮(前端)
2. 记录同步日志到数据库(审计)
3. 添加配置选项(可禁用、调整阈值)
4. 考虑添加累计盈亏字段(避免语义冲突)

Related: #issue-initial-balance-incorrect
Related: commit 8a1e931 (API 层保护,本次修复底层逻辑)
2025-11-05 03:05:51 +08:00

1790 lines
59 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 trader
import (
"encoding/json"
"fmt"
"log"
"math"
"nofx/config"
"nofx/decision"
"nofx/logger"
"nofx/market"
"nofx/mcp"
"nofx/news"
"nofx/news/provider/telegram"
"nofx/pool"
"strconv"
"strings"
"time"
"github.com/samber/lo"
)
// AutoTraderConfig 自动交易配置(简化版 - AI全权决策
type AutoTraderConfig struct {
// Trader标识
ID string // Trader唯一标识用于日志目录等
Name string // Trader显示名称
AIModel string // AI模型: "qwen" 或 "deepseek"
// 交易平台选择
Exchange string // "binance", "hyperliquid" 或 "aster"
// 币安API配置
BinanceAPIKey string
BinanceSecretKey string
// Hyperliquid配置
HyperliquidPrivateKey string
HyperliquidWalletAddr string
HyperliquidTestnet bool
// Aster配置
AsterUser string // Aster主钱包地址
AsterSigner string // Aster API钱包地址
AsterPrivateKey string // Aster API钱包私钥
CoinPoolAPIURL string
// AI配置
UseQwen bool
DeepSeekKey string
QwenKey string
// 自定义AI API配置
CustomAPIURL string
CustomAPIKey string
CustomModelName string
// 扫描配置
ScanInterval time.Duration // 扫描间隔建议3分钟
// 账户配置
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
// 杠杆配置
BTCETHLeverage int // BTC和ETH的杠杆倍数
AltcoinLeverage int // 山寨币的杠杆倍数
// 风险控制仅作为提示AI可自主决定
MaxDailyLoss float64 // 最大日亏损百分比(提示)
MaxDrawdown float64 // 最大回撤百分比(提示)
StopTradingTime time.Duration // 触发风控后暂停时长
// 仓位模式
IsCrossMargin bool // true=全仓模式, false=逐仓模式
// 币种配置
DefaultCoins []string // 默认币种列表(从数据库获取)
TradingCoins []string // 实际交易币种列表
// 系统提示词模板
SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive"
// 新闻源配置
NewsConfig []config.NewsConfig
}
// PositionSnapshot 持仓快照(用于检测自动平仓)
type PositionSnapshot struct {
Symbol string
Side string
Quantity float64
EntryPrice float64
Leverage int
}
// AutoTrader 自动交易器
type AutoTrader struct {
id string // Trader唯一标识
name string // Trader显示名称
aiModel string // AI模型名称
exchange string // 交易平台名称
config AutoTraderConfig
trader Trader // 使用Trader接口支持多平台
mcpClient *mcp.Client
decisionLogger *logger.DecisionLogger // 决策日志记录器
initialBalance float64
dailyPnL float64
customPrompt string // 自定义交易策略prompt
overrideBasePrompt bool // 是否覆盖基础prompt
systemPromptTemplate string // 系统提示词模板名称
defaultCoins []string // 默认币种列表(从数据库获取)
tradingCoins []string // 实际交易币种列表
lastResetTime time.Time
stopUntil time.Time
isRunning bool
startTime time.Time // 系统启动时间
callCount int // AI调用次数
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
lastPositions map[string]*PositionSnapshot // 上一个周期的持仓快照 (symbol_side -> snapshot)
newsProcessor []news.Provider // 新闻
lastBalanceSyncTime time.Time // 上次余额同步时间
database *config.Database // 数据库引用(用于自动更新余额)
userID string // 用户ID
}
// NewAutoTrader 创建自动交易器
func NewAutoTrader(traderConfig AutoTraderConfig, db *config.Database, userID string) (*AutoTrader, error) {
// 设置默认值
if traderConfig.ID == "" {
traderConfig.ID = "default_trader"
}
if traderConfig.Name == "" {
traderConfig.Name = "Default Trader"
}
if traderConfig.AIModel == "" {
if traderConfig.UseQwen {
traderConfig.AIModel = "qwen"
} else {
traderConfig.AIModel = "deepseek"
}
}
mcpClient := mcp.New()
// 初始化AI
if traderConfig.AIModel == "custom" {
// 使用自定义API
mcpClient.SetCustomAPI(traderConfig.CustomAPIURL, traderConfig.CustomAPIKey, traderConfig.CustomModelName)
log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", traderConfig.Name, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
} else if traderConfig.UseQwen || traderConfig.AIModel == "qwen" {
// 使用Qwen (支持自定义URL和Model)
mcpClient.SetQwenAPIKey(traderConfig.QwenKey, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
if traderConfig.CustomAPIURL != "" || traderConfig.CustomModelName != "" {
log.Printf("🤖 [%s] 使用阿里云Qwen AI (自定义URL: %s, 模型: %s)", traderConfig.Name, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
} else {
log.Printf("🤖 [%s] 使用阿里云Qwen AI", traderConfig.Name)
}
} else {
// 默认使用DeepSeek (支持自定义URL和Model)
mcpClient.SetDeepSeekAPIKey(traderConfig.DeepSeekKey, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
if traderConfig.CustomAPIURL != "" || traderConfig.CustomModelName != "" {
log.Printf("🤖 [%s] 使用DeepSeek AI (自定义URL: %s, 模型: %s)", traderConfig.Name, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
} else {
log.Printf("🤖 [%s] 使用DeepSeek AI", traderConfig.Name)
}
}
// 初始化新闻提取器
var newsProcessor []news.Provider
for _, newsCfg := range traderConfig.NewsConfig {
switch newsCfg.Provider {
case news.ProviderTelegram:
newsProvider, err := telegram.NewSearcher(newsCfg.Telegram.BaseURL,
newsCfg.Telegram.ProxyURL,
lo.Map(newsCfg.TelegramChannel, func(item config.NewsConfigTelegramChannel, _ int) telegram.Channel {
return telegram.Channel{
ID: item.ID,
Name: item.Name,
}
}),
)
if err != nil {
panic(err)
}
newsProcessor = append(newsProcessor, newsProvider)
}
}
// 初始化币种池API
if traderConfig.CoinPoolAPIURL != "" {
pool.SetCoinPoolAPI(traderConfig.CoinPoolAPIURL)
}
// 设置默认交易平台
if traderConfig.Exchange == "" {
traderConfig.Exchange = "binance"
}
// 根据配置创建对应的交易器
var trader Trader
var err error
// 记录仓位模式(通用)
marginModeStr := "全仓"
if !traderConfig.IsCrossMargin {
marginModeStr = "逐仓"
}
log.Printf("📊 [%s] 仓位模式: %s", traderConfig.Name, marginModeStr)
switch traderConfig.Exchange {
case "binance":
log.Printf("🏦 [%s] 使用币安合约交易", traderConfig.Name)
trader = NewFuturesTrader(traderConfig.BinanceAPIKey, traderConfig.BinanceSecretKey)
case "hyperliquid":
log.Printf("🏦 [%s] 使用Hyperliquid交易", traderConfig.Name)
trader, err = NewHyperliquidTrader(traderConfig.HyperliquidPrivateKey, traderConfig.HyperliquidWalletAddr, traderConfig.HyperliquidTestnet)
if err != nil {
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err)
}
case "aster":
log.Printf("🏦 [%s] 使用Aster交易", traderConfig.Name)
trader, err = NewAsterTrader(traderConfig.AsterUser, traderConfig.AsterSigner, traderConfig.AsterPrivateKey)
if err != nil {
return nil, fmt.Errorf("初始化Aster交易器失败: %w", err)
}
default:
return nil, fmt.Errorf("不支持的交易平台: %s", traderConfig.Exchange)
}
// 验证初始金额配置
if traderConfig.InitialBalance <= 0 {
return nil, fmt.Errorf("初始金额必须大于0请在配置中设置InitialBalance")
}
// 初始化决策日志记录器使用trader ID创建独立目录
logDir := fmt.Sprintf("decision_logs/%s", traderConfig.ID)
decisionLogger := logger.NewDecisionLogger(logDir)
// 设置默认系统提示词模板
systemPromptTemplate := traderConfig.SystemPromptTemplate
if systemPromptTemplate == "" {
// feature/partial-close-dynamic-tpsl 分支默认使用 adaptive支持动态止盈止损
systemPromptTemplate = "adaptive"
}
return &AutoTrader{
id: traderConfig.ID,
name: traderConfig.Name,
aiModel: traderConfig.AIModel,
exchange: traderConfig.Exchange,
config: traderConfig,
trader: trader,
mcpClient: mcpClient,
decisionLogger: decisionLogger,
initialBalance: traderConfig.InitialBalance,
systemPromptTemplate: systemPromptTemplate,
defaultCoins: traderConfig.DefaultCoins,
tradingCoins: traderConfig.TradingCoins,
lastResetTime: time.Now(),
startTime: time.Now(),
callCount: 0,
isRunning: false,
positionFirstSeenTime: make(map[string]int64),
lastPositions: make(map[string]*PositionSnapshot),
newsProcessor: newsProcessor,
lastBalanceSyncTime: time.Now(), // 初始化为当前时间
database: db,
userID: userID,
}, 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("⏹ 自动交易系统停止")
}
// autoSyncBalanceIfNeeded 自动同步余额(智能检测充值/提现,即使有持仓也能检测)
func (at *AutoTrader) autoSyncBalanceIfNeeded() {
// 距离上次同步不足10分钟跳过
if time.Since(at.lastBalanceSyncTime) < 10*time.Minute {
return
}
log.Printf("🔄 [%s] 开始自动检查余额变化...", at.name)
// 1. 查询实际余额
balanceInfo, err := at.trader.GetBalance()
if err != nil {
log.Printf("⚠️ [%s] 查询余额失败: %v", at.name, err)
at.lastBalanceSyncTime = time.Now()
return
}
// 2. 提取总资产(总钱包余额)
var totalBalance float64
if total, ok := balanceInfo["total_wallet_balance"].(float64); ok && total > 0 {
totalBalance = total
} else if total, ok := balanceInfo["totalWalletBalance"].(float64); ok && total > 0 {
totalBalance = total
} else if total, ok := balanceInfo["balance"].(float64); ok && total > 0 {
totalBalance = total
log.Printf("⚠️ [%s] 使用 'balance' 字段作为总资产", at.name)
} else {
log.Printf("⚠️ [%s] 无法提取总资产", at.name)
at.lastBalanceSyncTime = time.Now()
return
}
// 3. 获取持仓信息并计算未实现盈亏
positions, err := at.trader.GetPositions()
if err != nil {
log.Printf("⚠️ [%s] 获取持仓信息失败,为安全起见跳过余额同步: %v", at.name, err)
at.lastBalanceSyncTime = time.Now()
return
}
var totalUnrealizedPnl float64
hasOpenPosition := false
pnlFieldMissing := false
for _, pos := range positions {
if amt, ok := pos["positionAmt"].(float64); ok && math.Abs(amt) > 0.0001 {
hasOpenPosition = true
// 提取未实现盈亏(支持多种字段名)
pnlFound := false
if pnl, ok := pos["unrealizedProfit"].(float64); ok {
totalUnrealizedPnl += pnl
pnlFound = true
} else if pnl, ok := pos["unRealizedProfit"].(float64); ok {
totalUnrealizedPnl += pnl
pnlFound = true
} else if pnl, ok := pos["unRealizedProfit"].(string); ok {
// 处理字符串类型的 PNL
if parsedPnl, err := strconv.ParseFloat(pnl, 64); err == nil {
totalUnrealizedPnl += parsedPnl
pnlFound = true
}
}
if !pnlFound {
pnlFieldMissing = true
posSymbol, _ := pos["symbol"].(string)
log.Printf(" ⚠️ [%s] 持仓 %s 缺少未实现盈亏字段", at.name, posSymbol)
}
}
}
// 如果有持仓但无法获取盈亏数据,为安全起见跳过同步
if hasOpenPosition && pnlFieldMissing {
log.Printf(" ⚠️ [%s] 无法获取完整的未实现盈亏数据,跳过余额同步", at.name)
at.lastBalanceSyncTime = time.Now()
return
}
// 4. 计算净资产(总资产 - 未实现盈亏 = 实际投入本金)
// 这个值不受持仓盈亏影响,只受充值/提现影响
netBalance := totalBalance - totalUnrealizedPnl
log.Printf(" [%s] 余额详情: 总资产=%.2f, 未实现盈亏=%.2f, 净资产=%.2f",
at.name, totalBalance, totalUnrealizedPnl, netBalance)
oldBalance := at.initialBalance
// 防止除以零:如果初始余额无效,直接更新
if oldBalance <= 0 {
log.Printf("⚠️ [%s] 初始余额无效 (%.2f),更新为当前净资产 %.2f USDT",
at.name, oldBalance, netBalance)
at.initialBalance = netBalance
if at.database != nil {
at.database.UpdateTraderInitialBalance(at.userID, at.id, netBalance)
}
at.lastBalanceSyncTime = time.Now()
return
}
// 5. 计算净资产变化(这个变化排除了交易盈亏的影响)
netChangeDiff := netBalance - oldBalance
netChangePercent := (netChangeDiff / oldBalance) * 100
// 6. 智能同步逻辑
// - 净资产变化超过 10%
// - 且净资产增加(排除提现和亏损)
if math.Abs(netChangePercent) > 10.0 {
if netBalance > oldBalance {
// 净资产增加 → 很可能是充值
log.Printf("🔔 [%s] 检测到净资产增加: %.2f → %.2f USDT (+%.2f, +%.2f%%)",
at.name, oldBalance, netBalance, netChangeDiff, netChangePercent)
if hasOpenPosition {
log.Printf(" → 原因分析: 在有持仓的情况下净资产增加,很可能是用户充值")
} else {
log.Printf(" → 原因分析: 无持仓且净资产增加,可能是充值或交易盈利")
}
// 更新 initial_balance
at.initialBalance = netBalance
if at.database != nil {
err := at.database.UpdateTraderInitialBalance(at.userID, at.id, netBalance)
if err != nil {
log.Printf("❌ [%s] 更新数据库失败: %v", at.name, err)
} else {
log.Printf("✅ [%s] 已自动同步余额到数据库", at.name)
}
}
} else {
// 净资产减少 → 可能是提现或亏损
log.Printf(" ⚠️ [%s] 检测到净资产减少: %.2f → %.2f USDT (%.2f, %.2f%%)",
at.name, oldBalance, netBalance, netChangeDiff, netChangePercent)
if hasOpenPosition {
log.Printf(" → 原因分析: 在有持仓的情况下净资产减少,可能是用户提现")
log.Printf(" → 为保守起见,跳过同步(避免因交易亏损而错误更新)")
} else {
log.Printf(" → 原因分析: 无持仓且净资产减少,可能是提现或交易亏损")
log.Printf(" → 跳过同步以保留原始 initial_balance维持正确的盈亏基准")
}
}
} else {
log.Printf(" ✓ [%s] 净资产变化不大 (%.2f%%),无需同步", at.name, netChangePercent)
}
at.lastBalanceSyncTime = time.Now()
}
// runCycle 运行一个交易周期使用AI全权决策
func (at *AutoTrader) runCycle() error {
at.callCount++
log.Print("\n" + strings.Repeat("=", 70))
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
log.Print(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. 自动同步余额每10分钟检查一次充值/提现后自动更新)
at.autoSyncBalanceIfNeeded()
// 4. 收集交易上下文
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)
}
// 3.1 检测自动平仓(止损/止盈触发)
autoClosedActions := at.detectAutoClosedPositions(ctx.Positions)
for _, action := range autoClosedActions {
log.Printf("[AUTO-CLOSE] 检测到自动平仓: %s %s (价格: %.4f)", action.Symbol, action.Action, action.Price)
record.Decisions = append(record.Decisions, action)
record.ExecutionLog = append(record.ExecutionLog,
fmt.Sprintf("[AUTO-CLOSE] 自动平仓: %s %s (止损/止盈触发)", action.Symbol, action.Action))
}
// 保存账户状态快照
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.Printf("🤖 正在请求AI分析并决策... [模板: %s]", at.systemPromptTemplate)
decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt, at.systemPromptTemplate)
// 即使有错误也保存思维链、决策和输入prompt用于debug
if decision != nil {
record.SystemPrompt = decision.SystemPrompt // 保存系统提示词
record.InputPrompt = decision.UserPrompt
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 {
if decision.SystemPrompt != "" {
log.Print("\n" + strings.Repeat("=", 70))
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
log.Println(strings.Repeat("=", 70))
log.Println(decision.SystemPrompt)
log.Print(strings.Repeat("=", 70) + "\n")
}
if decision.CoTTrace != "" {
log.Print("\n" + strings.Repeat("-", 70))
log.Println("💭 AI思维链分析错误情况:")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Print(strings.Repeat("-", 70) + "\n")
}
}
at.decisionLogger.LogDecision(record)
return fmt.Errorf("获取AI决策失败: %w", err)
}
// // 5. 打印系统提示词
// log.Printf("\n" + strings.Repeat("=", 70))
// log.Printf("📋 系统提示词 [模板: %s]", at.systemPromptTemplate)
// log.Println(strings.Repeat("=", 70))
// log.Println(decision.SystemPrompt)
// log.Printf(strings.Repeat("=", 70) + "\n")
// 6. 打印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")
// 7. 打印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()
// 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
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)
}
// 9. 更新持仓快照(用于下一周期检测自动平仓)
// 注意:需要重新获取当前持仓,因为 AI 可能在本周期执行了平仓操作
// ctx.Positions 是周期开始时的持仓,不反映本周期的变化
currentPositionsAfterExecution, err := at.trader.GetPositions()
if err != nil {
log.Printf("⚠ 更新持仓快照失败,清空快照以避免误报: %v", err)
at.lastPositions = make(map[string]*PositionSnapshot) // 清空快照防止误报
} else {
// 将原始持仓数据转换为快照格式
snapshots := make([]PositionSnapshot, 0, len(currentPositionsAfterExecution))
for _, pos := range currentPositionsAfterExecution {
symbol := pos["symbol"].(string)
side := pos["side"].(string)
entryPrice := pos["entryPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity // 空仓数量为负,转为正数
}
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
snapshots = append(snapshots, PositionSnapshot{
Symbol: symbol,
Side: side,
EntryPrice: entryPrice,
Quantity: quantity,
Leverage: leverage,
})
}
at.lastPositions = make(map[string]*PositionSnapshot)
for _, snap := range snapshots {
posKey := snap.Symbol + "_" + snap.Side
// 创建副本避免指针问题
snapshot := snap
at.lastPositions[posKey] = &snapshot
}
}
// 10. 保存决策记录
if err := at.decisionLogger.LogDecision(record); err != nil {
log.Printf("⚠ 保存决策记录失败: %v", err)
}
return nil
}
// buildTradingContext 构建交易上下文
func (at *AutoTrader) buildTradingContext() (*decision.Context, 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 []decision.PositionInfo
totalMarginUsed := 0.0
// 当前持仓的key集合用于清理已平仓的记录
currentPositionKeys := make(map[string]bool)
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 // 空仓数量为负,转为正数
}
// 跳过已平仓的持仓quantity = 0防止"幽灵持仓"传递给AI
if quantity == 0 {
continue
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
liquidationPrice := pos["liquidationPrice"].(float64)
// 计算占用保证金(估算)
leverage := 10 // 默认值,实际应该从持仓信息获取
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
// 计算盈亏百分比(基于实际盈亏和保证金)
pnlPct := 0.0
if marginUsed > 0 {
pnlPct = (unrealizedPnl / marginUsed) * 100
}
totalMarginUsed += marginUsed
// 跟踪持仓首次出现时间
posKey := symbol + "_" + side
currentPositionKeys[posKey] = true
if _, exists := at.positionFirstSeenTime[posKey]; !exists {
// 新持仓,记录当前时间
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
}
updateTime := at.positionFirstSeenTime[posKey]
positionInfos = append(positionInfos, decision.PositionInfo{
Symbol: symbol,
Side: side,
EntryPrice: entryPrice,
MarkPrice: markPrice,
Quantity: quantity,
Leverage: leverage,
UnrealizedPnL: unrealizedPnl,
UnrealizedPnLPct: pnlPct,
LiquidationPrice: liquidationPrice,
MarginUsed: marginUsed,
UpdateTime: updateTime,
})
}
// 清理已平仓的持仓记录,并撤销孤儿委托单
for key := range at.positionFirstSeenTime {
if !currentPositionKeys[key] {
// 仓位消失了(可能被止损/止盈触发,或被強平)
// 提取币种名称key 格式BTCUSDT_long 或 SOLUSDT_short
parts := strings.Split(key, "_")
if len(parts) == 2 {
symbol := parts[0]
side := parts[1]
log.Printf("⚠️ 检测到仓位消失: %s %s → 自动撤销委托单", symbol, side)
// 撤销该币种的所有委托单(清理孤儿止损/止盈單)
if err := at.trader.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠️ 撤销 %s 委托单失败: %v", symbol, err)
} else {
log.Printf(" ✓ 已撤销 %s 的所有委托单", symbol)
}
}
delete(at.positionFirstSeenTime, key)
}
}
// 3. 获取交易员的候选币种池
candidateCoins, err := at.getCandidateCoins()
if err != nil {
return nil, fmt.Errorf("获取候选币种失败: %w", err)
}
// 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. 分析历史表现最近100个周期避免长期持仓的交易记录丢失
// 假设每3分钟一个周期100个周期 = 5小时足够覆盖大部分交易
performance, err := at.decisionLogger.AnalyzePerformance(100)
if err != nil {
log.Printf("⚠️ 分析历史表现失败: %v", err)
// 不影响主流程继续执行但设置performance为nil以避免传递错误数据
performance = nil
}
// 6. 提取新闻内容(根据持仓和候选币种动态收集)
newsItem := make(map[string][]news.NewsItem)
for _, newspro := range at.newsProcessor {
// 收集需要新闻的币种(持仓 + 候选币前几个)
newsSymbols := at.extractNewsSymbols(positionInfos, candidateCoins)
if len(newsSymbols) == 0 {
log.Printf("⚠️ 没有需要收集新闻的币种,跳过新闻收集")
continue
}
newsMap, err := newspro.FetchNews(newsSymbols, 100)
if err != nil {
log.Printf("⚠️ 获取新闻内容失败: %v", err)
continue
}
for symbol, value := range newsMap {
newsItem[symbol] = append(newsItem[symbol], value...)
}
log.Printf("📰 收集了 %d 个币种的新闻: %v", len(newsSymbols), newsSymbols)
}
// 7. 构建上下文
ctx := &decision.Context{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
CallCount: at.callCount,
BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数
AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数
Account: decision.AccountInfo{
TotalEquity: totalEquity,
AvailableBalance: availableBalance,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
MarginUsed: totalMarginUsed,
MarginUsedPct: marginUsedPct,
PositionCount: len(positionInfos),
},
Positions: positionInfos,
CandidateCoins: candidateCoins,
Performance: performance, // 添加历史表现分析
News: newsItem,
}
return ctx, nil
}
// executeDecisionWithRecord 执行AI决策并记录详细信息
func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, 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 "update_stop_loss":
return at.executeUpdateStopLossWithRecord(decision, actionRecord)
case "update_take_profit":
return at.executeUpdateTakeProfitWithRecord(decision, actionRecord)
case "partial_close":
return at.executePartialCloseWithRecord(decision, actionRecord)
case "hold", "wait":
// 无需执行,仅记录
return nil
default:
return fmt.Errorf("未知的action: %s", decision.Action)
}
}
// executeOpenLongWithRecord 执行开多仓并记录详细信息
func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, 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.Get(decision.Symbol)
if err != nil {
return err
}
// ⚠️ 保证金验证防止保证金不足错误code=-2019
// position_size_usd 是名义价值(包含杠杆),实际需要的保证金 = position_size_usd / leverage
requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage)
// 获取当前可用余额
balance, err := at.trader.GetBalance()
if err != nil {
return fmt.Errorf("获取账户余额失败: %w", err)
}
availableBalance := 0.0
if avail, ok := balance["availableBalance"].(float64); ok {
availableBalance = avail
}
// 手续费估算Taker费率 0.04%
estimatedFee := decision.PositionSizeUSD * 0.0004
totalRequired := requiredMargin + estimatedFee
// 验证保证金充足(需要保证金 + 手续费 <= 可用余额)
if totalRequired > availableBalance {
return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT保证金 %.2f + 手续费 %.2f),可用 %.2f USDT。建议降低仓位或杠杆",
totalRequired, requiredMargin, estimatedFee, availableBalance)
}
log.Printf(" ✓ 保证金检查通过: 需要 %.2f USDT可用 %.2f USDT", totalRequired, availableBalance)
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// 设置仓位模式
if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil {
log.Printf(" ⚠️ 设置仓位模式失败: %v", err)
// 继续执行,不影响交易
}
// 开仓
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)
// 记录开仓时间
posKey := decision.Symbol + "_long"
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
// 设置止损止盈
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 *decision.Decision, 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.Get(decision.Symbol)
if err != nil {
return err
}
// ⚠️ 保证金验证防止保证金不足错误code=-2019
// position_size_usd 是名义价值(包含杠杆),实际需要的保证金 = position_size_usd / leverage
requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage)
// 获取当前可用余额
balance, err := at.trader.GetBalance()
if err != nil {
return fmt.Errorf("获取账户余额失败: %w", err)
}
availableBalance := 0.0
if avail, ok := balance["availableBalance"].(float64); ok {
availableBalance = avail
}
// 手续费估算Taker费率 0.04%
estimatedFee := decision.PositionSizeUSD * 0.0004
totalRequired := requiredMargin + estimatedFee
// 验证保证金充足(需要保证金 + 手续费 <= 可用余额)
if totalRequired > availableBalance {
return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT保证金 %.2f + 手续费 %.2f),可用 %.2f USDT。建议降低仓位或杠杆",
totalRequired, requiredMargin, estimatedFee, availableBalance)
}
log.Printf(" ✓ 保证金检查通过: 需要 %.2f USDT可用 %.2f USDT", totalRequired, availableBalance)
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// 设置仓位模式
if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil {
log.Printf(" ⚠️ 设置仓位模式失败: %v", err)
// 继续执行,不影响交易
}
// 开仓
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)
// 记录开仓时间
posKey := decision.Symbol + "_short"
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
// 设置止损止盈
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 *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🔄 平多仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.Get(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 *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🔄 平空仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.Get(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
}
// executeUpdateStopLossWithRecord 执行调整止损并记录详细信息
func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🎯 调整止损: %s → %.2f", decision.Symbol, decision.NewStopLoss)
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
return fmt.Errorf("获取持仓失败: %w", err)
}
// 查找目标持仓
var targetPosition map[string]interface{}
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 {
targetPosition = pos
break
}
}
if targetPosition == nil {
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
}
// 获取持仓方向和数量
side, _ := targetPosition["side"].(string)
positionSide := strings.ToUpper(side)
positionAmt, _ := targetPosition["positionAmt"].(float64)
// 验证新止损价格合理性
if positionSide == "LONG" && decision.NewStopLoss >= marketData.CurrentPrice {
return fmt.Errorf("多单止损必须低于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
}
if positionSide == "SHORT" && decision.NewStopLoss <= marketData.CurrentPrice {
return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
}
// 取消旧的止损单(只删除止损单,不影响止盈单)
if err := at.trader.CancelStopLossOrders(decision.Symbol); err != nil {
log.Printf(" ⚠ 取消旧止损单失败: %v", err)
// 不中断执行,继续设置新止损
}
// 调用交易所 API 修改止损
quantity := math.Abs(positionAmt)
err = at.trader.SetStopLoss(decision.Symbol, positionSide, quantity, decision.NewStopLoss)
if err != nil {
return fmt.Errorf("修改止损失败: %w", err)
}
log.Printf(" ✓ 止损已调整: %.2f (当前价格: %.2f)", decision.NewStopLoss, marketData.CurrentPrice)
return nil
}
// executeUpdateTakeProfitWithRecord 执行调整止盈并记录详细信息
func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🎯 调整止盈: %s → %.2f", decision.Symbol, decision.NewTakeProfit)
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
return fmt.Errorf("获取持仓失败: %w", err)
}
// 查找目标持仓
var targetPosition map[string]interface{}
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 {
targetPosition = pos
break
}
}
if targetPosition == nil {
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
}
// 获取持仓方向和数量
side, _ := targetPosition["side"].(string)
positionSide := strings.ToUpper(side)
positionAmt, _ := targetPosition["positionAmt"].(float64)
// 验证新止盈价格合理性
if positionSide == "LONG" && decision.NewTakeProfit <= marketData.CurrentPrice {
return fmt.Errorf("多单止盈必须高于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
}
if positionSide == "SHORT" && decision.NewTakeProfit >= marketData.CurrentPrice {
return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
}
// 取消旧的止盈单(只删除止盈单,不影响止损单)
if err := at.trader.CancelTakeProfitOrders(decision.Symbol); err != nil {
log.Printf(" ⚠ 取消旧止盈单失败: %v", err)
// 不中断执行,继续设置新止盈
}
// 调用交易所 API 修改止盈
quantity := math.Abs(positionAmt)
err = at.trader.SetTakeProfit(decision.Symbol, positionSide, quantity, decision.NewTakeProfit)
if err != nil {
return fmt.Errorf("修改止盈失败: %w", err)
}
log.Printf(" ✓ 止盈已调整: %.2f (当前价格: %.2f)", decision.NewTakeProfit, marketData.CurrentPrice)
return nil
}
// executePartialCloseWithRecord 执行部分平仓并记录详细信息
func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 📊 部分平仓: %s %.1f%%", decision.Symbol, decision.ClosePercentage)
// 验证百分比范围
if decision.ClosePercentage <= 0 || decision.ClosePercentage > 100 {
return fmt.Errorf("平仓百分比必须在 0-100 之间,当前: %.1f", decision.ClosePercentage)
}
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
return fmt.Errorf("获取持仓失败: %w", err)
}
// 查找目标持仓
var targetPosition map[string]interface{}
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 {
targetPosition = pos
break
}
}
if targetPosition == nil {
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
}
// 获取持仓方向和数量
side, _ := targetPosition["side"].(string)
positionSide := strings.ToUpper(side)
positionAmt, _ := targetPosition["positionAmt"].(float64)
// 计算平仓数量
totalQuantity := math.Abs(positionAmt)
closeQuantity := totalQuantity * (decision.ClosePercentage / 100.0)
actionRecord.Quantity = closeQuantity
// ✅ Layer 2: 最小仓位检查(防止产生小额剩余)
markPrice, _ := targetPosition["markPrice"].(float64)
currentPositionValue := totalQuantity * markPrice
remainingQuantity := totalQuantity - closeQuantity
remainingValue := remainingQuantity * markPrice
const MIN_POSITION_VALUE = 10.0 // 最小持仓价值 10 USDT
if remainingValue > 0 && remainingValue < MIN_POSITION_VALUE {
log.Printf("⚠️ 检测到 partial_close 后剩余仓位 %.2f USDT < %.0f USDT",
remainingValue, MIN_POSITION_VALUE)
log.Printf(" → 当前仓位价值: %.2f USDT, 平仓 %.1f%%, 剩余: %.2f USDT",
currentPositionValue, decision.ClosePercentage, remainingValue)
log.Printf(" → 自动修正为全部平仓,避免产生无法平仓的小额剩余")
// 🔄 自动修正为全部平仓
if positionSide == "LONG" {
decision.Action = "close_long"
log.Printf(" ✓ 已修正为: close_long")
return at.executeCloseLongWithRecord(decision, actionRecord)
} else {
decision.Action = "close_short"
log.Printf(" ✓ 已修正为: close_short")
return at.executeCloseShortWithRecord(decision, actionRecord)
}
}
// 执行平仓
var order map[string]interface{}
if positionSide == "LONG" {
order, err = at.trader.CloseLong(decision.Symbol, closeQuantity)
} else {
order, err = at.trader.CloseShort(decision.Symbol, closeQuantity)
}
if err != nil {
return fmt.Errorf("部分平仓失败: %w", err)
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f",
closeQuantity, decision.ClosePercentage, remainingQuantity)
// 🔧 FIX: 部分平仓后重新设置止盈止损(基于剩余数量)
// 币安会自动取消原来的止盈止损订单(因为数量不匹配),所以必须重新设置
if decision.NewStopLoss > 0 || decision.NewTakeProfit > 0 {
log.Printf(" 🎯 更新剩余仓位的止盈止损...")
// 设置新止损(基于剩余数量)
if decision.NewStopLoss > 0 {
if err := at.trader.SetStopLoss(decision.Symbol, positionSide, remainingQuantity, decision.NewStopLoss); err != nil {
log.Printf(" ⚠️ 设置新止损失败: %v", err)
} else {
log.Printf(" ✓ 已设置新止损: %.4f (数量: %.4f)", decision.NewStopLoss, remainingQuantity)
}
}
// 设置新止盈(基于剩余数量)
if decision.NewTakeProfit > 0 {
if err := at.trader.SetTakeProfit(decision.Symbol, positionSide, remainingQuantity, decision.NewTakeProfit); err != nil {
log.Printf(" ⚠️ 设置新止盈失败: %v", err)
} else {
log.Printf(" ✓ 已设置新止盈: %.4f (数量: %.4f)", decision.NewTakeProfit, remainingQuantity)
}
}
} else {
// ⚠️ AI 没有提供新的止盈止损,剩余仓位将失去保护
log.Printf(" ⚠️⚠️⚠️ 警告: 部分平仓后AI未提供新的止盈止损价格")
log.Printf(" → 剩余仓位 %.4f (价值 %.2f USDT) 目前没有止盈止损保护", remainingQuantity, remainingValue)
log.Printf(" → 建议: 在 partial_close 决策中包含 new_stop_loss 和 new_take_profit 字段")
}
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
}
// GetExchange 获取交易所
func (at *AutoTrader) GetExchange() string {
return at.exchange
}
// SetCustomPrompt 设置自定义交易策略prompt
func (at *AutoTrader) SetCustomPrompt(prompt string) {
at.customPrompt = prompt
}
// SetOverrideBasePrompt 设置是否覆盖基础prompt
func (at *AutoTrader) SetOverrideBasePrompt(override bool) {
at.overrideBasePrompt = override
}
// SetSystemPromptTemplate 设置系统提示词模板
func (at *AutoTrader) SetSystemPromptTemplate(templateName string) {
at.systemPromptTemplate = templateName
}
// GetSystemPromptTemplate 获取当前系统提示词模板名称
func (at *AutoTrader) GetSystemPromptTemplate() string {
return at.systemPromptTemplate
}
// 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,
"exchange": at.exchange,
"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)
}
// 计算占用保证金
marginUsed := (quantity * markPrice) / float64(leverage)
// 计算盈亏百分比(基于保证金)
// 收益率 = 未实现盈亏 / 保证金 × 100%
pnlPct := 0.0
if marginUsed > 0 {
pnlPct = (unrealizedPnl / marginUsed) * 100
}
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 []decision.Decision) []decision.Decision {
if len(decisions) <= 1 {
return decisions
}
// 定义优先级
getActionPriority := func(action string) int {
switch action {
case "close_long", "close_short", "partial_close":
return 1 // 最高优先级:先平仓(包括部分平仓)
case "update_stop_loss", "update_take_profit":
return 2 // 调整持仓止盈止损
case "open_long", "open_short":
return 3 // 次优先级:后开仓
case "hold", "wait":
return 4 // 最低优先级:观望
default:
return 999 // 未知动作放最后
}
}
// 复制决策列表
sorted := make([]decision.Decision, 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
}
// getCandidateCoins 获取交易员的候选币种列表
func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
if len(at.tradingCoins) == 0 {
// 使用数据库配置的默认币种列表
var candidateCoins []decision.CandidateCoin
if len(at.defaultCoins) > 0 {
// 使用数据库中配置的默认币种
for _, coin := range at.defaultCoins {
symbol := normalizeSymbol(coin)
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: []string{"default"}, // 标记为数据库默认币种
})
}
log.Printf("📋 [%s] 使用数据库默认币种: %d个币种 %v",
at.name, len(candidateCoins), at.defaultCoins)
return candidateCoins, nil
} else {
// 如果数据库中没有配置默认币种则使用AI500+OI Top作为fallback
const ai500Limit = 20 // AI500取前20个评分最高的币种
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
if err != nil {
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
}
// 构建候选币种列表(包含来源信息)
for _, symbol := range mergedPool.AllSymbols {
sources := mergedPool.SymbolSources[symbol]
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: sources, // "ai500" 和/或 "oi_top"
})
}
log.Printf("📋 [%s] 数据库无默认币种配置使用AI500+OI Top: AI500前%d + OI_Top20 = 总计%d个候选币种",
at.name, ai500Limit, len(candidateCoins))
return candidateCoins, nil
}
} else {
// 使用自定义币种列表
var candidateCoins []decision.CandidateCoin
for _, coin := range at.tradingCoins {
// 确保币种格式正确转为大写USDT交易对
symbol := normalizeSymbol(coin)
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: []string{"custom"}, // 标记为自定义来源
})
}
log.Printf("📋 [%s] 使用自定义币种: %d个币种 %v",
at.name, len(candidateCoins), at.tradingCoins)
return candidateCoins, nil
}
}
// normalizeSymbol 标准化币种符号确保以USDT结尾
func normalizeSymbol(symbol string) string {
// 转为大写
symbol = strings.ToUpper(strings.TrimSpace(symbol))
// 确保以USDT结尾
if !strings.HasSuffix(symbol, "USDT") {
symbol = symbol + "USDT"
}
return symbol
}
// extractNewsSymbols 提取需要收集新闻的币种(持仓 + 候选币前几个 + BTC
func (at *AutoTrader) extractNewsSymbols(positions []decision.PositionInfo, candidates []decision.CandidateCoin) []string {
const (
maxNewsSymbols = 10 // 最多收集10个币种的新闻避免请求过多
maxCandidatesForNews = 5 // 从候选币中取前5个
)
symbolSet := make(map[string]bool)
result := make([]string, 0, maxNewsSymbols)
// 1. 总是包含 BTC市场风向标
symbolSet["btc"] = true
result = append(result, "btc")
// 2. 添加所有持仓币种(这些是最重要的)
for _, pos := range positions {
// 转换为新闻 API 格式(小写,移除 USDT 后缀)
baseSymbol := strings.ToLower(strings.TrimSuffix(pos.Symbol, "USDT"))
if !symbolSet[baseSymbol] && len(result) < maxNewsSymbols {
symbolSet[baseSymbol] = true
result = append(result, baseSymbol)
}
}
// 3. 添加候选币种(前几个,按优先级)
for i, coin := range candidates {
if i >= maxCandidatesForNews {
break
}
if len(result) >= maxNewsSymbols {
break
}
baseSymbol := strings.ToLower(strings.TrimSuffix(coin.Symbol, "USDT"))
if !symbolSet[baseSymbol] {
symbolSet[baseSymbol] = true
result = append(result, baseSymbol)
}
}
return result
}
// detectAutoClosedPositions 检测自动平仓的持仓(止损/止盈触发)
func (at *AutoTrader) detectAutoClosedPositions(currentPositions []decision.PositionInfo) []logger.DecisionAction {
var autoClosedActions []logger.DecisionAction
// 创建当前持仓的map便于查找
currentPosMap := make(map[string]bool)
for _, pos := range currentPositions {
posKey := pos.Symbol + "_" + pos.Side
currentPosMap[posKey] = true
}
// 检查上一个周期的持仓,哪些现在消失了
for posKey, lastPos := range at.lastPositions {
if !currentPosMap[posKey] {
// 这个持仓消失了,说明被自动平仓了(止损/止盈触发)
// 获取当前价格作为平仓价格的近似值
marketData, err := market.Get(lastPos.Symbol)
closePrice := 0.0
if err == nil {
closePrice = marketData.CurrentPrice
} else {
// 如果无法获取当前价格使用入场价作为fallback
closePrice = lastPos.EntryPrice
}
// 确定是平多仓还是平空仓
action := "auto_close_long"
if lastPos.Side == "short" {
action = "auto_close_short"
}
// 创建自动平仓记录
autoClosedAction := logger.DecisionAction{
Action: action,
Symbol: lastPos.Symbol,
Quantity: lastPos.Quantity,
Leverage: lastPos.Leverage,
Price: closePrice,
OrderID: 0, // 自动平仓没有特定的订单ID
Timestamp: time.Now(),
Success: true,
Error: "",
}
autoClosedActions = append(autoClosedActions, autoClosedAction)
log.Printf("[AUTO-CLOSE] 检测到自动平仓: %s %s @ %.4f (可能由止损/止盈触发)",
lastPos.Symbol, action, closePrice)
}
}
return autoClosedActions
}
// updatePositionSnapshots 更新持仓快照(用于下一周期检测自动平仓)