Files
nofx/market/ai_decision_engine.go
tinkle-community beaea70fbc Improve: Optimize AI decision engine and position management
- Add focused position strategy (max 3 concurrent positions)
- Increase position sizes for better capital efficiency
- Add historical reflection prompts for AI learning
- Fix JSON parsing with missing quotes in reasoning field
- Update position limits and leverage recommendations
- Enhance risk management with concentrated positions
2025-10-28 22:40:34 +08:00

680 lines
26 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 market
import (
"encoding/json"
"fmt"
"log"
"nofx/pool"
"regexp"
"strings"
"time"
)
// PositionInfo 持仓信息
type PositionInfo struct {
Symbol string `json:"symbol"`
Side string `json:"side"` // "long" or "short"
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
Quantity float64 `json:"quantity"`
Leverage int `json:"leverage"`
UnrealizedPnL float64 `json:"unrealized_pnl"`
UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"`
LiquidationPrice float64 `json:"liquidation_price"`
MarginUsed float64 `json:"margin_used"`
}
// AccountInfo 账户信息
type AccountInfo struct {
TotalEquity float64 `json:"total_equity"` // 账户净值
AvailableBalance float64 `json:"available_balance"` // 可用余额
TotalPnL float64 `json:"total_pnl"` // 总盈亏
TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比
MarginUsed float64 `json:"margin_used"` // 已用保证金
MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率
PositionCount int `json:"position_count"` // 持仓数量
}
// CandidateCoin 候选币种(来自币种池)
type CandidateCoin struct {
Symbol string `json:"symbol"`
Sources []string `json:"sources"` // 来源: "ai500" 和/或 "oi_top"
}
// OITopData 持仓量增长Top数据用于AI决策参考
type OITopData struct {
Rank int // OI Top排名
OIDeltaPercent float64 // 持仓量变化百分比1小时
OIDeltaValue float64 // 持仓量变化价值
PriceDeltaPercent float64 // 价格变化百分比
NetLong float64 // 净多仓
NetShort float64 // 净空仓
}
// TradingContext 交易上下文传递给AI的完整信息
type TradingContext struct {
CurrentTime string `json:"current_time"`
RuntimeMinutes int `json:"runtime_minutes"`
CallCount int `json:"call_count"`
Account AccountInfo `json:"account"`
Positions []PositionInfo `json:"positions"`
CandidateCoins []CandidateCoin `json:"candidate_coins"`
MarketDataMap map[string]*MarketData `json:"-"` // 不序列化,但内部使用
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
Performance interface{} `json:"-"` // 历史表现分析logger.PerformanceAnalysis
}
// TradingDecision AI的交易决策
type TradingDecision struct {
Symbol string `json:"symbol"`
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait"
Leverage int `json:"leverage,omitempty"`
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
StopLoss float64 `json:"stop_loss,omitempty"`
TakeProfit float64 `json:"take_profit,omitempty"`
Reasoning string `json:"reasoning"`
}
// AIFullDecision AI的完整决策包含思维链
type AIFullDecision struct {
CoTTrace string `json:"cot_trace"` // 思维链分析
Decisions []TradingDecision `json:"decisions"` // 具体决策列表
Timestamp time.Time `json:"timestamp"`
}
// GetFullTradingDecision 获取AI的完整交易决策批量分析所有币种和持仓
func GetFullTradingDecision(ctx *TradingContext) (*AIFullDecision, error) {
// 1. 为所有币种获取市场数据
if err := fetchMarketDataForContext(ctx); err != nil {
return nil, fmt.Errorf("获取市场数据失败: %w", err)
}
// 2. 构建AI提示
prompt := buildFullDecisionPrompt(ctx)
// 3. 调用AI API
aiResponse, err := callDeepSeekAPI(prompt)
if err != nil {
return nil, fmt.Errorf("调用AI API失败: %w", err)
}
// 4. 解析AI响应
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity)
if err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
decision.Timestamp = time.Now()
return decision, nil
}
// fetchMarketDataForContext 为上下文中的所有币种获取市场数据和OI数据
func fetchMarketDataForContext(ctx *TradingContext) error {
ctx.MarketDataMap = make(map[string]*MarketData)
ctx.OITopDataMap = make(map[string]*OITopData)
// 收集所有需要获取数据的币种
symbolSet := make(map[string]bool)
// 1. 优先获取持仓币种的数据(这是必须的)
for _, pos := range ctx.Positions {
symbolSet[pos.Symbol] = true
}
// 2. 候选币种数量根据账户状态动态调整
maxCandidates := calculateMaxCandidates(ctx)
for i, coin := range ctx.CandidateCoins {
if i >= maxCandidates {
break
}
symbolSet[coin.Symbol] = true
}
// 并发获取市场数据
// 持仓币种集合用于判断是否跳过OI检查
positionSymbols := make(map[string]bool)
for _, pos := range ctx.Positions {
positionSymbols[pos.Symbol] = true
}
for symbol := range symbolSet {
data, err := GetMarketData(symbol)
if err != nil {
// 单个币种失败不影响整体,只记录错误
continue
}
// ⚠️ 流动性过滤持仓价值低于15M USD的币种不做多空都不做
// 持仓价值 = 持仓量 × 当前价格
// 但现有持仓必须保留(需要决策是否平仓)
isExistingPosition := positionSymbols[symbol]
if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 {
// 计算持仓价值USD= 持仓量 × 当前价格
oiValue := data.OpenInterest.Latest * data.CurrentPrice
oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位
if oiValueInMillions < 15 {
log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < 15M),跳过此币种 [持仓量:%.0f × 价格:%.4f]",
symbol, oiValueInMillions, data.OpenInterest.Latest, data.CurrentPrice)
continue
}
}
ctx.MarketDataMap[symbol] = data
}
// 加载OI Top数据不影响主流程
oiPositions, err := pool.GetOITopPositions()
if err == nil {
for _, pos := range oiPositions {
// 标准化符号匹配
symbol := pos.Symbol
ctx.OITopDataMap[symbol] = &OITopData{
Rank: pos.Rank,
OIDeltaPercent: pos.OIDeltaPercent,
OIDeltaValue: pos.OIDeltaValue,
PriceDeltaPercent: pos.PriceDeltaPercent,
NetLong: pos.NetLong,
NetShort: pos.NetShort,
}
}
}
return nil
}
// calculateMaxCandidates 根据账户状态计算需要分析的候选币种数量
func calculateMaxCandidates(ctx *TradingContext) int {
// 直接返回候选池的全部币种数量
// 因为候选池已经在 auto_trader.go 中筛选过了
// 固定分析前20个评分最高的币种来自AI500
return len(ctx.CandidateCoins)
}
// buildFullDecisionPrompt 构建完整的AI决策提示
func buildFullDecisionPrompt(ctx *TradingContext) string {
var sb strings.Builder
sb.WriteString("# 🤖 加密货币交易AI竞赛系统\n\n")
sb.WriteString("你是专业的加密货币交易AI根据市场数据自主决策做多做空均可。\n\n")
// 添加BTC市场趋势
sb.WriteString("## 🌍 BTC市场趋势\n")
if btcData, hasBTC := ctx.MarketDataMap["BTCUSDT"]; hasBTC {
sb.WriteString(fmt.Sprintf("- 价格: %.2f | 1h: %+.2f%% | 4h: %+.2f%%\n",
btcData.CurrentPrice, btcData.PriceChange1h, btcData.PriceChange4h))
sb.WriteString(fmt.Sprintf("- MACD: %.4f | RSI: %.2f | 资金费率: %.6f\n\n",
btcData.CurrentMACD, btcData.CurrentRSI7, btcData.FundingRate))
} else {
sb.WriteString("BTC数据暂无\n\n")
}
// 系统状态
sb.WriteString("## 📊 系统状态\n")
sb.WriteString(fmt.Sprintf("- **当前时间**: %s\n", ctx.CurrentTime))
sb.WriteString(fmt.Sprintf("- **运行时长**: %d 分钟\n", ctx.RuntimeMinutes))
sb.WriteString(fmt.Sprintf("- **调用次数**: 第 %d 次\n\n", ctx.CallCount))
// 账户信息
sb.WriteString("## 💰 账户信息\n")
sb.WriteString(fmt.Sprintf("- **账户净值**: %.2f USDT\n", ctx.Account.TotalEquity))
sb.WriteString(fmt.Sprintf("- **可用余额**: %.2f USDT (%.1f%%)\n",
ctx.Account.AvailableBalance,
(ctx.Account.AvailableBalance/ctx.Account.TotalEquity)*100))
sb.WriteString(fmt.Sprintf("- **总盈亏**: %.2f USDT (%+.2f%%)\n",
ctx.Account.TotalPnL, ctx.Account.TotalPnLPct))
sb.WriteString(fmt.Sprintf("- **已用保证金**: %.2f USDT (%.1f%%)\n",
ctx.Account.MarginUsed, ctx.Account.MarginUsedPct))
sb.WriteString(fmt.Sprintf("- **持仓数量**: %d\n\n", ctx.Account.PositionCount))
// 当前持仓详情
if len(ctx.Positions) > 0 {
sb.WriteString("## 📈 当前持仓\n")
for i, pos := range ctx.Positions {
sb.WriteString(fmt.Sprintf("\n### 持仓 #%d: %s %s\n", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
sb.WriteString(fmt.Sprintf("- **入场价**: %.4f USDT\n", pos.EntryPrice))
sb.WriteString(fmt.Sprintf("- **当前价**: %.4f USDT\n", pos.MarkPrice))
sb.WriteString(fmt.Sprintf("- **数量**: %.4f\n", pos.Quantity))
sb.WriteString(fmt.Sprintf("- **杠杆**: %dx\n", pos.Leverage))
sb.WriteString(fmt.Sprintf("- **未实现盈亏**: %.2f USDT (%+.2f%%)\n",
pos.UnrealizedPnL, pos.UnrealizedPnLPct))
sb.WriteString(fmt.Sprintf("- **强平价**: %.4f USDT\n", pos.LiquidationPrice))
sb.WriteString(fmt.Sprintf("- **占用保证金**: %.2f USDT\n", pos.MarginUsed))
// 添加市场数据
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(formatMarketDataBrief(marketData))
}
}
sb.WriteString("\n")
} else {
sb.WriteString("## 📈 当前持仓\n")
sb.WriteString("暂无持仓\n\n")
}
// 候选币种池
sb.WriteString("## 🎯 候选币种池\n")
sb.WriteString(fmt.Sprintf("共 %d 个币种(已过滤持仓价值<15M USD的低流动性币种\n\n", len(ctx.MarketDataMap)))
displayedCount := 0
for _, coin := range ctx.CandidateCoins {
// 只显示已获取市场数据的币种
marketData, hasData := ctx.MarketDataMap[coin.Symbol]
if !hasData {
continue
}
displayedCount++
// 显示币种来源标签 - 使用圆括号避免与JSON混淆
sourceTags := ""
hasAI500 := false
hasOITop := false
for _, source := range coin.Sources {
if source == "ai500" {
hasAI500 = true
} else if source == "oi_top" {
hasOITop = true
}
}
if hasAI500 && hasOITop {
sourceTags = "(AI500+OI_Top双重信号)"
} else if hasAI500 {
sourceTags = "(AI500高评分)"
} else if hasOITop {
sourceTags = "(OI_Top持仓增长)"
}
sb.WriteString(fmt.Sprintf("\n### 币种 #%d: %s %s\n", displayedCount, coin.Symbol, sourceTags))
sb.WriteString(formatMarketDataBrief(marketData))
// 如果有OI Top数据也显示出来
if oiTopData, hasOI := ctx.OITopDataMap[coin.Symbol]; hasOI {
sb.WriteString(fmt.Sprintf("**市场热度数据** (OI Top排名 #%d):\n", oiTopData.Rank))
sb.WriteString(fmt.Sprintf(" - 持仓量1h变化: %+.2f%% (价值: $%.0f)\n",
oiTopData.OIDeltaPercent, oiTopData.OIDeltaValue))
sb.WriteString(fmt.Sprintf(" - 价格1h变化: %+.2f%% | 净多仓: %.0f | 净空仓: %.0f\n",
oiTopData.PriceDeltaPercent, oiTopData.NetLong, oiTopData.NetShort))
}
}
// 添加历史表现反馈(如果有)
if ctx.Performance != nil {
sb.WriteString(formatPerformanceFeedback(ctx.Performance))
}
// AI决策要求
sb.WriteString("## 🎯 任务\n\n")
sb.WriteString("分析市场数据,自主决策:\n")
sb.WriteString("1. **如有历史数据,先进行自我反思**:回顾之前的交易,总结经验教训\n")
sb.WriteString("2. 评估现有持仓 → 持有或平仓\n")
sb.WriteString(fmt.Sprintf("3. 从%d个候选币种中找交易机会\n", len(ctx.MarketDataMap)))
sb.WriteString("4. 开新仓(如果有机会)\n\n")
sb.WriteString("## 📋 规则 - **重要:集中资金,精选标的**\n\n")
sb.WriteString("### 🎯 仓位管理(核心规则)\n")
sb.WriteString("1. **最大持仓数量**: 同时最多持有 **3个币种**(质量 > 数量)\n")
sb.WriteString("2. **单个仓位大小**: \n")
sb.WriteString(fmt.Sprintf(" - 山寨币: %.0f-%.0f USDT推荐%.0f USDT\n",
ctx.Account.TotalEquity*0.8, ctx.Account.TotalEquity*1.5, ctx.Account.TotalEquity*1.2))
sb.WriteString(fmt.Sprintf(" - BTC/ETH: %.0f-%.0f USDT推荐%.0f USDT\n",
ctx.Account.TotalEquity*3, ctx.Account.TotalEquity*10, ctx.Account.TotalEquity*5))
sb.WriteString("3. **杠杆**: 山寨币=20倍 | BTC/ETH=50倍\n")
sb.WriteString("4. **保证金上限**: 总使用率≤90%%\n")
sb.WriteString("5. **风险回报比**: ≥1:2\n\n")
sb.WriteString("### ⚠️ 仓位策略\n")
sb.WriteString("- **集中火力**: 宁可持有1-2个大仓位也不要持有5-6个小仓位\n")
sb.WriteString("- **严格筛选**: 只做最有把握的机会,不确定的机会宁可不做\n")
sb.WriteString("- **快速止损**: 亏损超过2%%立即止损,不要让小亏变大亏\n")
sb.WriteString("- **及时止盈**: 盈利达到目标立即止盈,落袋为安\n\n")
sb.WriteString("### 📤 输出格式\n\n")
sb.WriteString("**重要严格按照JSON格式输出所有字符串值必须用双引号包裹**\n\n")
sb.WriteString("先输出思维链分析(纯文本)然后输出JSON数组\n\n")
sb.WriteString("**思维链分析**:\n")
sb.WriteString("1. **历史经验反思**(如有历史数据): 回顾表现,总结教训,是否仓位太分散?\n")
sb.WriteString("2. **市场分析**: 分析BTC趋势和当前持仓\n")
sb.WriteString("3. **仓位检查**: 当前持仓数量是否>3个如果是平掉表现差的集中资金\n")
sb.WriteString("4. **机会识别**: 从候选币种中找1-2个最好的机会不是3-5个\n")
sb.WriteString("5. **仓位大小**: 确保单个仓位足够大山寨币1200+ USDTBTC 5000+ USDT\n")
sb.WriteString("6. **风险控制**: 检查账户保证金和仓位限制\n")
sb.WriteString("7. **最终决策摘要**: 列出所有决策最多3个币种持仓\n\n")
sb.WriteString("---\n\n")
sb.WriteString("**JSON决策数组** (不要加```标记,所有字符串必须用双引号):\n")
sb.WriteString("[\n")
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"强势突破,集中资金\"},\n", ctx.Account.TotalEquity*5))
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"SOLUSDT\", \"action\": \"open_long\", \"leverage\": 20, \"position_size_usd\": %.0f, \"stop_loss\": 180, \"take_profit\": 200, \"reasoning\": \"技术面强势\"}\n", ctx.Account.TotalEquity*1.2))
sb.WriteString("]\n\n")
sb.WriteString("**action类型**: open_long | open_short | close_long | close_short | hold | wait\n")
sb.WriteString("**开仓必填**: leverage, position_size_usd, stop_loss, take_profit\n")
sb.WriteString("**注意**: reasoning字段必须用双引号包裹例如\"reasoning\": \"这里是理由\"\n\n")
sb.WriteString("### 📝 完整示例(集中资金策略)\n\n")
// 示例仓位:集中资金策略
btcSize := ctx.Account.TotalEquity * 5 // BTC5倍净值推荐值
sb.WriteString("【历史经验反思】\n")
sb.WriteString("回顾最近10笔交易仓位太分散同时持有5个币种但单个仓位太小赚不到钱。\n")
sb.WriteString("SOLUSDT做多3次2次小盈1次止损净盈利很少。决策应该用更大仓位做确定性高的机会。\n")
sb.WriteString("BTCUSDT做多2次1胜1负但因为仓位太小盈利不明显。\n")
sb.WriteString("**改进策略**: 集中资金在1-2个最有把握的币种加大仓位。\n\n")
sb.WriteString("【市场分析】\n")
sb.WriteString("BTC突破95000MACD金叉RSI 65趋势强势。\n")
sb.WriteString("当前持有ETHUSDT小仓位+0.8%、SOLUSDT小仓位-0.3%、LINKUSDT小仓位+0.2%)→ 太分散!\n")
sb.WriteString("决定平掉所有小仓位集中资金做BTC大仓位。\n\n")
sb.WriteString("【最终决策】平掉3个小仓位集中5000 USDT做BTC多头仓位是之前的3倍+)。\n\n")
sb.WriteString("---\n\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"小仓位盈利太少,释放资金\"},\n")
sb.WriteString(" {\"symbol\": \"SOLUSDT\", \"action\": \"close_long\", \"reasoning\": \"小亏损,释放资金\"},\n")
sb.WriteString(" {\"symbol\": \"LINKUSDT\", \"action\": \"close_long\", \"reasoning\": \"小仓位盈利太少,释放资金\"},\n")
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"强势突破,集中资金做大仓位\"}\n", btcSize))
sb.WriteString("]\n\n")
sb.WriteString("**说明**: 这样只持有1个BTC大仓位盈利空间是之前的3倍+,止损也更清晰。\n\n")
sb.WriteString("现在请开始分析并给出你的决策!\n")
return sb.String()
}
// formatPerformanceFeedback 格式化历史表现反馈
func formatPerformanceFeedback(perfInterface interface{}) string {
// 类型断言避免循环依赖使用interface{}
type TradeOutcome struct {
Symbol string
Side string
OpenPrice float64
ClosePrice float64
PnL float64
PnLPct float64
Duration string
}
type SymbolPerformance struct {
Symbol string
TotalTrades int
WinningTrades int
LosingTrades int
WinRate float64
TotalPnL float64
AvgPnL float64
}
type PerformanceAnalysis struct {
TotalTrades int
WinningTrades int
LosingTrades int
WinRate float64
AvgWin float64
AvgLoss float64
ProfitFactor float64
RecentTrades []TradeOutcome
SymbolStats map[string]*SymbolPerformance
BestSymbol string
WorstSymbol string
}
// 使用JSON转换进行类型转换避免直接类型断言
jsonData, _ := json.Marshal(perfInterface)
var perf PerformanceAnalysis
if err := json.Unmarshal(jsonData, &perf); err != nil {
return ""
}
var sb strings.Builder
sb.WriteString("## 📊 历史表现反馈\n\n")
if perf.TotalTrades == 0 {
sb.WriteString("暂无历史交易数据\n\n")
return sb.String()
}
// 整体统计
sb.WriteString("### 整体表现\n")
sb.WriteString(fmt.Sprintf("- **总交易数**: %d 笔 (盈利: %d | 亏损: %d)\n",
perf.TotalTrades, perf.WinningTrades, perf.LosingTrades))
sb.WriteString(fmt.Sprintf("- **胜率**: %.1f%%\n", perf.WinRate))
sb.WriteString(fmt.Sprintf("- **平均盈利**: +%.2f%% | 平均亏损: %.2f%%\n",
perf.AvgWin, perf.AvgLoss))
if perf.ProfitFactor > 0 {
sb.WriteString(fmt.Sprintf("- **盈亏比**: %.2f:1\n", perf.ProfitFactor))
}
sb.WriteString("\n")
// 最近交易
if len(perf.RecentTrades) > 0 {
sb.WriteString("### 最近交易\n")
displayCount := len(perf.RecentTrades)
if displayCount > 5 {
displayCount = 5
}
for i := 0; i < displayCount; i++ {
trade := perf.RecentTrades[i]
outcome := "✓"
if trade.PnL < 0 {
outcome = "✗"
}
sb.WriteString(fmt.Sprintf("%d. %s %s: %.4f → %.4f = %+.2f%% %s\n",
i+1, trade.Symbol, strings.ToUpper(trade.Side),
trade.OpenPrice, trade.ClosePrice,
trade.PnLPct, outcome))
}
sb.WriteString("\n")
}
// 币种表现显示前3个最好和最差
if len(perf.SymbolStats) > 0 {
sb.WriteString("### 币种表现\n")
if perf.BestSymbol != "" {
if stats, exists := perf.SymbolStats[perf.BestSymbol]; exists {
sb.WriteString(fmt.Sprintf("- **最佳**: %s (胜率%.0f%%, 平均%+.2f%%)\n",
stats.Symbol, stats.WinRate, stats.AvgPnL))
}
}
if perf.WorstSymbol != "" {
if stats, exists := perf.SymbolStats[perf.WorstSymbol]; exists {
sb.WriteString(fmt.Sprintf("- **最差**: %s (胜率%.0f%%, 平均%+.2f%%)\n",
stats.Symbol, stats.WinRate, stats.AvgPnL))
}
}
sb.WriteString("\n")
}
return sb.String()
}
// formatMarketDataBrief 格式化市场数据(简洁版)
func formatMarketDataBrief(data *MarketData) string {
var sb strings.Builder
sb.WriteString("**市场数据** (3分钟线):\n")
sb.WriteString(fmt.Sprintf(" - 价格: %.4f | 1h变化: %+.2f%% | 4h变化: %+.2f%% (%s)\n",
data.CurrentPrice, data.PriceChange1h, data.PriceChange4h, priceTrend(data.PriceChange1h, data.PriceChange4h)))
sb.WriteString(fmt.Sprintf(" - EMA20: %.4f (%s) | MACD: %.4f (%s) | RSI(7): %.2f (%s)\n",
data.CurrentEMA20, pricePosition(data.CurrentPrice, data.CurrentEMA20),
data.CurrentMACD, macdTrend(data.CurrentMACD), data.CurrentRSI7, rsiStatus(data.CurrentRSI7)))
if data.OpenInterest != nil {
oiChange := ((data.OpenInterest.Latest - data.OpenInterest.Average) / data.OpenInterest.Average) * 100
fundingSignal := fundingRateSignal(data.FundingRate)
sb.WriteString(fmt.Sprintf(" - 持仓量: %+.2f%% | 资金费率: %.6f (%s)\n", oiChange, data.FundingRate, fundingSignal))
}
return sb.String()
}
// parseFullDecisionResponse 解析AI的完整决策响应
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*AIFullDecision, error) {
// 1. 提取思维链
cotTrace := extractCoTTrace(aiResponse)
// 2. 提取JSON决策列表
decisions, err := extractDecisions(aiResponse)
if err != nil {
return &AIFullDecision{
CoTTrace: cotTrace,
Decisions: []TradingDecision{},
}, fmt.Errorf("提取决策失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
}
// 3. 验证决策
if err := validateDecisions(decisions, accountEquity); err != nil {
return &AIFullDecision{
CoTTrace: cotTrace,
Decisions: decisions,
}, fmt.Errorf("决策验证失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
}
return &AIFullDecision{
CoTTrace: cotTrace,
Decisions: decisions,
}, nil
}
// extractCoTTrace 提取思维链分析
func extractCoTTrace(response string) string {
// 查找JSON数组的开始位置
jsonStart := strings.Index(response, "[")
if jsonStart > 0 {
// 思维链是JSON数组之前的内容
return strings.TrimSpace(response[:jsonStart])
}
// 如果找不到JSON整个响应都是思维链
return strings.TrimSpace(response)
}
// extractDecisions 提取JSON决策列表
func extractDecisions(response string) ([]TradingDecision, error) {
// 直接查找JSON数组 - 找第一个完整的JSON数组
arrayStart := strings.Index(response, "[")
if arrayStart == -1 {
return nil, fmt.Errorf("无法找到JSON数组起始")
}
// 从 [ 开始,匹配括号找到对应的 ]
arrayEnd := findMatchingBracket(response, arrayStart)
if arrayEnd == -1 {
return nil, fmt.Errorf("无法找到JSON数组结束")
}
jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1])
// 🔧 修复常见的JSON格式错误缺少引号的字段值
// 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号)
// 修复为: "reasoning": "内容"}
// 使用简单的字符串扫描而不是正则表达式
jsonContent = fixMissingQuotes(jsonContent)
// 解析JSON
var decisions []TradingDecision
if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil {
return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent)
}
return decisions, nil
}
// fixMissingQuotes 修复缺少引号的reasoning字段
func fixMissingQuotes(jsonStr string) string {
// 匹配: "reasoning": 内容"} 或 "reasoning": 内容}
// 不匹配: "reasoning": "已经有引号"
// 替换为: "reasoning": "内容"}
re := regexp.MustCompile(`"reasoning":\s*([^"}\n,][^}\n,]*?)([}\n,])`)
return re.ReplaceAllString(jsonStr, `"reasoning": "$1"$2`)
}
// validateDecisions 验证所有决策(需要账户信息)
func validateDecisions(decisions []TradingDecision, accountEquity float64) error {
for i, decision := range decisions {
if err := validateDecision(&decision, accountEquity); err != nil {
return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
}
}
return nil
}
// findMatchingBracket 查找匹配的右括号
func findMatchingBracket(s string, start int) int {
if start >= len(s) || s[start] != '[' {
return -1
}
depth := 0
for i := start; i < len(s); i++ {
switch s[i] {
case '[':
depth++
case ']':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// validateDecision 验证单个决策的有效性
func validateDecision(d *TradingDecision, accountEquity float64) error {
// 验证action
validActions := map[string]bool{
"open_long": true,
"open_short": true,
"close_long": true,
"close_short": true,
"hold": true,
"wait": true,
}
if !validActions[d.Action] {
return fmt.Errorf("无效的action: %s", d.Action)
}
// 开仓操作必须提供完整参数
if d.Action == "open_long" || d.Action == "open_short" {
// 根据币种判断杠杆上限和仓位价值上限
maxLeverage := 20 // 山寨币固定20倍
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
maxLeverage = 50 // BTC和ETH固定50倍
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
}
if d.Leverage <= 0 || d.Leverage > maxLeverage {
return fmt.Errorf("杠杆必须在1-%d之间%s: %d", maxLeverage, d.Symbol, d.Leverage)
}
if d.PositionSizeUSD <= 0 {
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
}
// 验证仓位价值上限加1%容差以避免浮点数精度问题)
tolerance := maxPositionValue * 0.01 // 1%容差
if d.PositionSizeUSD > maxPositionValue+tolerance {
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
return fmt.Errorf("BTC/ETH单币种仓位价值不能超过%.0f USDT10倍账户净值实际: %.0f", maxPositionValue, d.PositionSizeUSD)
} else {
return fmt.Errorf("山寨币单币种仓位价值不能超过%.0f USDT1.5倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD)
}
}
if d.StopLoss <= 0 || d.TakeProfit <= 0 {
return fmt.Errorf("止损和止盈必须大于0")
}
// 验证止损止盈的合理性
if d.Action == "open_long" {
if d.StopLoss >= d.TakeProfit {
return fmt.Errorf("做多时止损价必须小于止盈价")
}
} else {
if d.StopLoss <= d.TakeProfit {
return fmt.Errorf("做空时止损价必须大于止盈价")
}
}
}
return nil
}