Files
nofx/market/ai_decision_engine.go
tinkle-community 96b342d481 Update: Merge nofx improvements
- Frontend trading records and UI enhancements
- Optimized AI prompts and decision engine
- Performance analysis and comparison features
- Binance-style UI improvements
2025-10-28 21:45:28 +08:00

662 lines
24 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(fmt.Sprintf("1. **单币种仓位上限**: 山寨币≤%.0f USDT | BTC/ETH≤%.0f USDT\n", ctx.Account.TotalEquity*1.5, ctx.Account.TotalEquity*10))
sb.WriteString("2. **杠杆**: 山寨币=20倍 | BTC/ETH=50倍\n")
sb.WriteString("3. **保证金上限**: 总使用率≤90%%\n")
sb.WriteString("4. **风险回报比**: ≥1:2\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. **机会识别**: 从候选币种中找交易机会\n")
sb.WriteString("4. **风险控制**: 检查账户保证金和仓位限制\n")
sb.WriteString("5. **最终决策摘要**: 列出所有决策\n\n")
sb.WriteString("---\n\n")
sb.WriteString("**JSON决策数组** (不要加```标记,所有字符串必须用双引号):\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": 15000, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"突破做多\"},\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"hold\", \"reasoning\": \"持续观察\"}\n")
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 * 8 // BTC示例8倍净值不超过10倍上限
sb.WriteString("【历史经验反思】\n")
sb.WriteString("回顾最近10笔交易胜率40%盈亏比0.8:1表现欠佳。\n")
sb.WriteString("SOLUSDT做多3次全部止损该币种波动大暂时避开。\n")
sb.WriteString("BTCUSDT做多2次1胜1负可继续关注。\n\n")
sb.WriteString("【市场分析】\n")
sb.WriteString("BTC突破上涨MACD金叉趋势向上。\n")
sb.WriteString("当前持仓ETHUSDT多头盈利+2.3%,继续持有。\n\n")
sb.WriteString("【最终决策】持有ETHUSDT开多BTCUSDT。\n\n")
sb.WriteString("---\n\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"hold\", \"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("现在请开始分析并给出你的决策!\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
}