Files
nofx/trader/auto_trader.go
0xYYBB | ZYY | Bobo 8dffff60a2 feat(lighter): 完整集成 LIGHTER DEX - SDK + 前端配置 UI (#1085)
* feat(trader): add LIGHTER DEX integration (initial implementation)

Add pure Go implementation of LIGHTER DEX trader following NOFX architecture

Features:
-  Account management with Ethereum wallet authentication
-  Order operations: market/limit orders, cancel, query
-  Position & balance queries
-  Zero-fee trading support (Standard accounts)
-  Up to 50x leverage for BTC/ETH

Implementation:
- Pure Go (no CGO dependencies) for easy deployment
- Based on hyperliquid_trader.go architecture
- Uses Ethereum ECDSA signatures (like Hyperliquid)
- API base URL: https://mainnet.zklighter.elliot.ai

Files:
- lighter_trader.go: Core trader structure & auth
- lighter_orders.go: Order management (create/cancel/query)
- lighter_account.go: Balance & position queries

Status: ⚠️ Partial implementation
-  Core structure complete
- ⏸️ Auth token generation needs implementation
- ⏸️ Transaction signing logic needs completion
- ⏸️ Config integration pending

Next steps:
1. Complete auth token generation
2. Add to config/exchange registry
3. Add frontend UI support
4. Create test suite

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* feat: Add LIGHTER DEX integration (快速整合階段)

## 🚀 新增功能
-  添加 LIGHTER DEX 作為第四個支持的交易所 (Binance, Hyperliquid, Aster, LIGHTER)
-  完整的數據庫配置支持(ExchangeConfig 新增 LighterWalletAddr, LighterPrivateKey 字段)
-  交易所註冊與初始化(initDefaultData 註冊 "lighter")
-  TraderManager 集成(配置傳遞邏輯完成)
-  AutoTrader 支持(NewAutoTrader 添加 "lighter" case)

## 📝 實現細節

### 後端整合
1. **數據庫層** (config/database.go):
   - ExchangeConfig 添加 LIGHTER 字段
   - 創建表時添加 lighter_wallet_addr, lighter_private_key 欄位
   - ALTER TABLE 語句用於向後兼容
   - UpdateExchange/CreateExchange/GetExchanges 支持 LIGHTER
   - migrateExchangesTable 支持 LIGHTER 字段

2. **API 層** (api/server.go, api/utils.go):
   - UpdateExchangeConfigRequest 添加 LIGHTER 字段
   - SanitizeExchangeConfigForLog 添加脫敏處理

3. **Trader 層** (trader/):
   - lighter_trader.go: 核心結構、認證、初始化
   - lighter_account.go: 餘額、持倉、市場價格查詢
   - lighter_orders.go: 訂單管理(創建、取消、查詢)
   - lighter_trading.go: 交易功能實現(開多/空、平倉、止損/盈)
   - 實現完整 Trader interface (13個方法)

4. **Manager 層** (manager/trader_manager.go):
   - addTraderFromDB 添加 LIGHTER 配置設置
   - AutoTraderConfig 添加 LIGHTER 字段

### 實現的功能(快速整合階段)
 基礎交易功能 (OpenLong, OpenShort, CloseLong, CloseShort)
 餘額查詢 (GetBalance, GetAccountBalance)
 持倉查詢 (GetPositions, GetPosition)
 訂單管理 (CreateOrder, CancelOrder, CancelAllOrders)
 止損/止盈 (SetStopLoss, SetTakeProfit, CancelStopLossOrders)
 市場數據 (GetMarketPrice)
 格式化工具 (FormatQuantity)

## ⚠️ TODO(完整實現階段)
- [ ] 完整認證令牌生成邏輯 (refreshAuthToken)
- [ ] 完整交易簽名邏輯(參考 Python SDK)
- [ ] 從 API 獲取幣種精度
- [ ] 區分止損/止盈訂單類型
- [ ] 前端 UI 支持
- [ ] 完整測試套件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* feat: 完整集成 LIGHTER DEX with SDK

- 集成官方 lighter-go SDK (v0.0.0-20251104171447-78b9b55ebc48)
- 集成 Poseidon2 Goldilocks 簽名庫 (poseidon_crypto v0.0.11)
- 實現完整的 LighterTraderV2 使用官方 SDK
- 實現 17 個 Trader 接口方法(賬戶、交易、訂單管理)
- 支持雙密鑰系統(L1 錢包 + API Key)
- V1/V2 自動切換機制(向後兼容)
- 自動認證令牌管理(8小時有效期)
- 添加完整集成文檔 LIGHTER_INTEGRATION.md

新增文件:
- trader/lighter_trader_v2.go - V2 核心結構和初始化
- trader/lighter_trader_v2_account.go - 賬戶查詢方法
- trader/lighter_trader_v2_trading.go - 交易操作方法
- trader/lighter_trader_v2_orders.go - 訂單管理方法
- LIGHTER_INTEGRATION.md - 完整文檔

修改文件:
- trader/auto_trader.go - 添加 LighterAPIKeyPrivateKey 配置
- config/database.go - 添加 API Key 字段支持
- go.mod, go.sum - 添加 SDK 依賴

🤖 Generated with Claude Code

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* feat(lighter): 實現完整 HTTP 調用與動態市場映射

### 實現的功能

#### 1. submitOrder() - 真實訂單提交
- 使用 POST /api/v1/sendTx 提交已簽名訂單
- tx_type: 14 (CREATE_ORDER)
- 價格保護機制 (price_protection)
- 完整錯誤處理與響應解析

#### 2. GetActiveOrders() - 查詢活躍訂單
- GET /api/v1/accountActiveOrders
- 使用認證令牌 (Authorization header)
- 支持按市場索引過濾

#### 3. CancelOrder() - 真實取消訂單
- 使用 SDK 簽名 CancelOrderTxReq
- POST /api/v1/sendTx with tx_type: 15 (CANCEL_ORDER)
- 自動 nonce 管理

#### 4. getMarketIndex() - 動態市場映射
- 從 GET /api/v1/orderBooks 獲取市場列表
- 內存緩存 (marketIndexMap) 提高性能
- 回退到硬編碼映射(API 失敗時)
- 線程安全 (sync.RWMutex)

### 技術實現

**數據結構**:
- SendTxRequest/SendTxResponse - sendTx 請求響應
- MarketInfo - 市場信息緩存

**並發安全**:
- marketMutex - 保護市場索引緩存
- 讀寫鎖優化性能

**錯誤處理**:
- API 失敗回退機制
- 詳細日誌記錄
- HTTP 狀態碼驗證

### 測試

 編譯通過 (CGO_ENABLED=1)
 所有 Trader 接口方法實現完整
 HTTP 調用格式符合 LIGHTER API 規範

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* feat(lighter): 數據庫遷移與前端類型支持

### 數據庫變更

#### 新增欄位
- `exchanges.lighter_api_key_private_key` TEXT DEFAULT ''
- 支持 LIGHTER V2 的 40 字節 API Key 私鑰

#### 遷移腳本
- 📄 `migrations/002_add_lighter_api_key.sql`
- 包含完整的驗證和統計查詢
- 向後兼容現有配置(默認為空,使用 V1)

#### Schema 更新
- `config/database.go`:
  - 更新 CREATE TABLE 語句
  - 更新 exchanges_new 表結構
  - 新增 ALTER TABLE 遷移命令

### 前端類型更新

#### types.ts
- 新增 `Exchange` 接口字段:
  - `lighterWalletAddr?: string` - L1 錢包地址
  - `lighterPrivateKey?: string` - L1 私鑰
  - `lighterApiKeyPrivateKey?: string` - API Key 私鑰(新增)

### 技術細節

**數據庫兼容性**:
- 使用 ALTER TABLE ADD COLUMN IF NOT EXISTS
- 默認值為空字符串
- 不影響現有數據

**類型安全**:
- TypeScript 可選字段
- 與後端 ExchangeConfig 結構對齊

### 下一步

 **待完成**:
1. ExchangeConfigModal 組件更新
2. API 調用參數傳遞
3. V1/V2 狀態顯示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* docs(lighter): 更新 LIGHTER_INTEGRATION.md 文檔狀態

* feat(lighter): 前端完整實現 - API Key 配置與 V1/V2 狀態

**英文**:
- `lighterWalletAddress`, `lighterPrivateKey`, `lighterApiKeyPrivateKey`
- `lighterWalletAddressDesc`, `lighterPrivateKeyDesc`, `lighterApiKeyPrivateKeyDesc`
- `lighterApiKeyOptionalNote` - V1 模式提示
- `lighterV1Description`, `lighterV2Description` - 狀態說明
- `lighterPrivateKeyImported` - 導入成功提示

**中文(繁體)**:
- 完整的中文翻譯對應
- 專業術語保留原文(L1、API Key、Poseidon2)

**Exchange 接口**:
- `lighterWalletAddr?: string`
- `lighterPrivateKey?: string`
- `lighterApiKeyPrivateKey?: string`

**UpdateExchangeConfigRequest 接口**:
- `lighter_wallet_addr?: string`
- `lighter_private_key?: string`
- `lighter_api_key_private_key?: string`

**狀態管理**:
- 添加 3 個 LIGHTER 狀態變量
- 更新 `secureInputTarget` 類型包含 'lighter'

**表單字段**:
- L1 錢包地址(必填,text input)
- L1 私鑰(必填,password + 安全輸入)
- API Key 私鑰(可選,password,40 字節)

**V1/V2 狀態顯示**:
- 動態背景顏色(V1: 橙色 #3F2E0F,V2: 綠色 #0F3F2E)
- 圖標指示(V1: ⚠️,V2: )
- 狀態說明文字

**驗證邏輯**:
- 必填字段:錢包地址 + L1 私鑰
- API Key 為可選字段
- 自動 V1/V2 檢測

**安全輸入**:
- 支持通過 TwoStageKeyModal 安全導入私鑰
- 導入成功後顯示 toast 提示

**handleSaveExchange**:
- 添加 3 個 LIGHTER 參數
- 更新交易所對象(新增/更新)
- 構建 API 請求(snake_case 字段)

**V1 模式(無 API Key)**:
```
┌────────────────────────────────────────┐
│ ⚠️ LIGHTER V1                          │
│ 基本模式 - 功能受限,僅用於測試框架       │
└────────────────────────────────────────┘
背景: #3F2E0F (橙色調)
邊框: #F59E0B (橙色)
```

**V2 模式(有 API Key)**:
```
┌────────────────────────────────────────┐
│  LIGHTER V2                          │
│ 完整模式 - 支持 Poseidon2 簽名和真實交易 │
└────────────────────────────────────────┘
背景: #0F3F2E (綠色調)
邊框: #10B981 (綠色)
```

1. **類型安全**
   - 完整的 TypeScript 類型定義
   - Props 接口正確對齊
   -  無 LIGHTER 相關編譯錯誤

2. **用戶體驗**
   - 清晰的必填/可選字段區分
   - 實時 V1/V2 狀態反饋
   - 安全私鑰輸入支持

3. **向後兼容**
   - 不影響現有交易所配置
   - 所有字段為可選(Optional)
   - API 請求格式統一

 TypeScript 編譯通過(無 LIGHTER 錯誤)
 類型定義完整且正確
 所有必需文件已更新
 與後端 API 格式對齊

Modified:
- `web/src/i18n/translations.ts` - 中英文翻譯
- `web/src/types.ts` - 類型定義
- `web/src/components/traders/ExchangeConfigModal.tsx` - Modal 組件
- `web/src/hooks/useTraderActions.ts` - Actions hook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* test(lighter): 添加 V1 測試套件與修復 SafeFloat64 缺失

- 新增 trader/helpers.go: 添加 SafeFloat64/SafeString/SafeInt 輔助函數
- 新增 trader/lighter_trader_test.go: LIGHTER V1 測試套件
  -  測試通過 (7/10):
    - NewTrader 驗證 (無效私鑰, 有效私鑰格式)
    - FormatQuantity
    - GetExchangeType
    - InvalidQuantity 驗證
    - InvalidLeverage 驗證
    - HelperFunctions (SafeFloat64)
  - ⚠️ 待改進 (3/10):
    - GetBalance (需要調整 mock 響應格式)
    - GetPositions (需要調整 mock 響應格式)
    - GetMarketPrice (需要調整 mock 響應格式)

- 修復 Bug: lighter_account.go 和 lighter_trader_v2_account.go 中未定義的 SafeFloat64
- 測試框架: httptest.Server mock LIGHTER API
- 安全: 使用固定測試私鑰 (不含真實資金)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

---------

Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com>
Co-authored-by: tinkle-community <tinklefund@gmail.com>
2025-11-20 19:29:01 +08:00

1683 lines
55 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/decision"
"nofx/logger"
"nofx/market"
"nofx/mcp"
"nofx/pool"
"strings"
"sync"
"time"
)
// AutoTraderConfig 自动交易配置(简化版 - AI全权决策
type AutoTraderConfig struct {
// Trader标识
ID string // Trader唯一标识用于日志目录等
Name string // Trader显示名称
AIModel string // AI模型: "qwen" 或 "deepseek"
// 交易平台选择
Exchange string // "binance", "hyperliquid", "aster" 或 "lighter"
// 币安API配置
BinanceAPIKey string
BinanceSecretKey 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
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"
}
// AutoTrader 自动交易器
type AutoTrader struct {
id string // Trader唯一标识
name string // Trader显示名称
aiModel string // AI模型名称
exchange string // 交易平台名称
config AutoTraderConfig
trader Trader // 使用Trader接口支持多平台
mcpClient mcp.AIClient
decisionLogger logger.IDecisionLogger // 决策日志记录器
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毫秒)
stopMonitorCh chan struct{} // 用于停止监控goroutine
monitorWg sync.WaitGroup // 用于等待监控goroutine结束
peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比)
peakPnLCacheMutex sync.RWMutex // 缓存读写锁
lastBalanceSyncTime time.Time // 上次余额同步时间
database interface{} // 数据库引用(用于自动更新余额)
userID string // 用户ID
}
// NewAutoTrader 创建自动交易器
func NewAutoTrader(config AutoTraderConfig, database interface{}, 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)
log.Printf("🤖 [%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 != "" {
log.Printf("🤖 [%s] 使用阿里云Qwen AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
} else {
log.Printf("🤖 [%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 != "" {
log.Printf("🤖 [%s] 使用DeepSeek AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
} else {
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
}
}
// 初始化币种池API
if config.CoinPoolAPIURL != "" {
pool.SetCoinPoolAPI(config.CoinPoolAPIURL)
}
// 设置默认交易平台
if config.Exchange == "" {
config.Exchange = "binance"
}
// 根据配置创建对应的交易器
var trader Trader
var err error
// 记录仓位模式(通用)
marginModeStr := "全仓"
if !config.IsCrossMargin {
marginModeStr = "逐仓"
}
log.Printf("📊 [%s] 仓位模式: %s", config.Name, marginModeStr)
switch config.Exchange {
case "binance":
log.Printf("🏦 [%s] 使用币安合约交易", config.Name)
trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey, userID)
case "hyperliquid":
log.Printf("🏦 [%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":
log.Printf("🏦 [%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":
log.Printf("🏦 [%s] 使用LIGHTER交易", config.Name)
// 優先使用 V2需要 API Key
if config.LighterAPIKeyPrivateKey != "" {
log.Printf("✓ 使用 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實現
log.Printf("⚠️ 使用 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)
}
// 验证初始金额配置
if config.InitialBalance <= 0 {
return nil, fmt.Errorf("初始金额必须大于0请在配置中设置InitialBalance")
}
// 初始化决策日志记录器使用trader ID创建独立目录
logDir := fmt.Sprintf("decision_logs/%s", config.ID)
decisionLogger := logger.NewDecisionLogger(logDir)
// 设置默认系统提示词模板
systemPromptTemplate := config.SystemPromptTemplate
if systemPromptTemplate == "" {
// feature/partial-close-dynamic-tpsl 分支默认使用 adaptive支持动态止盈止损
systemPromptTemplate = "adaptive"
}
return &AutoTrader{
id: config.ID,
name: config.Name,
aiModel: config.AIModel,
exchange: config.Exchange,
config: config,
trader: trader,
mcpClient: mcpClient,
decisionLogger: decisionLogger,
initialBalance: config.InitialBalance,
systemPromptTemplate: systemPromptTemplate,
defaultCoins: config.DefaultCoins,
tradingCoins: config.TradingCoins,
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(), // 初始化为当前时间
database: database,
userID: userID,
}, nil
}
// Run 运行自动交易主循环
func (at *AutoTrader) Run() error {
at.isRunning = true
at.stopMonitorCh = make(chan struct{})
at.startTime = time.Now()
log.Println("🚀 AI驱动自动交易系统启动")
log.Printf("💰 初始余额: %.2f USDT", at.initialBalance)
log.Printf("⚙️ 扫描间隔: %v", at.config.ScanInterval)
log.Println("🤖 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 {
log.Printf("❌ 执行失败: %v", err)
}
for at.isRunning {
select {
case <-ticker.C:
if err := at.runCycle(); err != nil {
log.Printf("❌ 执行失败: %v", err)
}
case <-at.stopMonitorCh:
log.Printf("[%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结束
log.Println("⏹ 自动交易系统停止")
}
// runCycle 运行一个交易周期使用AI全权决策
func (at *AutoTrader) runCycle() error {
at.callCount++
log.Print("\n" + strings.Repeat("=", 70) + "\n")
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
log.Println(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("📅 日盈亏已重置")
}
// 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)
}
// 保存账户状态快照
record.AccountState = logger.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, 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,
})
}
log.Print(strings.Repeat("=", 70))
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)
// 5. 调用AI获取完整决策
log.Printf("🤖 正在请求AI分析并决策... [模板: %s]", at.systemPromptTemplate)
decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt, at.systemPromptTemplate)
if decision != nil && decision.AIRequestDurationMs > 0 {
record.AIRequestDurationMs = decision.AIRequestDurationMs
log.Printf("⏱️ AI调用耗时: %.2f 秒", float64(record.AIRequestDurationMs)/1000)
record.ExecutionLog = append(record.ExecutionLog,
fmt.Sprintf("AI调用耗时: %d ms", record.AIRequestDurationMs))
}
// 即使有错误也保存思维链、决策和输入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 {
log.Print("\n" + strings.Repeat("=", 70) + "\n")
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
log.Println(strings.Repeat("=", 70))
log.Println(decision.SystemPrompt)
log.Println(strings.Repeat("=", 70))
if decision.CoTTrace != "" {
log.Print("\n" + strings.Repeat("-", 70) + "\n")
log.Println("💭 AI思维链分析错误情况:")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Println(strings.Repeat("-", 70))
}
}
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()
log.Print(strings.Repeat("-", 70))
// 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
log.Print(strings.Repeat("-", 70))
// 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. 保存决策记录
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)
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. 获取交易员的候选币种池
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. 构建上下文
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,
UnrealizedPnL: totalUnrealizedProfit,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
MarginUsed: totalMarginUsed,
MarginUsedPct: marginUsedPct,
PositionCount: len(positionInfos),
},
Positions: positionInfos,
CandidateCoins: candidateCoins,
Performance: performance, // 添加历史表现分析
}
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
}
// 计算数量
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 {
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
}
// 计算数量
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 {
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)
}
// ⚠️ 防御性检查:检测是否存在双向持仓(不应该出现,但提供保护)
var hasOppositePosition bool
oppositeSide := ""
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posSide, _ := pos["side"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 && strings.ToUpper(posSide) != positionSide {
hasOppositePosition = true
oppositeSide = strings.ToUpper(posSide)
break
}
}
if hasOppositePosition {
log.Printf(" 🚨 警告:检测到 %s 存在双向持仓(%s + %s这违反了策略规则",
decision.Symbol, positionSide, oppositeSide)
log.Printf(" 🚨 取消止损单将影响两个方向的订单,请检查是否为用户手动操作导致")
log.Printf(" 🚨 建议手动平掉其中一个方向的持仓或检查系统是否有BUG")
}
// 取消旧的止损单(只删除止损单,不影响止盈单)
// 注意:如果存在双向持仓,这会删除两个方向的止损单
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)
}
// ⚠️ 防御性检查:检测是否存在双向持仓(不应该出现,但提供保护)
var hasOppositePosition bool
oppositeSide := ""
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posSide, _ := pos["side"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 && strings.ToUpper(posSide) != positionSide {
hasOppositePosition = true
oppositeSide = strings.ToUpper(posSide)
break
}
}
if hasOppositePosition {
log.Printf(" 🚨 警告:检测到 %s 存在双向持仓(%s + %s这违反了策略规则",
decision.Symbol, positionSide, oppositeSide)
log.Printf(" 🚨 取消止盈单将影响两个方向的订单,请检查是否为用户手动操作导致")
log.Printf(" 🚨 建议手动平掉其中一个方向的持仓或检查系统是否有BUG")
}
// 取消旧的止盈单(只删除止盈单,不影响止损单)
// 注意:如果存在双向持仓,这会删除两个方向的止盈单
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, ok := targetPosition["markPrice"].(float64)
if !ok || markPrice <= 0 {
return fmt.Errorf("无法解析当前价格,无法执行最小仓位检查")
}
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)
// ✅ Step 4: 恢复止盈止损(防止剩余仓位裸奔)
// 重要:币安等交易所在部分平仓后会自动取消原有的 TP/SL 订单(因为数量不匹配)
// 如果 AI 提供了新的止损止盈价格,则为剩余仓位重新设置保护
if decision.NewStopLoss > 0 {
log.Printf(" → 为剩余仓位 %.4f 恢复止损单: %.2f", remainingQuantity, decision.NewStopLoss)
err = at.trader.SetStopLoss(decision.Symbol, positionSide, remainingQuantity, decision.NewStopLoss)
if err != nil {
log.Printf(" ⚠️ 恢复止损失败: %v不影响平仓结果", err)
}
}
if decision.NewTakeProfit > 0 {
log.Printf(" → 为剩余仓位 %.4f 恢复止盈单: %.2f", remainingQuantity, decision.NewTakeProfit)
err = at.trader.SetTakeProfit(decision.Symbol, positionSide, remainingQuantity, decision.NewTakeProfit)
if err != nil {
log.Printf(" ⚠️ 恢复止盈失败: %v不影响平仓结果", err)
}
}
// 如果 AI 没有提供新的止盈止损,记录警告
if decision.NewStopLoss <= 0 && decision.NewTakeProfit <= 0 {
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.IDecisionLogger {
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
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的误差
log.Printf("⚠️ 未实现盈亏不一致: API=%.4f, 计算=%.4f, 差异=%.4f",
totalUnrealizedProfit, totalUnrealizedPnLCalculated, diff)
}
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
if at.initialBalance > 0 {
totalPnLPct = (totalPnL / at.initialBalance) * 100
} else {
log.Printf("⚠️ 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", "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
}
// 启动回撤监控
func (at *AutoTrader) startDrawdownMonitor() {
at.monitorWg.Add(1)
go func() {
defer at.monitorWg.Done()
ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次
defer ticker.Stop()
log.Println("📊 启动持仓回撤监控(每分钟检查一次)")
for {
select {
case <-ticker.C:
at.checkPositionDrawdown()
case <-at.stopMonitorCh:
log.Println("⏹ 停止持仓回撤监控")
return
}
}
}()
}
// 检查持仓回撤情况
func (at *AutoTrader) checkPositionDrawdown() {
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
log.Printf("❌ 回撤监控:获取持仓失败: %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 {
log.Printf("🚨 触发回撤平仓条件: %s %s | 当前收益: %.2f%% | 最高收益: %.2f%% | 回撤: %.2f%%",
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)
// 执行平仓
if err := at.emergencyClosePosition(symbol, side); err != nil {
log.Printf("❌ 回撤平仓失败 (%s %s): %v", symbol, side, err)
} else {
log.Printf("✅ 回撤平仓成功: %s %s", symbol, side)
// 平仓后清理该持仓的缓存
at.ClearPeakPnLCache(symbol, side)
}
} else if currentPnLPct > 5.0 {
// 记录接近平仓条件的情况(用于调试)
log.Printf("📊 回撤监控: %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
}
log.Printf("✅ 紧急平多仓成功订单ID: %v", order["orderId"])
case "short":
order, err := at.trader.CloseShort(symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
log.Printf("✅ 紧急平空仓成功订单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)
}