Files
nofx/trader/auto_trader.go
tinkle-community 1e5ece947c Feature/okx trading (#1177)
* feat: add OKX exchange trading support
- Add OKX trader client with full trading API integration
- Support API Key, Secret Key, and Passphrase authentication
- Add OKX icon and frontend configuration modal
- Update exchange store and types for OKX fields
* fix: add passphrase column migration and fix exchange type mapping
* fix: show OKX input fields in exchange config modal
* fix: ensure all supported exchanges exist for user when listing
* fix: simplify exchange type check condition for OKX
* debug: add visible debug info for exchange id
* fix: remove debug info from exchange config modal
* fix: add OKX to exchange type condition in AITradersPage
* feat: complete OKX trading support and fix exchange config issues
- Add LIGHTER exchange UI support in AITradersPage
- Add passphrase field to UpdateExchangeConfigRequest type
- Fix OKX HTTP client to bypass proxy (disable system proxy)
- Auto-fetch initial balance from exchange when not set
- Support multiple balance field names for different exchanges
- Add detailed error messages when trader fails to load
- Add lighter_api_key_private_key field to exchange store
2025-12-06 18:04:59 +08:00

1569 lines
49 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"
"math"
"nofx/decision"
"nofx/logger"
"nofx/market"
"nofx/mcp"
"nofx/store"
"strings"
"sync"
"time"
)
// AutoTraderConfig 自动交易配置(简化版 - AI全权决策
type AutoTraderConfig struct {
// Trader标识
ID string // Trader唯一标识用于日志目录等
Name string // Trader显示名称
AIModel string // AI模型: "qwen" 或 "deepseek"
// 交易平台选择
Exchange string // "binance", "bybit", "okx", "hyperliquid", "aster" 或 "lighter"
// 币安API配置
BinanceAPIKey string
BinanceSecretKey string
// Bybit API配置
BybitAPIKey string
BybitSecretKey string
// OKX API配置
OKXAPIKey string
OKXSecretKey string
OKXPassphrase string
// Hyperliquid配置
HyperliquidPrivateKey string
HyperliquidWalletAddr string
HyperliquidTestnet bool
// Aster配置
AsterUser string // Aster主钱包地址
AsterSigner string // Aster API钱包地址
AsterPrivateKey string // Aster API钱包私钥
// LIGHTER配置
LighterWalletAddr string // LIGHTER钱包地址L1 wallet
LighterPrivateKey string // LIGHTER L1私钥用于识别账户
LighterAPIKeyPrivateKey string // LIGHTER API Key私钥40字节用于签名交易
LighterTestnet bool // 是否使用testnet
// AI配置
UseQwen bool
DeepSeekKey string
QwenKey string
// 自定义AI API配置
CustomAPIURL string
CustomAPIKey string
CustomModelName string
// 扫描配置
ScanInterval time.Duration // 扫描间隔建议3分钟
// 账户配置
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
// 风险控制仅作为提示AI可自主决定
MaxDailyLoss float64 // 最大日亏损百分比(提示)
MaxDrawdown float64 // 最大回撤百分比(提示)
StopTradingTime time.Duration // 触发风控后暂停时长
// 仓位模式
IsCrossMargin bool // true=全仓模式, false=逐仓模式
// 策略配置(使用完整策略配置)
StrategyConfig *store.StrategyConfig // 策略配置包含币种来源、指标、风控、Prompt等
}
// AutoTrader 自动交易器
type AutoTrader struct {
id string // Trader唯一标识
name string // Trader显示名称
aiModel string // AI模型名称
exchange string // 交易平台名称
config AutoTraderConfig
trader Trader // 使用Trader接口支持多平台
mcpClient mcp.AIClient
store *store.Store // 数据存储(决策记录等)
strategyEngine *decision.StrategyEngine // 策略引擎(使用策略配置)
cycleNumber int // 当前周期编号
initialBalance float64
dailyPnL float64
customPrompt string // 自定义交易策略prompt
overrideBasePrompt bool // 是否覆盖基础prompt
lastResetTime time.Time
stopUntil time.Time
isRunning bool
startTime time.Time // 系统启动时间
callCount int // AI调用次数
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
stopMonitorCh chan struct{} // 用于停止监控goroutine
monitorWg sync.WaitGroup // 用于等待监控goroutine结束
peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比)
peakPnLCacheMutex sync.RWMutex // 缓存读写锁
lastBalanceSyncTime time.Time // 上次余额同步时间
userID string // 用户ID
}
// NewAutoTrader 创建自动交易器
// st 参数用于存储决策记录到数据库
func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*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"
}
}
mcpClient := mcp.New()
// 初始化AI
if config.AIModel == "custom" {
// 使用自定义API
mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName)
logger.Infof("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
} else if config.UseQwen || config.AIModel == "qwen" {
// 使用Qwen (支持自定义URL和Model)
mcpClient = mcp.NewQwenClient()
mcpClient.SetAPIKey(config.QwenKey, config.CustomAPIURL, config.CustomModelName)
if config.CustomAPIURL != "" || config.CustomModelName != "" {
logger.Infof("🤖 [%s] 使用阿里云Qwen AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
} else {
logger.Infof("🤖 [%s] 使用阿里云Qwen AI", config.Name)
}
} else {
// 默认使用DeepSeek (支持自定义URL和Model)
mcpClient = mcp.NewDeepSeekClient()
mcpClient.SetAPIKey(config.DeepSeekKey, config.CustomAPIURL, config.CustomModelName)
if config.CustomAPIURL != "" || config.CustomModelName != "" {
logger.Infof("🤖 [%s] 使用DeepSeek AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
} else {
logger.Infof("🤖 [%s] 使用DeepSeek AI", config.Name)
}
}
// 设置默认交易平台
if config.Exchange == "" {
config.Exchange = "binance"
}
// 根据配置创建对应的交易器
var trader Trader
var err error
// 记录仓位模式(通用)
marginModeStr := "全仓"
if !config.IsCrossMargin {
marginModeStr = "逐仓"
}
logger.Infof("📊 [%s] 仓位模式: %s", config.Name, marginModeStr)
switch config.Exchange {
case "binance":
logger.Infof("🏦 [%s] 使用币安合约交易", config.Name)
trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey, userID)
case "bybit":
logger.Infof("🏦 [%s] 使用Bybit合约交易", config.Name)
trader = NewBybitTrader(config.BybitAPIKey, config.BybitSecretKey)
case "okx":
logger.Infof("🏦 [%s] 使用OKX合约交易", config.Name)
trader = NewOKXTrader(config.OKXAPIKey, config.OKXSecretKey, config.OKXPassphrase)
case "hyperliquid":
logger.Infof("🏦 [%s] 使用Hyperliquid交易", config.Name)
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
if err != nil {
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err)
}
case "aster":
logger.Infof("🏦 [%s] 使用Aster交易", config.Name)
trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey)
if err != nil {
return nil, fmt.Errorf("初始化Aster交易器失败: %w", err)
}
case "lighter":
logger.Infof("🏦 [%s] 使用LIGHTER交易", config.Name)
// 優先使用 V2需要 API Key
if config.LighterAPIKeyPrivateKey != "" {
logger.Infof("✓ 使用 LIGHTER SDK (V2) - 完整簽名支持")
trader, err = NewLighterTraderV2(
config.LighterPrivateKey,
config.LighterWalletAddr,
config.LighterAPIKeyPrivateKey,
config.LighterTestnet,
)
if err != nil {
return nil, fmt.Errorf("初始化LIGHTER交易器(V2)失败: %w", err)
}
} else {
// 降級使用 V1基本HTTP實現
logger.Infof("⚠️ 使用 LIGHTER 基本實現 (V1) - 功能受限,請配置 API Key")
trader, err = NewLighterTrader(config.LighterPrivateKey, config.LighterWalletAddr, config.LighterTestnet)
if err != nil {
return nil, fmt.Errorf("初始化LIGHTER交易器(V1)失败: %w", err)
}
}
default:
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
}
// 验证初始金额配置如果为0则自动从交易所获取
if config.InitialBalance <= 0 {
logger.Infof("📊 [%s] 初始金额未设置,尝试从交易所获取当前余额...", config.Name)
account, err := trader.GetBalance()
if err != nil {
return nil, fmt.Errorf("初始金额未设置且无法从交易所获取余额: %w", err)
}
// 尝试多种余额字段名(不同交易所返回格式不同)
balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"}
var foundBalance float64
for _, key := range balanceKeys {
if balance, ok := account[key].(float64); ok && balance > 0 {
foundBalance = balance
break
}
}
if foundBalance > 0 {
config.InitialBalance = foundBalance
logger.Infof("✓ [%s] 自动获取初始金额: %.2f USDT", config.Name, foundBalance)
} else {
return nil, fmt.Errorf("初始金额必须大于0请在配置中设置InitialBalance或确保交易所账户有余额")
}
}
// 获取最后的周期编号(用于恢复)
var cycleNumber int
if st != nil {
cycleNumber, _ = st.Decision().GetLastCycleNumber(config.ID)
logger.Infof("📊 [%s] 决策记录将存储到数据库", config.Name)
}
// 创建策略引擎(必须有策略配置)
if config.StrategyConfig == nil {
return nil, fmt.Errorf("[%s] 未配置策略", config.Name)
}
strategyEngine := decision.NewStrategyEngine(config.StrategyConfig)
logger.Infof("✓ [%s] 使用策略引擎(策略配置已加载)", config.Name)
return &AutoTrader{
id: config.ID,
name: config.Name,
aiModel: config.AIModel,
exchange: config.Exchange,
config: config,
trader: trader,
mcpClient: mcpClient,
store: st,
strategyEngine: strategyEngine,
cycleNumber: cycleNumber,
initialBalance: config.InitialBalance,
lastResetTime: time.Now(),
startTime: time.Now(),
callCount: 0,
isRunning: false,
positionFirstSeenTime: make(map[string]int64),
stopMonitorCh: make(chan struct{}),
monitorWg: sync.WaitGroup{},
peakPnLCache: make(map[string]float64),
peakPnLCacheMutex: sync.RWMutex{},
lastBalanceSyncTime: time.Now(),
userID: userID,
}, nil
}
// Run 运行自动交易主循环
func (at *AutoTrader) Run() error {
at.isRunning = true
at.stopMonitorCh = make(chan struct{})
at.startTime = time.Now()
logger.Info("🚀 AI驱动自动交易系统启动")
logger.Infof("💰 初始余额: %.2f USDT", at.initialBalance)
logger.Infof("⚙️ 扫描间隔: %v", at.config.ScanInterval)
logger.Info("🤖 AI将全权决定杠杆、仓位大小、止损止盈等参数")
at.monitorWg.Add(1)
defer at.monitorWg.Done()
// 启动回撤监控
at.startDrawdownMonitor()
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
// 首次立即执行
if err := at.runCycle(); err != nil {
logger.Infof("❌ 执行失败: %v", err)
}
for at.isRunning {
select {
case <-ticker.C:
if err := at.runCycle(); err != nil {
logger.Infof("❌ 执行失败: %v", err)
}
case <-at.stopMonitorCh:
logger.Infof("[%s] ⏹ 收到停止信号,退出自动交易主循环", at.name)
return nil
}
}
return nil
}
// Stop 停止自动交易
func (at *AutoTrader) Stop() {
if !at.isRunning {
return
}
at.isRunning = false
close(at.stopMonitorCh) // 通知监控goroutine停止
at.monitorWg.Wait() // 等待监控goroutine结束
logger.Info("⏹ 自动交易系统停止")
}
// runCycle 运行一个交易周期使用AI全权决策
func (at *AutoTrader) runCycle() error {
at.callCount++
logger.Info("\n" + strings.Repeat("=", 70) + "\n")
logger.Infof("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
logger.Info(strings.Repeat("=", 70))
// 创建决策记录
record := &store.DecisionRecord{
ExecutionLog: []string{},
Success: true,
}
// 1. 检查是否需要停止交易
if time.Now().Before(at.stopUntil) {
remaining := at.stopUntil.Sub(time.Now())
logger.Infof("⏸ 风险控制:暂停交易中,剩余 %.0f 分钟", remaining.Minutes())
record.Success = false
record.ErrorMessage = fmt.Sprintf("风险控制暂停中,剩余 %.0f 分钟", remaining.Minutes())
at.saveDecision(record)
return nil
}
// 2. 重置日盈亏(每天重置)
if time.Since(at.lastResetTime) > 24*time.Hour {
at.dailyPnL = 0
at.lastResetTime = time.Now()
logger.Info("📅 日盈亏已重置")
}
// 4. 收集交易上下文
ctx, err := at.buildTradingContext()
if err != nil {
record.Success = false
record.ErrorMessage = fmt.Sprintf("构建交易上下文失败: %v", err)
at.saveDecision(record)
return fmt.Errorf("构建交易上下文失败: %w", err)
}
// 保存账户状态快照
record.AccountState = store.AccountSnapshot{
TotalBalance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL,
AvailableBalance: ctx.Account.AvailableBalance,
TotalUnrealizedProfit: ctx.Account.UnrealizedPnL,
PositionCount: ctx.Account.PositionCount,
MarginUsedPct: ctx.Account.MarginUsedPct,
InitialBalance: at.initialBalance, // 记录当时的初始余额基准
}
// 保存持仓快照
for _, pos := range ctx.Positions {
record.Positions = append(record.Positions, store.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,
})
}
logger.Info(strings.Repeat("=", 70))
for _, coin := range ctx.CandidateCoins {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
}
logger.Infof("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d",
ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount)
// 5. 使用策略引擎调用AI获取决策
logger.Infof("🤖 正在请求AI分析并决策... [策略引擎]")
aiDecision, err := decision.GetFullDecisionWithStrategy(ctx, at.mcpClient, at.strategyEngine, "balanced")
if aiDecision != nil && aiDecision.AIRequestDurationMs > 0 {
record.AIRequestDurationMs = aiDecision.AIRequestDurationMs
logger.Infof("⏱️ AI调用耗时: %.2f 秒", float64(record.AIRequestDurationMs)/1000)
record.ExecutionLog = append(record.ExecutionLog,
fmt.Sprintf("AI调用耗时: %d ms", record.AIRequestDurationMs))
}
// 即使有错误也保存思维链、决策和输入prompt用于debug
if aiDecision != nil {
record.SystemPrompt = aiDecision.SystemPrompt // 保存系统提示词
record.InputPrompt = aiDecision.UserPrompt
record.CoTTrace = aiDecision.CoTTrace
if len(aiDecision.Decisions) > 0 {
decisionJSON, _ := json.MarshalIndent(aiDecision.Decisions, "", " ")
record.DecisionJSON = string(decisionJSON)
}
}
if err != nil {
record.Success = false
record.ErrorMessage = fmt.Sprintf("获取AI决策失败: %v", err)
// 打印系统提示词和AI思维链即使有错误也要输出以便调试
if aiDecision != nil {
logger.Info("\n" + strings.Repeat("=", 70) + "\n")
logger.Infof("📋 系统提示词 (错误情况)")
logger.Info(strings.Repeat("=", 70))
logger.Info(aiDecision.SystemPrompt)
logger.Info(strings.Repeat("=", 70))
if aiDecision.CoTTrace != "" {
logger.Info("\n" + strings.Repeat("-", 70) + "\n")
logger.Info("💭 AI思维链分析错误情况:")
logger.Info(strings.Repeat("-", 70))
logger.Info(aiDecision.CoTTrace)
logger.Info(strings.Repeat("-", 70))
}
}
at.saveDecision(record)
return fmt.Errorf("获取AI决策失败: %w", err)
}
// // 5. 打印系统提示词
// logger.Infof("\n" + strings.Repeat("=", 70))
// logger.Infof("📋 系统提示词 [模板: %s]", at.systemPromptTemplate)
// logger.Info(strings.Repeat("=", 70))
// logger.Info(decision.SystemPrompt)
// logger.Infof(strings.Repeat("=", 70) + "\n")
// 6. 打印AI思维链
// logger.Infof("\n" + strings.Repeat("-", 70))
// logger.Info("💭 AI思维链分析:")
// logger.Info(strings.Repeat("-", 70))
// logger.Info(decision.CoTTrace)
// logger.Infof(strings.Repeat("-", 70) + "\n")
// 7. 打印AI决策
// logger.Infof("📋 AI决策列表 (%d 个):\n", len(decision.Decisions))
// for i, d := range decision.Decisions {
// logger.Infof(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning)
// if d.Action == "open_long" || d.Action == "open_short" {
// logger.Infof(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f",
// d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit)
// }
// }
logger.Info()
logger.Info(strings.Repeat("-", 70))
// 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
logger.Info(strings.Repeat("-", 70))
// 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
sortedDecisions := sortDecisionsByPriority(aiDecision.Decisions)
logger.Info("🔄 执行顺序(已优化): 先平仓→后开仓")
for i, d := range sortedDecisions {
logger.Infof(" [%d] %s %s", i+1, d.Symbol, d.Action)
}
logger.Info()
// 执行决策并记录结果
for _, d := range sortedDecisions {
actionRecord := store.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 {
logger.Infof("❌ 执行决策失败 (%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. 保存决策记录
if err := at.saveDecision(record); err != nil {
logger.Infof("⚠ 保存决策记录失败: %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)
totalMarginUsed += marginUsed
// 计算盈亏百分比(基于保证金,考虑杠杆)
pnlPct := calculatePnLPercentage(unrealizedPnl, marginUsed)
// 跟踪持仓首次出现时间
posKey := symbol + "_" + side
currentPositionKeys[posKey] = true
if _, exists := at.positionFirstSeenTime[posKey]; !exists {
// 新持仓,记录当前时间
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
}
updateTime := at.positionFirstSeenTime[posKey]
// 获取该持仓的历史最高收益率
at.peakPnLCacheMutex.RLock()
peakPnlPct := at.peakPnLCache[posKey]
at.peakPnLCacheMutex.RUnlock()
positionInfos = append(positionInfos, decision.PositionInfo{
Symbol: symbol,
Side: side,
EntryPrice: entryPrice,
MarkPrice: markPrice,
Quantity: quantity,
Leverage: leverage,
UnrealizedPnL: unrealizedPnl,
UnrealizedPnLPct: pnlPct,
PeakPnLPct: peakPnlPct,
LiquidationPrice: liquidationPrice,
MarginUsed: marginUsed,
UpdateTime: updateTime,
})
}
// 清理已平仓的持仓记录
for key := range at.positionFirstSeenTime {
if !currentPositionKeys[key] {
delete(at.positionFirstSeenTime, key)
}
}
// 3. 使用策略引擎获取候选币种(必须有策略引擎)
if at.strategyEngine == nil {
return nil, fmt.Errorf("交易员未配置策略引擎")
}
candidateCoins, err := at.strategyEngine.GetCandidateCoins()
if err != nil {
return nil, fmt.Errorf("获取候选币种失败: %w", err)
}
logger.Infof("📋 [%s] 策略引擎获取候选币种: %d个", at.name, 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. 从策略配置获取杠杆
strategyConfig := at.strategyEngine.GetConfig()
btcEthLeverage := strategyConfig.RiskControl.BTCETHMaxLeverage
altcoinLeverage := strategyConfig.RiskControl.AltcoinMaxLeverage
logger.Infof("📋 [%s] 策略杠杆配置: BTC/ETH=%dx, 山寨币=%dx", at.name, btcEthLeverage, altcoinLeverage)
// 6. 构建上下文
ctx := &decision.Context{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
CallCount: at.callCount,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
Account: decision.AccountInfo{
TotalEquity: totalEquity,
AvailableBalance: availableBalance,
UnrealizedPnL: totalUnrealizedProfit,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
MarginUsed: totalMarginUsed,
MarginUsedPct: marginUsedPct,
PositionCount: len(positionInfos),
},
Positions: positionInfos,
CandidateCoins: candidateCoins,
}
// 7. 添加交易统计和历史订单如果store可用
if at.store != nil {
// 获取交易统计(使用新的 positions 表)
if stats, err := at.store.Position().GetFullStats(at.id); err == nil {
ctx.TradingStats = &decision.TradingStats{
TotalTrades: stats.TotalTrades,
WinRate: stats.WinRate,
ProfitFactor: stats.ProfitFactor,
SharpeRatio: stats.SharpeRatio,
TotalPnL: stats.TotalPnL,
AvgWin: stats.AvgWin,
AvgLoss: stats.AvgLoss,
MaxDrawdownPct: stats.MaxDrawdownPct,
}
}
// 获取最近10条已平仓交易使用新的 positions 表)
if recentTrades, err := at.store.Position().GetRecentTrades(at.id, 10); err == nil {
for _, trade := range recentTrades {
ctx.RecentOrders = append(ctx.RecentOrders, decision.RecentOrder{
Symbol: trade.Symbol,
Side: trade.Side,
EntryPrice: trade.EntryPrice,
ExitPrice: trade.ExitPrice,
RealizedPnL: trade.RealizedPnL,
PnLPct: trade.PnLPct,
FilledAt: trade.ExitTime,
})
}
}
}
// 8. 获取量化数据(如果策略配置启用)
if strategyConfig.Indicators.EnableQuantData && strategyConfig.Indicators.QuantDataAPIURL != "" {
// 收集需要查询的币种(候选币种 + 持仓币种)
symbolsToQuery := make(map[string]bool)
for _, coin := range candidateCoins {
symbolsToQuery[coin.Symbol] = true
}
for _, pos := range positionInfos {
symbolsToQuery[pos.Symbol] = true
}
symbols := make([]string, 0, len(symbolsToQuery))
for sym := range symbolsToQuery {
symbols = append(symbols, sym)
}
logger.Infof("📊 [%s] 正在获取 %d 个币种的量化数据...", at.name, len(symbols))
ctx.QuantDataMap = at.strategyEngine.FetchQuantDataBatch(symbols)
logger.Infof("📊 [%s] 成功获取 %d 个币种的量化数据", at.name, len(ctx.QuantDataMap))
}
return ctx, nil
}
// executeDecisionWithRecord 执行AI决策并记录详细信息
func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, actionRecord *store.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)
}
}
// executeOpenLongWithRecord 执行开多仓并记录详细信息
func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error {
logger.Infof(" 📈 开多仓: %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
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// ⚠️ 保证金验证防止保证金不足错误code=-2019
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)
}
// 设置仓位模式
if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil {
logger.Infof(" ⚠️ 设置仓位模式失败: %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
}
logger.Infof(" ✓ 开仓成功订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 记录订单到数据库并轮询确认
at.recordAndConfirmOrder(order, decision.Symbol, "open_long", quantity, marketData.CurrentPrice, decision.Leverage, 0)
// 记录开仓时间
posKey := decision.Symbol + "_long"
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil {
logger.Infof(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil {
logger.Infof(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeOpenShortWithRecord 执行开空仓并记录详细信息
func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error {
logger.Infof(" 📉 开空仓: %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
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// ⚠️ 保证金验证防止保证金不足错误code=-2019
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)
}
// 设置仓位模式
if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil {
logger.Infof(" ⚠️ 设置仓位模式失败: %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
}
logger.Infof(" ✓ 开仓成功订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 记录订单到数据库并轮询确认
at.recordAndConfirmOrder(order, decision.Symbol, "open_short", quantity, marketData.CurrentPrice, decision.Leverage, 0)
// 记录开仓时间
posKey := decision.Symbol + "_short"
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil {
logger.Infof(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil {
logger.Infof(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeCloseLongWithRecord 执行平多仓并记录详细信息
func (at *AutoTrader) executeCloseLongWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error {
logger.Infof(" 🔄 平多仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取开仓价格(用于计算盈亏)
var entryPrice float64
var quantity float64
if at.store != nil {
if openOrder, err := at.store.Order().GetLatestOpenOrder(at.id, decision.Symbol, "long"); err == nil {
entryPrice = openOrder.AvgPrice
quantity = openOrder.ExecutedQty
}
}
// 平仓
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
}
// 记录订单到数据库并轮询确认
at.recordAndConfirmOrder(order, decision.Symbol, "close_long", quantity, marketData.CurrentPrice, 0, entryPrice)
logger.Infof(" ✓ 平仓成功")
return nil
}
// executeCloseShortWithRecord 执行平空仓并记录详细信息
func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error {
logger.Infof(" 🔄 平空仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取开仓价格(用于计算盈亏)
var entryPrice float64
var quantity float64
if at.store != nil {
if openOrder, err := at.store.Order().GetLatestOpenOrder(at.id, decision.Symbol, "short"); err == nil {
entryPrice = openOrder.AvgPrice
quantity = openOrder.ExecutedQty
}
}
// 平仓
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
}
// 记录订单到数据库并轮询确认
at.recordAndConfirmOrder(order, decision.Symbol, "close_short", quantity, marketData.CurrentPrice, 0, entryPrice)
logger.Infof(" ✓ 平仓成功")
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
}
// GetSystemPromptTemplate 获取当前系统提示词模板名称(从策略配置获取)
func (at *AutoTrader) GetSystemPromptTemplate() string {
if at.strategyEngine != nil {
config := at.strategyEngine.GetConfig()
if config.CustomPrompt != "" {
return "custom"
}
}
return "strategy"
}
// saveDecision 保存决策记录到数据库
func (at *AutoTrader) saveDecision(record *store.DecisionRecord) error {
if at.store == nil {
return nil // 没有 store 时静默忽略
}
at.cycleNumber++
record.CycleNumber = at.cycleNumber
record.TraderID = at.id
if record.Timestamp.IsZero() {
record.Timestamp = time.Now().UTC()
}
if err := at.store.Decision().LogDecision(record); err != nil {
logger.Infof("⚠️ 保存决策记录失败: %v", err)
return err
}
logger.Infof("📝 决策记录已保存: trader=%s, cycle=%d", at.id, at.cycleNumber)
return nil
}
// GetStore 获取数据存储(用于外部访问决策记录等)
func (at *AutoTrader) GetStore() *store.Store {
return at.store
}
// 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
totalUnrealizedPnLCalculated := 0.0
for _, pos := range positions {
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
totalUnrealizedPnLCalculated += unrealizedPnl
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
totalMarginUsed += marginUsed
}
// 验证未实现盈亏的一致性API值 vs 从持仓计算)
diff := math.Abs(totalUnrealizedProfit - totalUnrealizedPnLCalculated)
if diff > 0.1 { // 允许0.01 USDT的误差
logger.Infof("⚠️ 未实现盈亏不一致: API=%.4f, 计算=%.4f, 差异=%.4f",
totalUnrealizedProfit, totalUnrealizedPnLCalculated, diff)
}
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
if at.initialBalance > 0 {
totalPnLPct = (totalPnL / at.initialBalance) * 100
} else {
logger.Infof("⚠️ Initial Balance异常: %.2f无法计算PNL百分比", at.initialBalance)
}
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, // 总盈亏百分比
"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)
// 计算盈亏百分比(基于保证金)
pnlPct := calculatePnLPercentage(unrealizedPnl, marginUsed)
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
}
// calculatePnLPercentage 计算盈亏百分比(基于保证金,自动考虑杠杆)
// 收益率 = 未实现盈亏 / 保证金 × 100%
func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 {
if marginUsed > 0 {
return (unrealizedPnl / marginUsed) * 100
}
return 0.0
}
// 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":
return 1 // 最高优先级:先平仓
case "open_long", "open_short":
return 2 // 次优先级:后开仓
case "hold", "wait":
return 3 // 最低优先级:观望
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
}
// 启动回撤监控
func (at *AutoTrader) startDrawdownMonitor() {
at.monitorWg.Add(1)
go func() {
defer at.monitorWg.Done()
ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次
defer ticker.Stop()
logger.Info("📊 启动持仓回撤监控(每分钟检查一次)")
for {
select {
case <-ticker.C:
at.checkPositionDrawdown()
case <-at.stopMonitorCh:
logger.Info("⏹ 停止持仓回撤监控")
return
}
}
}()
}
// 检查持仓回撤情况
func (at *AutoTrader) checkPositionDrawdown() {
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
logger.Infof("❌ 回撤监控:获取持仓失败: %v", err)
return
}
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 // 空仓数量为负,转为正数
}
// 计算当前盈亏百分比
leverage := 10 // 默认值
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
var currentPnLPct float64
if side == "long" {
currentPnLPct = ((markPrice - entryPrice) / entryPrice) * float64(leverage) * 100
} else {
currentPnLPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100
}
// 构造持仓唯一标识(区分多空)
posKey := symbol + "_" + side
// 获取该持仓的历史最高收益
at.peakPnLCacheMutex.RLock()
peakPnLPct, exists := at.peakPnLCache[posKey]
at.peakPnLCacheMutex.RUnlock()
if !exists {
// 如果没有历史最高记录,使用当前盈亏作为初始值
peakPnLPct = currentPnLPct
at.UpdatePeakPnL(symbol, side, currentPnLPct)
} else {
// 更新峰值缓存
at.UpdatePeakPnL(symbol, side, currentPnLPct)
}
// 计算回撤(从最高点下跌的幅度)
var drawdownPct float64
if peakPnLPct > 0 && currentPnLPct < peakPnLPct {
drawdownPct = ((peakPnLPct - currentPnLPct) / peakPnLPct) * 100
}
// 检查平仓条件收益大于5%且回撤超过40%
if currentPnLPct > 5.0 && drawdownPct >= 40.0 {
logger.Infof("🚨 触发回撤平仓条件: %s %s | 当前收益: %.2f%% | 最高收益: %.2f%% | 回撤: %.2f%%",
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)
// 执行平仓
if err := at.emergencyClosePosition(symbol, side); err != nil {
logger.Infof("❌ 回撤平仓失败 (%s %s): %v", symbol, side, err)
} else {
logger.Infof("✅ 回撤平仓成功: %s %s", symbol, side)
// 平仓后清理该持仓的缓存
at.ClearPeakPnLCache(symbol, side)
}
} else if currentPnLPct > 5.0 {
// 记录接近平仓条件的情况(用于调试)
logger.Infof("📊 回撤监控: %s %s | 收益: %.2f%% | 最高: %.2f%% | 回撤: %.2f%%",
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)
}
}
}
// 紧急平仓函数
func (at *AutoTrader) emergencyClosePosition(symbol, side string) error {
switch side {
case "long":
order, err := at.trader.CloseLong(symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
logger.Infof("✅ 紧急平多仓成功订单ID: %v", order["orderId"])
case "short":
order, err := at.trader.CloseShort(symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
logger.Infof("✅ 紧急平空仓成功订单ID: %v", order["orderId"])
default:
return fmt.Errorf("未知的持仓方向: %s", side)
}
return nil
}
// GetPeakPnLCache 获取最高收益缓存
func (at *AutoTrader) GetPeakPnLCache() map[string]float64 {
at.peakPnLCacheMutex.RLock()
defer at.peakPnLCacheMutex.RUnlock()
// 返回缓存的副本
cache := make(map[string]float64)
for k, v := range at.peakPnLCache {
cache[k] = v
}
return cache
}
// UpdatePeakPnL 更新最高收益缓存
func (at *AutoTrader) UpdatePeakPnL(symbol, side string, currentPnLPct float64) {
at.peakPnLCacheMutex.Lock()
defer at.peakPnLCacheMutex.Unlock()
posKey := symbol + "_" + side
if peak, exists := at.peakPnLCache[posKey]; exists {
// 更新峰值如果是多头取较大值如果是空头currentPnLPct为负也要比较
if currentPnLPct > peak {
at.peakPnLCache[posKey] = currentPnLPct
}
} else {
// 首次记录
at.peakPnLCache[posKey] = currentPnLPct
}
}
// ClearPeakPnLCache 清除指定持仓的峰值缓存
func (at *AutoTrader) ClearPeakPnLCache(symbol, side string) {
at.peakPnLCacheMutex.Lock()
defer at.peakPnLCacheMutex.Unlock()
posKey := symbol + "_" + side
delete(at.peakPnLCache, posKey)
}
// recordAndConfirmOrder 记录订单并轮询确认状态
// action: open_long, open_short, close_long, close_short
// entryPrice: 平仓时的开仓价开仓时为0
func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{}, symbol, action string, quantity float64, price float64, leverage int, entryPrice float64) {
if at.store == nil {
return
}
// 获取订单ID支持多种类型
var orderID string
switch v := orderResult["orderId"].(type) {
case int64:
orderID = fmt.Sprintf("%d", v)
case float64:
orderID = fmt.Sprintf("%.0f", v)
case string:
orderID = v
default:
orderID = fmt.Sprintf("%v", v)
}
if orderID == "" || orderID == "0" {
logger.Infof(" ⚠️ 订单ID为空跳过记录")
return
}
// 确定 side 和 positionSide
var side, positionSide string
switch action {
case "open_long":
side = "BUY"
positionSide = "LONG"
case "close_long":
side = "SELL"
positionSide = "LONG"
case "open_short":
side = "SELL"
positionSide = "SHORT"
case "close_short":
side = "BUY"
positionSide = "SHORT"
}
// 创建订单记录
order := &store.TraderOrder{
TraderID: at.id,
OrderID: orderID,
Symbol: symbol,
Side: side,
PositionSide: positionSide,
Action: action,
OrderType: "MARKET",
Quantity: quantity,
Price: price,
Leverage: leverage,
Status: "NEW",
EntryPrice: entryPrice,
}
// 保存到数据库
if err := at.store.Order().Create(order); err != nil {
logger.Infof(" ⚠️ 记录订单失败: %v", err)
return
}
logger.Infof(" 📝 订单已记录 (ID: %s, action: %s)", orderID, action)
// 记录仓位变化
at.recordPositionChange(orderID, symbol, positionSide, action, quantity, price, leverage, entryPrice)
}
// recordPositionChange 记录仓位变化(开仓创建记录,平仓更新记录)
func (at *AutoTrader) recordPositionChange(orderID, symbol, side, action string, quantity, price float64, leverage int, entryPrice float64) {
if at.store == nil {
return
}
switch action {
case "open_long", "open_short":
// 开仓:创建新的仓位记录
pos := &store.TraderPosition{
TraderID: at.id,
ExchangeID: at.exchange, // 记录具体的交易所ID
Symbol: symbol,
Side: side, // LONG or SHORT
Quantity: quantity,
EntryPrice: price,
EntryOrderID: orderID,
EntryTime: time.Now(),
Leverage: leverage,
Status: "OPEN",
}
if err := at.store.Position().Create(pos); err != nil {
logger.Infof(" ⚠️ 记录仓位失败: %v", err)
} else {
logger.Infof(" 📊 仓位已记录 [%s] %s %s @ %.4f", at.id[:8], symbol, side, price)
}
case "close_long", "close_short":
// 平仓:找到对应的开仓记录并更新
openPos, err := at.store.Position().GetOpenPositionBySymbol(at.id, symbol, side)
if err != nil || openPos == nil {
logger.Infof(" ⚠️ 找不到对应的开仓记录 (%s %s)", symbol, side)
return
}
// 计算盈亏
var realizedPnL float64
if side == "LONG" {
realizedPnL = (price - openPos.EntryPrice) * openPos.Quantity
} else {
realizedPnL = (openPos.EntryPrice - price) * openPos.Quantity
}
// 更新仓位记录
err = at.store.Position().ClosePosition(
openPos.ID,
price, // exitPrice
orderID, // exitOrderID
realizedPnL,
0, // fee (暂不计算)
"ai_decision",
)
if err != nil {
logger.Infof(" ⚠️ 更新仓位失败: %v", err)
} else {
logger.Infof(" 📊 仓位已平仓 [%s] %s %s @ %.4f → %.4f, PnL: %.2f",
at.id[:8], symbol, side, openPos.EntryPrice, price, realizedPnL)
}
}
}