feat: implement hybrid database architecture and frontend encryption

- Add PostgreSQL + SQLite hybrid database support with automatic switching
- Implement frontend AES-GCM + RSA-OAEP encryption for sensitive data
- Add comprehensive DatabaseInterface with all required methods
- Fix compilation issues with interface consistency
- Update all database method signatures to use DatabaseInterface
- Add missing UpdateTraderInitialBalance method to PostgreSQL implementation
- Integrate RSA public key distribution via /api/config endpoint
- Add frontend crypto service with proper error handling
- Support graceful degradation between encrypted and plaintext transmission
- Add directory creation for RSA keys and PEM parsing fixes
- Test both SQLite and PostgreSQL modes successfully

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

Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
icy
2025-11-06 01:50:06 +08:00
parent aecc5e58a1
commit 65053518d6
104 changed files with 16864 additions and 4152 deletions

View File

@@ -4,12 +4,14 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"nofx/decision"
"nofx/logger"
"nofx/market"
"nofx/mcp"
"nofx/pool"
"strings"
"sync"
"time"
)
@@ -98,10 +100,17 @@ type AutoTrader struct {
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) (*AutoTrader, error) {
func NewAutoTrader(config AutoTraderConfig, database interface{}, userID string) (*AutoTrader, error) {
// 设置默认值
if config.ID == "" {
config.ID = "default_trader"
@@ -195,7 +204,8 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
// 设置默认系统提示词模板
systemPromptTemplate := config.SystemPromptTemplate
if systemPromptTemplate == "" {
systemPromptTemplate = "default" // 默认使用 default 模板
// feature/partial-close-dynamic-tpsl 分支默认使用 adaptive支持动态止盈止损
systemPromptTemplate = "adaptive"
}
return &AutoTrader{
@@ -216,6 +226,13 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
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
}
@@ -227,6 +244,9 @@ func (at *AutoTrader) Run() error {
log.Printf("⚙️ 扫描间隔: %v", at.config.ScanInterval)
log.Println("🤖 AI将全权决定杠杆、仓位大小、止损止盈等参数")
// 启动回撤监控
at.startDrawdownMonitor()
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
@@ -250,16 +270,113 @@ func (at *AutoTrader) Run() error {
// Stop 停止自动交易
func (at *AutoTrader) Stop() {
at.isRunning = false
close(at.stopMonitorCh) // 通知监控goroutine停止
at.monitorWg.Wait() // 等待监控goroutine结束
log.Println("⏹ 自动交易系统停止")
}
// autoSyncBalanceIfNeeded 自动同步余额每10分钟检查一次变化>5%才更新)
func (at *AutoTrader) autoSyncBalanceIfNeeded() {
// 距离上次同步不足10分钟跳过
if time.Since(at.lastBalanceSyncTime) < 10*time.Minute {
return
}
log.Printf("🔄 [%s] 开始自动检查余额变化...", at.name)
// 查询实际余额
balanceInfo, err := at.trader.GetBalance()
if err != nil {
log.Printf("⚠️ [%s] 查询余额失败: %v", at.name, err)
at.lastBalanceSyncTime = time.Now() // 即使失败也更新时间,避免频繁重试
return
}
// 提取可用余额
var actualBalance float64
if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
actualBalance = totalBalance
} else {
log.Printf("⚠️ [%s] 无法提取可用余额", at.name)
at.lastBalanceSyncTime = time.Now()
return
}
oldBalance := at.initialBalance
// 防止除以零:如果初始余额无效,直接更新为实际余额
if oldBalance <= 0 {
log.Printf("⚠️ [%s] 初始余额无效 (%.2f),直接更新为实际余额 %.2f USDT", at.name, oldBalance, actualBalance)
at.initialBalance = actualBalance
if at.database != nil {
type DatabaseUpdater interface {
UpdateTraderInitialBalance(userID, id string, newBalance float64) error
}
if db, ok := at.database.(DatabaseUpdater); ok {
if err := db.UpdateTraderInitialBalance(at.userID, at.id, actualBalance); err != nil {
log.Printf("❌ [%s] 更新数据库失败: %v", at.name, err)
} else {
log.Printf("✅ [%s] 已自动同步余额到数据库", at.name)
}
} else {
log.Printf("⚠️ [%s] 数据库类型不支持UpdateTraderInitialBalance接口", at.name)
}
} else {
log.Printf("⚠️ [%s] 数据库引用为空,余额仅在内存中更新", at.name)
}
at.lastBalanceSyncTime = time.Now()
return
}
changePercent := ((actualBalance - oldBalance) / oldBalance) * 100
// 变化超过5%才更新
if math.Abs(changePercent) > 5.0 {
log.Printf("🔔 [%s] 检测到余额大幅变化: %.2f → %.2f USDT (%.2f%%)",
at.name, oldBalance, actualBalance, changePercent)
// 更新内存中的 initialBalance
at.initialBalance = actualBalance
// 更新数据库(需要类型断言)
if at.database != nil {
// 这里需要根据实际的数据库类型进行类型断言
// 由于使用了 interface{},我们需要在 TraderManager 层面处理更新
// 或者在这里进行类型检查
type DatabaseUpdater interface {
UpdateTraderInitialBalance(userID, id string, newBalance float64) error
}
if db, ok := at.database.(DatabaseUpdater); ok {
err := db.UpdateTraderInitialBalance(at.userID, at.id, actualBalance)
if err != nil {
log.Printf("❌ [%s] 更新数据库失败: %v", at.name, err)
} else {
log.Printf("✅ [%s] 已自动同步余额到数据库", at.name)
}
} else {
log.Printf("⚠️ [%s] 数据库类型不支持UpdateTraderInitialBalance接口", at.name)
}
} else {
log.Printf("⚠️ [%s] 数据库引用为空,余额仅在内存中更新", at.name)
}
} else {
log.Printf("✓ [%s] 余额变化不大 (%.2f%%),无需更新", at.name, changePercent)
}
at.lastBalanceSyncTime = time.Now()
}
// runCycle 运行一个交易周期使用AI全权决策
func (at *AutoTrader) runCycle() error {
at.callCount++
log.Print("\n" + strings.Repeat("=", 70))
log.Print("\n" + strings.Repeat("=", 70) + "\n")
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
log.Print(strings.Repeat("=", 70))
log.Println(strings.Repeat("=", 70))
// 创建决策记录
record := &logger.DecisionRecord{
@@ -284,7 +401,10 @@ func (at *AutoTrader) runCycle() error {
log.Println("📅 日盈亏已重置")
}
// 3. 收集交易上下文
// 3. 自动同步余额每10分钟检查一次充值/提现后自动更新)
at.autoSyncBalanceIfNeeded()
// 4. 收集交易上下文
ctx, err := at.buildTradingContext()
if err != nil {
record.Success = false
@@ -316,7 +436,7 @@ func (at *AutoTrader) runCycle() error {
})
}
// 保存候选币种列表
log.Print(strings.Repeat("=", 70))
for _, coin := range ctx.CandidateCoins {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
}
@@ -324,7 +444,7 @@ func (at *AutoTrader) runCycle() error {
log.Printf("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d",
ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount)
// 4. 调用AI获取完整决策
// 5. 调用AI获取完整决策
log.Printf("🤖 正在请求AI分析并决策... [模板: %s]", at.systemPromptTemplate)
decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt, at.systemPromptTemplate)
@@ -345,20 +465,18 @@ func (at *AutoTrader) runCycle() error {
// 打印系统提示词和AI思维链即使有错误也要输出以便调试
if decision != nil {
if decision.SystemPrompt != "" {
log.Print("\n" + strings.Repeat("=", 70))
log.Print("\n" + strings.Repeat("=", 70) + "\n")
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
log.Println(strings.Repeat("=", 70))
log.Println(decision.SystemPrompt)
log.Print(strings.Repeat("=", 70) + "\n")
}
log.Println(strings.Repeat("=", 70))
if decision.CoTTrace != "" {
log.Print("\n" + strings.Repeat("-", 70))
log.Print("\n" + strings.Repeat("-", 70) + "\n")
log.Println("💭 AI思维链分析错误情况:")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Print(strings.Repeat("-", 70) + "\n")
log.Println(strings.Repeat("-", 70))
}
}
@@ -383,13 +501,16 @@ func (at *AutoTrader) runCycle() error {
// 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.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)
@@ -481,6 +602,12 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
if quantity < 0 {
quantity = -quantity // 空仓数量为负,转为正数
}
// 跳过已平仓的持仓quantity = 0防止"幽灵持仓"传递给AI
if quantity == 0 {
continue
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
liquidationPrice := pos["liquidationPrice"].(float64)
@@ -593,6 +720,12 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, act
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
@@ -626,6 +759,27 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, act
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)
@@ -685,6 +839,27 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, ac
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)
@@ -771,6 +946,201 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, a
return nil
}
// executeUpdateStopLossWithRecord 执行调整止损并记录详细信息
func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🎯 调整止损: %s → %.2f", decision.Symbol, decision.NewStopLoss)
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
return fmt.Errorf("获取持仓失败: %w", err)
}
// 查找目标持仓
var targetPosition map[string]interface{}
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 {
targetPosition = pos
break
}
}
if targetPosition == nil {
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
}
// 获取持仓方向和数量
side, _ := targetPosition["side"].(string)
positionSide := strings.ToUpper(side)
positionAmt, _ := targetPosition["positionAmt"].(float64)
// 验证新止损价格合理性
if positionSide == "LONG" && decision.NewStopLoss >= marketData.CurrentPrice {
return fmt.Errorf("多单止损必须低于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
}
if positionSide == "SHORT" && decision.NewStopLoss <= marketData.CurrentPrice {
return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss)
}
// 取消旧的止损单(避免多个止损单共存)
if err := at.trader.CancelStopOrders(decision.Symbol); err != nil {
log.Printf(" ⚠ 取消旧止损单失败: %v", err)
// 不中断执行,继续设置新止损
}
// 调用交易所 API 修改止损
quantity := math.Abs(positionAmt)
err = at.trader.SetStopLoss(decision.Symbol, positionSide, quantity, decision.NewStopLoss)
if err != nil {
return fmt.Errorf("修改止损失败: %w", err)
}
log.Printf(" ✓ 止损已调整: %.2f (当前价格: %.2f)", decision.NewStopLoss, marketData.CurrentPrice)
return nil
}
// executeUpdateTakeProfitWithRecord 执行调整止盈并记录详细信息
func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🎯 调整止盈: %s → %.2f", decision.Symbol, decision.NewTakeProfit)
// 获取当前价格
marketData, err := market.Get(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 获取当前持仓
positions, err := at.trader.GetPositions()
if err != nil {
return fmt.Errorf("获取持仓失败: %w", err)
}
// 查找目标持仓
var targetPosition map[string]interface{}
for _, pos := range positions {
symbol, _ := pos["symbol"].(string)
posAmt, _ := pos["positionAmt"].(float64)
if symbol == decision.Symbol && posAmt != 0 {
targetPosition = pos
break
}
}
if targetPosition == nil {
return fmt.Errorf("持仓不存在: %s", decision.Symbol)
}
// 获取持仓方向和数量
side, _ := targetPosition["side"].(string)
positionSide := strings.ToUpper(side)
positionAmt, _ := targetPosition["positionAmt"].(float64)
// 验证新止盈价格合理性
if positionSide == "LONG" && decision.NewTakeProfit <= marketData.CurrentPrice {
return fmt.Errorf("多单止盈必须高于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
}
if positionSide == "SHORT" && decision.NewTakeProfit >= marketData.CurrentPrice {
return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit)
}
// 取消旧的止盈单(避免多个止盈单共存)
if err := at.trader.CancelStopOrders(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
// 执行平仓
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
}
remainingQuantity := totalQuantity - closeQuantity
log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f",
closeQuantity, decision.ClosePercentage, remainingQuantity)
return nil
}
// GetID 获取trader ID
func (at *AutoTrader) GetID() string {
return at.id
@@ -984,12 +1354,14 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision
// 定义优先级
getActionPriority := func(action string) int {
switch action {
case "close_long", "close_short":
return 1 // 最高优先级:先平仓
case "close_long", "close_short", "partial_close":
return 1 // 最高优先级:先平仓(包括部分平仓)
case "update_stop_loss", "update_take_profit":
return 2 // 调整持仓止盈止损
case "open_long", "open_short":
return 2 // 次优先级:后开仓
return 3 // 次优先级:后开仓
case "hold", "wait":
return 3 // 最低优先级:观望
return 4 // 最低优先级:观望
default:
return 999 // 未知动作放最后
}
@@ -1081,3 +1453,158 @@ func normalizeSymbol(symbol string) string {
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
}
// 获取该持仓的历史最高收益
at.peakPnLCacheMutex.RLock()
peakPnLPct, exists := at.peakPnLCache[symbol]
at.peakPnLCacheMutex.RUnlock()
if !exists {
// 如果没有历史最高记录,使用当前盈亏作为初始值
peakPnLPct = currentPnLPct
at.UpdatePeakPnL(symbol, currentPnLPct)
} else {
// 更新峰值缓存
at.UpdatePeakPnL(symbol, 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)
// 平仓后清理该symbol的缓存
at.ClearPeakPnLCache(symbol)
}
} 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 string, currentPnLPct float64) {
at.peakPnLCacheMutex.Lock()
defer at.peakPnLCacheMutex.Unlock()
if peak, exists := at.peakPnLCache[symbol]; exists {
// 更新峰值如果是多头取较大值如果是空头currentPnLPct为负也要比较
if currentPnLPct > peak {
at.peakPnLCache[symbol] = currentPnLPct
}
} else {
// 首次记录
at.peakPnLCache[symbol] = currentPnLPct
}
}
// ClearPeakPnLCache 清除指定symbol的峰值缓存
func (at *AutoTrader) ClearPeakPnLCache(symbol string) {
at.peakPnLCacheMutex.Lock()
defer at.peakPnLCacheMutex.Unlock()
delete(at.peakPnLCache, symbol)
}