Files
nofx/decision/engine.go
2025-11-01 20:17:20 +08:00

687 lines
28 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 decision
import (
"encoding/json"
"fmt"
"log"
"nofx/market"
"nofx/mcp"
"nofx/pool"
"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"`
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", "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"`
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"`
}
// 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
aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil {
return nil, fmt.Errorf("调用AI API失败: %w", err)
}
// 4. 解析AI响应
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
if err != nil {
return nil, 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
}
// ⚠️ 流动性过滤持仓价值低于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 *Context) int {
// 直接返回候选池的全部币种数量
// 因为候选池已经在 auto_trader.go 中筛选过了
// 固定分析前20个评分最高的币种来自AI500
return len(ctx.CandidateCoins)
}
// 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(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n",
accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage))
sb.WriteString("4. 保证金: 总使用率 ≤ 90%\n\n")
// === 震荡交易策略 ===
sb.WriteString("# 📦 震荡交易策略(核心)\n\n")
sb.WriteString("**策略定位**: 专门做 BTC 震荡行情,快进快出,高胜率低盈亏比\n\n")
sb.WriteString("**震荡区间识别**\n")
sb.WriteString("- 价格在15分钟/1小时 EMA20上下波动±2-4%\n")
sb.WriteString("- MACD 在零轴附近(-200到+200之间\n")
sb.WriteString("- 多个时间框架方向不一致如15m上涨但1h下跌\n")
sb.WriteString("- RSI 在30-70区间反复震荡\n\n")
sb.WriteString("**交易逻辑**\n")
sb.WriteString("- **区间下沿**RSI<35 或接近支撑) → 做多\n")
sb.WriteString("- **区间上沿**RSI>65 或接近压力) → 做空\n")
sb.WriteString("- **趋势行情**(多时间框架共振,放量突破) → 立即止损\n\n")
// === 交易频率认知 ===
sb.WriteString("# ⏱️ 交易频率认知\n\n")
sb.WriteString("**量化标准**:\n")
sb.WriteString("- 优秀交易员每天2-4笔 = 每小时0.1-0.2笔\n")
sb.WriteString("- 过度交易:每小时>2笔 = 严重问题\n")
sb.WriteString("- 最佳节奏开仓后持有至少30-60分钟\n\n")
sb.WriteString("**自查**:\n")
sb.WriteString("如果你发现自己每个周期都在交易 → 说明标准太低\n")
sb.WriteString("如果你发现持仓<30分钟就平仓 → 说明太急躁\n\n")
// === 开仓信号强度 ===
sb.WriteString("# 🎯 开仓标准(严格)\n\n")
sb.WriteString("只在**强信号**时开仓,不确定就观望。\n\n")
sb.WriteString("**你拥有的完整数据(专为震荡交易优化)**\n\n")
sb.WriteString("**📊 四个时间框架序列**每个包含最近10个数据点\n")
sb.WriteString("1. **3分钟序列**:实时价格 + 放量分析(当前价格 = 最后一根K线的收盘价\n")
sb.WriteString(" - Mid prices, EMA20, MACD, RSI7, RSI14\n")
sb.WriteString(" - **Volumes**: 成交量序列(用于检测放量)\n")
sb.WriteString(" - **BuySellRatios**: 买卖压力比(>0.6多方强,<0.4空方强)\n")
sb.WriteString("2. **15分钟序列**短期震荡区间识别覆盖最近2.5小时)\n")
sb.WriteString(" - Mid prices, EMA20, MACD, RSI7, RSI14\n")
sb.WriteString("3. **1小时序列**中期支撑压力确认覆盖最近10小时\n")
sb.WriteString(" - Mid prices, EMA20, MACD, RSI7, RSI14\n")
sb.WriteString("4. **4小时序列**大趋势预警覆盖最近40小时\n")
sb.WriteString(" - EMA20 vs EMA50, ATR, Volume, MACD, RSI14\n\n")
sb.WriteString("**💰 资金数据**\n")
sb.WriteString("- 持仓量(OI)变化、资金费率、成交量对比\n\n")
sb.WriteString("**🎯 震荡交易分析方法**\n\n")
sb.WriteString("**1. 震荡区间识别**\n")
sb.WriteString("- 价格在15m/1h EMA20 上下±2-4%波动\n")
sb.WriteString("- RSI 在30-70区间反复未出现持续超买/超卖\n")
sb.WriteString("- MACD 在零轴附近震荡,未出现明确金叉/死叉\n")
sb.WriteString("- 1h和4h时间框架方向不一致矛盾 = 震荡)\n\n")
sb.WriteString("**2. 买卖压力分析**3分钟放量检测\n")
sb.WriteString("- **连续放量** = 最近2-3根3分钟K线成交量 > 平均成交量1.5倍\n")
sb.WriteString("- **买方力量**BuySellRatio > 0.6(主动买入占比 > 60%\n")
sb.WriteString("- **卖方力量**BuySellRatio < 0.4(主动卖出占比 > 60%\n")
sb.WriteString("- **放量+买压** → 可能向上突破,做多或止损空单\n")
sb.WriteString("- **放量+卖压** → 可能向下突破,做空或止损多单\n\n")
sb.WriteString("**3. 入场信号**(高胜率位置):\n")
sb.WriteString("- **区间下沿做多**RSI < 35 + 买卖压力比 > 0.5 + 价格接近15m EMA20下方\n")
sb.WriteString("- **区间上沿做空**RSI > 65 + 买卖压力比 < 0.5 + 价格接近15m EMA20上方\n")
sb.WriteString("- **综合信心度 ≥ 75 才开仓**\n\n")
sb.WriteString("**4. 止损信号**(趋势突破,立即离场):\n")
sb.WriteString("- 多时间框架共振15m/1h/4h方向一致\n")
sb.WriteString("- 连续2根以上3分钟K线放量突破区间\n")
sb.WriteString("- MACD 突破零轴并加速\n\n")
sb.WriteString("**避免低质量信号**\n")
sb.WriteString("- 单一维度(只看一个指标)\n")
sb.WriteString("- 区间中部交易(等待区间边界)\n")
sb.WriteString("- 刚平仓不久(<10分钟\n")
sb.WriteString("- 无买卖压力确认的入场\n\n")
// === 夏普比率自我进化 ===
sb.WriteString("# 🧬 夏普比率自我进化\n\n")
sb.WriteString("每次你会收到**夏普比率**作为绩效反馈(周期级别):\n\n")
sb.WriteString("**夏普比率 < -0.5** (持续亏损):\n")
sb.WriteString(" → 🛑 停止交易连续观望至少6个周期18分钟\n")
sb.WriteString(" → 🔍 深度反思:\n")
sb.WriteString(" • 交易频率过高?(每小时>2次就是过度\n")
sb.WriteString(" • 持仓时间过短?(<30分钟就是过早平仓\n")
sb.WriteString(" • 信号强度不足?(信心度<75\n")
sb.WriteString(" • 是否在做空?(单边做多是错误的)\n\n")
sb.WriteString("**夏普比率 -0.5 ~ 0** (轻微亏损):\n")
sb.WriteString(" → ⚠️ 严格控制:只做信心度>80的交易\n")
sb.WriteString(" → 减少交易频率每小时最多1笔新开仓\n")
sb.WriteString(" → 耐心持仓至少持有30分钟以上\n\n")
sb.WriteString("**夏普比率 0 ~ 0.7** (正收益):\n")
sb.WriteString(" → ✅ 维持当前策略\n\n")
sb.WriteString("**夏普比率 > 0.7** (优异表现):\n")
sb.WriteString(" → 🚀 可适度扩大仓位\n\n")
sb.WriteString("**关键**: 夏普比率是唯一指标,它会自然惩罚频繁交易和过度进出。\n\n")
// === 决策流程 ===
sb.WriteString("# 📋 决策流程\n\n")
sb.WriteString("1. **分析夏普比率**: 当前策略是否有效?需要调整吗?\n")
sb.WriteString("2. **评估持仓**: 趋势是否改变?是否该止盈/止损?\n")
sb.WriteString("3. **寻找新机会**: 有强信号吗?多空机会?\n")
sb.WriteString("4. **输出决策**: 思维链分析 + JSON\n\n")
// 3. 输出格式 - 动态生成
sb.WriteString("#输出格式\n\n")
sb.WriteString("第一步: 思维链(纯文本)\n")
sb.WriteString("简洁分析你的思考过程\n\n")
sb.WriteString("第二步: JSON决策数组\n\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\n")
sb.WriteString("字段说明:\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)
}
}
sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 盈亏%+.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f%s\n\n",
i+1, pos.Symbol, strings.ToUpper(pos.Side),
pos.EntryPrice, pos.MarkPrice, pos.UnrealizedPnLPct,
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\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
}
// 3. 验证决策
if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
return &FullDecision{
CoTTrace: cotTrace,
Decisions: decisions,
}, fmt.Errorf("决策验证失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
}
return &FullDecision{
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) ([]Decision, 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 []Decision
if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil {
return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent)
}
return decisions, nil
}
// fixMissingQuotes 替换中文引号为英文引号(避免输入法自动转换)
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", "'") // '
return jsonStr
}
// 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,
"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倍账户净值
}
if d.Leverage <= 0 || d.Leverage > maxLeverage {
return fmt.Errorf("杠杆必须在1-%d之间%s当前配置上限%d倍: %d", maxLeverage, d.Symbol, maxLeverage, 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("做空时止损价必须大于止盈价")
}
}
// 验证风险回报比必须≥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)
}
}
return nil
}