package decision import ( "encoding/json" "fmt" "log" "math" "nofx/market" "nofx/mcp" "nofx/pool" "regexp" "strings" "time" ) // 预编译正则表达式(性能优化:避免每次调用时重新编译) var ( // ✅ 安全的正則:精確匹配 ```json 代碼塊 // 使用反引號 + 拼接避免轉義問題 reJSONFence = regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```") reJSONArray = regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) reArrayHead = regexp.MustCompile(`^\[\s*\{`) reArrayOpenSpace = regexp.MustCompile(`^\[\s+\{`) reInvisibleRunes = regexp.MustCompile("[\u200B\u200C\u200D\uFEFF]") // 新增:XML标签提取(支持思维链中包含任何字符) reReasoningTag = regexp.MustCompile(`(?s)(.*?)`) reDecisionTag = regexp.MustCompile(`(?s)(.*?)`) ) // 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"` PeakPnLPct float64 `json:"peak_pnl_pct"` // 历史最高收益率(百分比) LiquidationPrice float64 `json:"liquidation_price"` MarginUsed float64 `json:"margin_used"` UpdateTime int64 `json:"update_time"` // 持仓更新时间戳(毫秒) } // 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 // 净空仓 } // Context 交易上下文(传递给AI的完整信息) type Context 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]*market.Data `json:"-"` // 不序列化,但内部使用 OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射 Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis) BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取) AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取) } // Decision AI的交易决策 type Decision struct { Symbol string `json:"symbol"` Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "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"` // 调整参数(新增) NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100) // 通用参数 Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 Reasoning string `json:"reasoning"` } // FullDecision AI的完整决策(包含思维链) type FullDecision struct { SystemPrompt string `json:"system_prompt"` // 系统提示词(发送给AI的系统prompt) UserPrompt string `json:"user_prompt"` // 发送给AI的输入prompt CoTTrace string `json:"cot_trace"` // 思维链分析(AI输出) Decisions []Decision `json:"decisions"` // 具体决策列表 Timestamp time.Time `json:"timestamp"` // AIRequestDurationMs 记录 AI API 调用耗时(毫秒)方便排查延迟问题 AIRequestDurationMs int64 `json:"ai_request_duration_ms,omitempty"` } // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) { return GetFullDecisionWithCustomPrompt(ctx, mcpClient, "", false, "") } // GetFullDecisionWithCustomPrompt 获取AI的完整交易决策(支持自定义prompt和模板选择) func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient *mcp.Client, customPrompt string, overrideBase bool, templateName string) (*FullDecision, error) { // 1. 为所有币种获取市场数据 if err := fetchMarketDataForContext(ctx); err != nil { return nil, fmt.Errorf("获取市场数据失败: %w", err) } // 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据) systemPrompt := buildSystemPromptWithCustom(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage, customPrompt, overrideBase, templateName) userPrompt := buildUserPrompt(ctx) // 3. 调用AI API(使用 system + user prompt) aiCallStart := time.Now() aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) aiCallDuration := time.Since(aiCallStart) if err != nil { return nil, fmt.Errorf("调用AI API失败: %w", err) } // 4. 解析AI响应 decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage) // 无论是否有错误,都要保存 SystemPrompt 和 UserPrompt(用于调试和决策未执行后的问题定位) if decision != nil { decision.Timestamp = time.Now() decision.SystemPrompt = systemPrompt // 保存系统prompt decision.UserPrompt = userPrompt // 保存输入prompt decision.AIRequestDurationMs = aiCallDuration.Milliseconds() } if err != nil { return decision, fmt.Errorf("解析AI响应失败: %w", err) } decision.Timestamp = time.Now() decision.SystemPrompt = systemPrompt // 保存系统prompt decision.UserPrompt = userPrompt // 保存输入prompt return decision, nil } // fetchMarketDataForContext 为上下文中的所有币种获取市场数据和OI数据 func fetchMarketDataForContext(ctx *Context) error { ctx.MarketDataMap = make(map[string]*market.Data) 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 := market.Get(symbol) if err != nil { // 单个币种失败不影响整体,只记录错误 continue } // ⚠️ 流动性过滤:持仓价值低于阈值的币种不做(多空都不做) // 持仓价值 = 持仓量 × 当前价格 // 但现有持仓必须保留(需要决策是否平仓) // 💡 OI 門檻配置:用戶可根據風險偏好調整 const minOIThresholdMillions = 15.0 // 可調整:15M(保守) / 10M(平衡) / 8M(寬鬆) / 5M(激進) 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 < minOIThresholdMillions { log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < %.1fM),跳过此币种 [持仓量:%.0f × 价格:%.4f]", symbol, oiValueInMillions, minOIThresholdMillions, 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 *Context) int { // ⚠️ 重要:限制候选币种数量,避免 Prompt 过大 // 根据持仓数量动态调整:持仓越少,可以分析更多候选币 const ( maxCandidatesWhenEmpty = 30 // 无持仓时最多分析30个候选币 maxCandidatesWhenHolding1 = 25 // 持仓1个时最多分析25个候选币 maxCandidatesWhenHolding2 = 20 // 持仓2个时最多分析20个候选币 maxCandidatesWhenHolding3 = 15 // 持仓3个时最多分析15个候选币(避免 Prompt 过大) ) positionCount := len(ctx.Positions) var maxCandidates int switch positionCount { case 0: maxCandidates = maxCandidatesWhenEmpty case 1: maxCandidates = maxCandidatesWhenHolding1 case 2: maxCandidates = maxCandidatesWhenHolding2 default: // 3+ 持仓 maxCandidates = maxCandidatesWhenHolding3 } // 返回实际候选币数量和上限中的较小值 return min(len(ctx.CandidateCoins), maxCandidates) } // buildSystemPromptWithCustom 构建包含自定义内容的 System Prompt func buildSystemPromptWithCustom(accountEquity float64, btcEthLeverage, altcoinLeverage int, customPrompt string, overrideBase bool, templateName string) string { // 如果覆盖基础prompt且有自定义prompt,只使用自定义prompt if overrideBase && customPrompt != "" { return customPrompt } // 获取基础prompt(使用指定的模板) basePrompt := buildSystemPrompt(accountEquity, btcEthLeverage, altcoinLeverage, templateName) // 如果没有自定义prompt,直接返回基础prompt if customPrompt == "" { return basePrompt } // 添加自定义prompt部分到基础prompt var sb strings.Builder sb.WriteString(basePrompt) sb.WriteString("\n\n") sb.WriteString("# 📌 个性化交易策略\n\n") sb.WriteString(customPrompt) sb.WriteString("\n\n") sb.WriteString("注意: 以上个性化策略是对基础规则的补充,不能违背基础风险控制原则。\n") return sb.String() } // buildSystemPrompt 构建 System Prompt(使用模板+动态部分) func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int, templateName string) string { var sb strings.Builder // 1. 加载提示词模板(核心交易策略部分) if templateName == "" { templateName = "default" // 默认使用 default 模板 } template, err := GetPromptTemplate(templateName) if err != nil { // 如果模板不存在,记录错误并使用 default log.Printf("⚠️ 提示词模板 '%s' 不存在,使用 default: %v", templateName, err) template, err = GetPromptTemplate("default") if err != nil { // 如果连 default 都不存在,使用内置的简化版本 log.Printf("❌ 无法加载任何提示词模板,使用内置简化版本") sb.WriteString("你是专业的加密货币交易AI。请根据市场数据做出交易决策。\n\n") } else { sb.WriteString(template.Content) sb.WriteString("\n\n") } } else { sb.WriteString(template.Content) sb.WriteString("\n\n") } // 2. 硬约束(风险控制)- 动态生成 sb.WriteString("# 硬约束(风险控制)\n\n") sb.WriteString("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\n") sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n", accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆** (⚠️ 严格执行,不可超过)\n", altcoinLeverage, btcEthLeverage)) sb.WriteString("5. 保证金: 总使用率 ≤ 90%\n") sb.WriteString("6. 开仓金额: 建议 **≥12 USDT** (交易所最小名义价值 10 USDT + 安全边际)\n\n") // 3. 输出格式 - 动态生成 sb.WriteString("# 输出格式 (严格遵守)\n\n") sb.WriteString("**必须使用XML标签 标签分隔思维链和决策JSON,避免解析错误**\n\n") sb.WriteString("## 格式要求\n\n") sb.WriteString("\n") sb.WriteString("你的思维链分析...\n") sb.WriteString("- 简洁分析你的思考过程 \n") sb.WriteString("\n\n") sb.WriteString("\n") sb.WriteString("```json\n[\n") sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", btcEthLeverage, accountEquity*5)) sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈离场\"}\n") sb.WriteString("]\n```\n") sb.WriteString("\n\n") sb.WriteString("## 字段说明\n\n") sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") sb.WriteString("- `confidence`: 0-100(开仓建议≥75)\n") sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd, reasoning\n\n") return sb.String() } // buildUserPrompt 构建 User Prompt(动态数据) func buildUserPrompt(ctx *Context) string { var sb strings.Builder // 系统状态 sb.WriteString(fmt.Sprintf("时间: %s | 周期: #%d | 运行: %d分钟\n\n", ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)) // BTC 市场 if btcData, hasBTC := ctx.MarketDataMap["BTCUSDT"]; hasBTC { sb.WriteString(fmt.Sprintf("BTC: %.2f (1h: %+.2f%%, 4h: %+.2f%%) | MACD: %.4f | RSI: %.2f\n\n", btcData.CurrentPrice, btcData.PriceChange1h, btcData.PriceChange4h, btcData.CurrentMACD, btcData.CurrentRSI7)) } // 账户 sb.WriteString(fmt.Sprintf("账户: 净值%.2f | 余额%.2f (%.1f%%) | 盈亏%+.2f%% | 保证金%.1f%% | 持仓%d个\n\n", ctx.Account.TotalEquity, ctx.Account.AvailableBalance, (ctx.Account.AvailableBalance/ctx.Account.TotalEquity)*100, ctx.Account.TotalPnLPct, ctx.Account.MarginUsedPct, ctx.Account.PositionCount)) // 持仓(完整市场数据) if len(ctx.Positions) > 0 { sb.WriteString("## 当前持仓\n") for i, pos := range ctx.Positions { // 计算持仓时长 holdingDuration := "" if pos.UpdateTime > 0 { durationMs := time.Now().UnixMilli() - pos.UpdateTime durationMin := durationMs / (1000 * 60) // 转换为分钟 if durationMin < 60 { holdingDuration = fmt.Sprintf(" | 持仓时长%d分钟", durationMin) } else { durationHour := durationMin / 60 durationMinRemainder := durationMin % 60 holdingDuration = fmt.Sprintf(" | 持仓时长%d小时%d分钟", durationHour, durationMinRemainder) } } // 计算仓位价值(用于 partial_close 检查) positionValue := math.Abs(pos.Quantity) * pos.MarkPrice sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 数量%.4f | 仓位价值%.2f USDT | 盈亏%+.2f%% | 盈亏金额%+.2f USDT | 最高收益率%.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f%s\n\n", i+1, pos.Symbol, strings.ToUpper(pos.Side), pos.EntryPrice, pos.MarkPrice, pos.Quantity, positionValue, pos.UnrealizedPnLPct, pos.UnrealizedPnL, pos.PeakPnLPct, pos.Leverage, pos.MarginUsed, pos.LiquidationPrice, holdingDuration)) // 使用FormatMarketData输出完整市场数据 if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok { sb.WriteString(market.Format(marketData)) sb.WriteString("\n") } } } else { sb.WriteString("当前持仓: 无\n\n") } // 候选币种(完整市场数据) sb.WriteString(fmt.Sprintf("## 候选币种 (%d个)\n\n", len(ctx.MarketDataMap))) displayedCount := 0 for _, coin := range ctx.CandidateCoins { marketData, hasData := ctx.MarketDataMap[coin.Symbol] if !hasData { continue } displayedCount++ sourceTags := "" if len(coin.Sources) > 1 { sourceTags = " (AI500+OI_Top双重信号)" } else if len(coin.Sources) == 1 && coin.Sources[0] == "oi_top" { sourceTags = " (OI_Top持仓增长)" } // 使用FormatMarketData输出完整市场数据 sb.WriteString(fmt.Sprintf("### %d. %s%s\n\n", displayedCount, coin.Symbol, sourceTags)) sb.WriteString(market.Format(marketData)) sb.WriteString("\n") } sb.WriteString("\n") // 夏普比率(直接传值,不要复杂格式化) if ctx.Performance != nil { // 直接从interface{}中提取SharpeRatio type PerformanceData struct { SharpeRatio float64 `json:"sharpe_ratio"` } var perfData PerformanceData if jsonData, err := json.Marshal(ctx.Performance); err == nil { if err := json.Unmarshal(jsonData, &perfData); err == nil { sb.WriteString(fmt.Sprintf("## 📊 夏普比率: %.2f\n\n", perfData.SharpeRatio)) } } } sb.WriteString("---\n\n") sb.WriteString("现在请分析并输出决策(思维链 + JSON)\n") return sb.String() } // parseFullDecisionResponse 解析AI的完整决策响应 func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) { // 1. 提取思维链 cotTrace := extractCoTTrace(aiResponse) // 2. 提取JSON决策列表 decisions, err := extractDecisions(aiResponse) if err != nil { return &FullDecision{ CoTTrace: cotTrace, Decisions: []Decision{}, }, fmt.Errorf("提取决策失败: %w", err) } // 3. 验证决策 if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil { return &FullDecision{ CoTTrace: cotTrace, Decisions: decisions, }, fmt.Errorf("决策验证失败: %w", err) } return &FullDecision{ CoTTrace: cotTrace, Decisions: decisions, }, nil } // extractCoTTrace 提取思维链分析 func extractCoTTrace(response string) string { // 方法1: 优先尝试提取 标签内容 if match := reReasoningTag.FindStringSubmatch(response); match != nil && len(match) > 1 { log.Printf("✓ 使用 标签提取思维链") return strings.TrimSpace(match[1]) } // 方法2: 如果没有 标签,但有 标签,提取 之前的内容 if decisionIdx := strings.Index(response, ""); decisionIdx > 0 { log.Printf("✓ 提取 标签之前的内容作为思维链") return strings.TrimSpace(response[:decisionIdx]) } // 方法3: 后备方案 - 查找JSON数组的开始位置 jsonStart := strings.Index(response, "[") if jsonStart > 0 { log.Printf("⚠️ 使用旧版格式([ 字符分离)提取思维链") return strings.TrimSpace(response[:jsonStart]) } // 如果找不到任何标记,整个响应都是思维链 return strings.TrimSpace(response) } // extractDecisions 提取JSON决策列表 func extractDecisions(response string) ([]Decision, error) { // 预清洗:去零宽/BOM s := removeInvisibleRunes(response) s = strings.TrimSpace(s) // 🔧 关键修复 (Critical Fix):在正则匹配之前就先修复全角字符! // 否则正则表达式 \[ 无法匹配全角的 [ s = fixMissingQuotes(s) // 方法1: 优先尝试从 标签中提取 var jsonPart string if match := reDecisionTag.FindStringSubmatch(s); match != nil && len(match) > 1 { jsonPart = strings.TrimSpace(match[1]) log.Printf("✓ 使用 标签提取JSON") } else { // 后备方案:使用整个响应 jsonPart = s log.Printf("⚠️ 未找到 标签,使用全文搜索JSON") } // 修复 jsonPart 中的全角字符 jsonPart = fixMissingQuotes(jsonPart) // 1) 优先从 ```json 代码块中提取 if m := reJSONFence.FindStringSubmatch(jsonPart); m != nil && len(m) > 1 { jsonContent := strings.TrimSpace(m[1]) jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" jsonContent = fixMissingQuotes(jsonContent) // 二次修复(防止 regex 提取后还有残留全角) if err := validateJSONFormat(jsonContent); err != nil { return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) } var decisions []Decision if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent) } return decisions, nil } // 2) 退而求其次 (Fallback):全文寻找首个对象数组 // 注意:此时 jsonPart 已经过 fixMissingQuotes(),全角字符已转换为半角 jsonContent := strings.TrimSpace(reJSONArray.FindString(jsonPart)) if jsonContent == "" { // 🔧 安全回退 (Safe Fallback):当AI只输出思维链没有JSON时,生成保底决策(避免系统崩溃) log.Printf("⚠️ [SafeFallback] AI未输出JSON决策,进入安全等待模式 (AI response without JSON, entering safe wait mode)") // 提取思维链摘要(最多 240 字符) cotSummary := jsonPart if len(cotSummary) > 240 { cotSummary = cotSummary[:240] + "..." } // 生成保底决策:所有币种进入 wait 状态 fallbackDecision := Decision{ Symbol: "ALL", Action: "wait", Reasoning: fmt.Sprintf("模型未输出结构化JSON决策,进入安全等待;摘要:%s", cotSummary), } return []Decision{fallbackDecision}, nil } // 🔧 规整格式(此时全角字符已在前面修复过) jsonContent = compactArrayOpen(jsonContent) jsonContent = fixMissingQuotes(jsonContent) // 二次修复(防止 regex 提取后还有残留全角) // 🔧 验证 JSON 格式(检测常见错误) if err := validateJSONFormat(jsonContent); err != nil { return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) } // 解析JSON var decisions []Decision if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent) } return decisions, nil } // fixMissingQuotes 替换中文引号和全角字符为英文引号和半角字符(避免AI输出全角JSON字符导致解析失败) func fixMissingQuotes(jsonStr string) string { // 替换中文引号 jsonStr = strings.ReplaceAll(jsonStr, "\u201c", "\"") // " jsonStr = strings.ReplaceAll(jsonStr, "\u201d", "\"") // " jsonStr = strings.ReplaceAll(jsonStr, "\u2018", "'") // ' jsonStr = strings.ReplaceAll(jsonStr, "\u2019", "'") // ' // ⚠️ 替换全角括号、冒号、逗号(防止AI输出全角JSON字符) jsonStr = strings.ReplaceAll(jsonStr, "[", "[") // U+FF3B 全角左方括号 jsonStr = strings.ReplaceAll(jsonStr, "]", "]") // U+FF3D 全角右方括号 jsonStr = strings.ReplaceAll(jsonStr, "{", "{") // U+FF5B 全角左花括号 jsonStr = strings.ReplaceAll(jsonStr, "}", "}") // U+FF5D 全角右花括号 jsonStr = strings.ReplaceAll(jsonStr, ":", ":") // U+FF1A 全角冒号 jsonStr = strings.ReplaceAll(jsonStr, ",", ",") // U+FF0C 全角逗号 // ⚠️ 替换CJK标点符号(AI在中文上下文中也可能输出这些) jsonStr = strings.ReplaceAll(jsonStr, "【", "[") // CJK左方头括号 U+3010 jsonStr = strings.ReplaceAll(jsonStr, "】", "]") // CJK右方头括号 U+3011 jsonStr = strings.ReplaceAll(jsonStr, "〔", "[") // CJK左龟壳括号 U+3014 jsonStr = strings.ReplaceAll(jsonStr, "〕", "]") // CJK右龟壳括号 U+3015 jsonStr = strings.ReplaceAll(jsonStr, "、", ",") // CJK顿号 U+3001 // ⚠️ 替换全角空格为半角空格(JSON中不应该有全角空格) jsonStr = strings.ReplaceAll(jsonStr, " ", " ") // U+3000 全角空格 return jsonStr } // validateJSONFormat 验证 JSON 格式,检测常见错误 func validateJSONFormat(jsonStr string) error { trimmed := strings.TrimSpace(jsonStr) // 允许 [ 和 { 之间存在任意空白(含零宽) if !reArrayHead.MatchString(trimmed) { // 检查是否是纯数字/范围数组(常见错误) if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") { return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))]) } return fmt.Errorf("JSON 必须以 [{ 开头(允许空白),实际: %s", trimmed[:min(20, len(trimmed))]) } // 检查是否包含范围符号 ~(LLM 常见错误) if strings.Contains(jsonStr, "~") { return fmt.Errorf("JSON 中不可包含范围符号 ~,所有数字必须是精确的单一值") } // 检查是否包含千位分隔符(如 98,000) // 使用简单的模式匹配:数字+逗号+3位数字 for i := 0; i < len(jsonStr)-4; i++ { if jsonStr[i] >= '0' && jsonStr[i] <= '9' && jsonStr[i+1] == ',' && jsonStr[i+2] >= '0' && jsonStr[i+2] <= '9' && jsonStr[i+3] >= '0' && jsonStr[i+3] <= '9' && jsonStr[i+4] >= '0' && jsonStr[i+4] <= '9' { return fmt.Errorf("JSON 数字不可包含千位分隔符逗号,发现: %s", jsonStr[i:min(i+10, len(jsonStr))]) } } return nil } // min 返回两个整数中的较小值 func min(a, b int) int { if a < b { return a } return b } // removeInvisibleRunes 去除零宽字符和 BOM,避免肉眼看不见的前缀破坏校验 func removeInvisibleRunes(s string) string { return reInvisibleRunes.ReplaceAllString(s, "") } // compactArrayOpen 规整开头的 "[ {" → "[{" func compactArrayOpen(s string) string { return reArrayOpenSpace.ReplaceAllString(strings.TrimSpace(s), "[{") } // validateDecisions 验证所有决策(需要账户信息和杠杆配置) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage); 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 *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { // 验证action validActions := map[string]bool{ "open_long": true, "open_short": true, "close_long": true, "close_short": true, "update_stop_loss": true, "update_take_profit": true, "partial_close": 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 := altcoinLeverage // 山寨币使用配置的杠杆 maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值 if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆 maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值 } // ✅ Fallback 机制:杠杆超限时自动修正为上限值(而不是直接拒绝决策) if d.Leverage <= 0 { return fmt.Errorf("杠杆必须大于0: %d", d.Leverage) } if d.Leverage > maxLeverage { log.Printf("⚠️ [Leverage Fallback] %s 杠杆超限 (%dx > %dx),自动调整为上限值 %dx", d.Symbol, d.Leverage, maxLeverage, maxLeverage) d.Leverage = maxLeverage // 自动修正为上限值 } if d.PositionSizeUSD <= 0 { return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) } // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) // Binance 最小名义价值 10 USDT + 安全边际 const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.PositionSizeUSD < minPositionSizeBTCETH { return fmt.Errorf("%s 开仓金额过小(%.2f USDT),必须≥%.2f USDT(因价格高且精度限制,避免数量四舍五入为0)", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH) } } else { if d.PositionSizeUSD < minPositionSizeGeneral { return fmt.Errorf("开仓金额过小(%.2f USDT),必须≥%.2f USDT(Binance 最小名义价值要求)", d.PositionSizeUSD, minPositionSizeGeneral) } } // 验证仓位价值上限(加1%容差以避免浮点数精度问题) tolerance := maxPositionValue * 0.01 // 1%容差 if d.PositionSizeUSD > maxPositionValue+tolerance { if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { return fmt.Errorf("BTC/ETH单币种仓位价值不能超过%.0f USDT(10倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD) } else { return fmt.Errorf("山寨币单币种仓位价值不能超过%.0f USDT(1.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("做空时止损价必须大于止盈价") } } // 验证风险回报比(必须≥1:3) // 计算入场价(假设当前市价) var entryPrice float64 if d.Action == "open_long" { // 做多:入场价在止损和止盈之间 entryPrice = d.StopLoss + (d.TakeProfit-d.StopLoss)*0.2 // 假设在20%位置入场 } else { // 做空:入场价在止损和止盈之间 entryPrice = d.StopLoss - (d.StopLoss-d.TakeProfit)*0.2 // 假设在20%位置入场 } var riskPercent, rewardPercent, riskRewardRatio float64 if d.Action == "open_long" { riskPercent = (entryPrice - d.StopLoss) / entryPrice * 100 rewardPercent = (d.TakeProfit - entryPrice) / entryPrice * 100 if riskPercent > 0 { riskRewardRatio = rewardPercent / riskPercent } } else { riskPercent = (d.StopLoss - entryPrice) / entryPrice * 100 rewardPercent = (entryPrice - d.TakeProfit) / entryPrice * 100 if riskPercent > 0 { riskRewardRatio = rewardPercent / riskPercent } } // 硬约束:风险回报比必须≥3.0 if riskRewardRatio < 3.0 { return fmt.Errorf("风险回报比过低(%.2f:1),必须≥3.0:1 [风险:%.2f%% 收益:%.2f%%] [止损:%.2f 止盈:%.2f]", riskRewardRatio, riskPercent, rewardPercent, d.StopLoss, d.TakeProfit) } } // 动态调整止损验证 if d.Action == "update_stop_loss" { if d.NewStopLoss <= 0 { return fmt.Errorf("新止损价格必须大于0: %.2f", d.NewStopLoss) } } // 动态调整止盈验证 if d.Action == "update_take_profit" { if d.NewTakeProfit <= 0 { return fmt.Errorf("新止盈价格必须大于0: %.2f", d.NewTakeProfit) } } // 部分平仓验证 if d.Action == "partial_close" { if d.ClosePercentage <= 0 || d.ClosePercentage > 100 { return fmt.Errorf("平仓百分比必须在0-100之间: %.1f", d.ClosePercentage) } } return nil }