From 29b31f1b6462dcd9feacf1f8f26a4938585ff0c4 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Wed, 29 Oct 2025 10:50:09 +0800 Subject: [PATCH] chore: Remove obsolete market and scanner files Remove deprecated files that don't exist in nofx internal version: - market/ai_decision_engine.go - market/ai_signal.go - market/market_data.go - scanner/ai_scanner.go Keep only market/data.go to align with internal version structure. Co-Authored-By: tinkle-community --- market/ai_decision_engine.go | 613 ----------------------------------- market/ai_signal.go | 605 ---------------------------------- market/market_data.go | 552 ------------------------------- scanner/ai_scanner.go | 361 --------------------- 4 files changed, 2131 deletions(-) delete mode 100644 market/ai_decision_engine.go delete mode 100644 market/ai_signal.go delete mode 100644 market/market_data.go delete mode 100644 scanner/ai_scanner.go diff --git a/market/ai_decision_engine.go b/market/ai_decision_engine.go deleted file mode 100644 index 6e1a8ab3..00000000 --- a/market/ai_decision_engine.go +++ /dev/null @@ -1,613 +0,0 @@ -package market - -import ( - "encoding/json" - "fmt" - "log" - "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"` -} - -// 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"` - Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) - RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 - Reasoning string `json:"reasoning"` -} - -// AIFullDecision AI的完整决策(包含思维链) -type AIFullDecision struct { - UserPrompt string `json:"user_prompt"` // 发送给AI的输入prompt - CoTTrace string `json:"cot_trace"` // 思维链分析(AI输出) - 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. 构建 System Prompt(固定规则)和 User Prompt(动态数据) - systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity) - userPrompt := buildUserPrompt(ctx) - - // 3. 调用AI API(使用 system + user prompt) - aiResponse, err := callAIWithMessages(systemPrompt, userPrompt) - 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() - decision.UserPrompt = userPrompt // 保存输入prompt - 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) -} - -// buildSystemPrompt 构建 System Prompt(固定规则,可缓存) -func buildSystemPrompt(accountEquity float64) string { - var sb strings.Builder - - // 角色定义 - sb.WriteString("你是专业的加密货币交易AI,在币安合约市场进行自主交易。\n\n") - sb.WriteString("**使命**: 最大化风险调整后收益(Sharpe Ratio)\n\n") - - // 自我进化核心 - sb.WriteString("## 🧬 自我进化机制\n") - sb.WriteString("每次调用你都会收到**夏普比率**作为你的业绩指标(周期级别,非年化):\n\n") - sb.WriteString("**夏普比率解读**(正常范围 -2 到 +2):\n") - sb.WriteString("- < -0.5:持续亏损 → 🔴 极度保守策略(减仓、收紧止损、减少持仓数)\n") - sb.WriteString("- -0.5 到 0:轻微亏损 → 🟡 优化策略(保守仓位、提高选币标准)\n") - sb.WriteString("- 0 到 0.7:正收益 → 🟢 维持/优化当前策略\n") - sb.WriteString("- > 0.7:优异表现 → 🟢 可适度扩大仓位\n\n") - - // 仓位管理规则 - sb.WriteString("## 仓位管理\n") - sb.WriteString("- 最多持有 **3个币种**(质量>数量)\n") - sb.WriteString(fmt.Sprintf("- 山寨币: %.0f-%.0f USDT/仓(推荐%.0f),杠杆20x\n", - accountEquity*0.8, accountEquity*1.5, accountEquity*1.2)) - sb.WriteString(fmt.Sprintf("- BTC/ETH: %.0f-%.0f USDT/仓(推荐%.0f),杠杆50x\n", - accountEquity*3, accountEquity*10, accountEquity*5)) - sb.WriteString("- 保证金使用率 ≤90%%\n") - sb.WriteString("- 风险回报比 ≥1:2\n\n") - - // 决策流程 - sb.WriteString("## 决策流程\n") - sb.WriteString("1. **检查夏普比率**:理解当前策略效果,根据夏普比率调整策略\n") - sb.WriteString("2. **评估持仓**:决定平仓/持有\n") - sb.WriteString("3. **寻找机会**:筛选候选币种\n") - sb.WriteString("4. **执行决策**:输出思维链和JSON决策\n\n") - - // JSON 输出格式 - sb.WriteString("## 输出格式\n\n") - sb.WriteString("**先输出思维链(纯文本),再输出JSON数组**\n\n") - sb.WriteString("JSON示例:\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, \"confidence\": 85, \"risk_usd\": 200, \"reasoning\": \"强势突破\"},\n", accountEquity*5)) - sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈\"}\n") - sb.WriteString("]\n") - sb.WriteString("```\n\n") - sb.WriteString("**字段说明**:\n") - sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") - sb.WriteString("- `confidence`: 信心度0-100(必填,即使不确定也要给出)\n") - sb.WriteString("- `risk_usd`: 最大美元风险 = (entry_price - stop_loss) × quantity(开仓时必填)\n") - sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n\n") - - // DeepSeek/Qwen 特定优化 - sb.WriteString("**提示**: 运用技术分析原理,趋势确认>指标信号,不要过度依赖单一指标\n") - - return sb.String() -} - -// buildUserPrompt 构建 User Prompt(动态数据) -func buildUserPrompt(ctx *TradingContext) 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 { - sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 盈亏%+.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.4f\n\n", - i+1, pos.Symbol, strings.ToUpper(pos.Side), - pos.EntryPrice, pos.MarkPrice, pos.UnrealizedPnLPct, - pos.Leverage, pos.MarginUsed, pos.LiquidationPrice)) - - // 使用FormatMarketData输出完整市场数据 - if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok { - sb.WriteString(FormatMarketData(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(FormatMarketData(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) (*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 替换中文引号为英文引号(避免输入法自动转换) -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 []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 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("做空时止损价必须大于止盈价") - } - } - } - - return nil -} - -// interpretSharpeRatio 解释夏普比率的含义 -// 注:这里是周期级别(非年化)的夏普比率,正常范围在 -2 到 +2 -func interpretSharpeRatio(sharpe float64) string { - if sharpe < -0.5 { - return "持续亏损,策略需大幅调整" - } else if sharpe < 0 { - return "轻微亏损,需优化策略" - } else if sharpe < 0.3 { - return "正收益但波动大" - } else if sharpe < 0.7 { - return "良好表现" - } else if sharpe < 1.0 { - return "优秀表现" - } else { - return "卓越表现" - } -} - -// getAdaptiveBehaviorRecommendation 根据夏普比率生成自适应行为建议 -// 这是AI自我进化的核心:根据风险调整后收益动态调整交易策略 -// 注:sharpe是周期级别(非年化),正常范围 -2 到 +2 -func getAdaptiveBehaviorRecommendation(sharpe float64, accountEquity float64) string { - var sb strings.Builder - - sb.WriteString("### 🎯 自适应行为建议(基于夏普比率)\n\n") - - if sharpe < -0.5 { - // 🔴 持续亏损:需要极度保守 - sb.WriteString("**⚠️ 警告:当前策略产生负收益,立即调整!**\n\n") - sb.WriteString("**策略调整**:\n") - sb.WriteString(fmt.Sprintf("- 仓位规模:**减半**(山寨币: %.0f USDT, BTC/ETH: %.0f USDT)\n", - accountEquity*0.6, accountEquity*2.5)) - sb.WriteString("- 止损幅度:**收紧至-1%**(快速止损,保护本金)\n") - sb.WriteString("- 选币标准:**只做最高确定性**(信心度≥95%,风险回报比≥1:3)\n") - sb.WriteString("- 持仓数量:**最多1个**(极度精选)\n") - sb.WriteString("- 决策频率:**减少交易**(宁可不做,也不要乱做)\n\n") - sb.WriteString("**反思要点**:\n") - sb.WriteString("- 为什么之前的交易亏损?是选币问题还是时机问题?\n") - sb.WriteString("- 是否追涨杀跌?是否逆势交易?\n") - sb.WriteString("- 止损是否执行到位?\n\n") - - } else if sharpe < 0 { - // 🟡 -0.5 到 0:轻微亏损 - sb.WriteString("**状态:轻微亏损,需要优化策略**\n\n") - sb.WriteString("**策略调整**:\n") - sb.WriteString(fmt.Sprintf("- 仓位规模:**保守**(山寨币: %.0f USDT, BTC/ETH: %.0f USDT)\n", - accountEquity*0.8, accountEquity*3.5)) - sb.WriteString("- 止损幅度:**收紧至-1.5%**\n") - sb.WriteString("- 选币标准:**提高阈值**(信心度≥80%,风险回报比≥1:2.5)\n") - sb.WriteString("- 持仓数量:**最多2个**\n") - sb.WriteString("- 重点改进:**找出亏损原因**,调整选币或时机\n\n") - - } else if sharpe < 0.7 { - // 🟢 0-0.7:正收益但可继续优化 - sb.WriteString("**状态:正收益但风险较高,需要优化**\n\n") - sb.WriteString("**策略调整**:\n") - sb.WriteString(fmt.Sprintf("- 仓位规模:**保守**(山寨币: %.0f USDT, BTC/ETH: %.0f USDT)\n", - accountEquity*0.8, accountEquity*3.5)) - sb.WriteString("- 止损幅度:**收紧至-1.5%**\n") - sb.WriteString("- 选币标准:**提高阈值**(信心度≥80%,风险回报比≥1:2.5)\n") - sb.WriteString("- 持仓数量:**最多2个**\n") - sb.WriteString("- 重点改进:**减少亏损幅度**,提高止损执行力\n\n") - sb.WriteString("**优化方向**:\n") - sb.WriteString("- 避免冲动交易,等待更好的入场时机\n") - sb.WriteString("- 减少交易频率,提高单笔交易质量\n") - sb.WriteString("- 盈利时及时止盈,不要贪多\n\n") - - } else if sharpe < 1.0 { - // 🟢 0.7-1.0:优秀表现 - sb.WriteString("**状态:表现良好,继续保持当前策略**\n\n") - sb.WriteString("**策略调整**:\n") - sb.WriteString(fmt.Sprintf("- 仓位规模:**标准**(山寨币: %.0f USDT, BTC/ETH: %.0f USDT)\n", - accountEquity*1.2, accountEquity*5)) - sb.WriteString("- 止损幅度:**-2%**(标准设置)\n") - sb.WriteString("- 选币标准:**正常**(信心度≥75%,风险回报比≥1:2)\n") - sb.WriteString("- 持仓数量:**最多3个**\n") - sb.WriteString("- 保持纪律:**严格执行止损止盈**\n\n") - sb.WriteString("**持续改进**:\n") - sb.WriteString("- 总结盈利交易的共性特征,复制成功模式\n") - sb.WriteString("- 分析亏损交易,避免重复错误\n") - sb.WriteString("- 保持冷静客观,不要因为短期盈利而冒进\n\n") - - } else { - // 🟢 >1.0:卓越表现 - sb.WriteString("**状态:卓越表现,策略非常有效!**\n\n") - sb.WriteString("**策略调整**:\n") - sb.WriteString(fmt.Sprintf("- 仓位规模:**可适度放大**(山寨币: %.0f USDT, BTC/ETH: %.0f USDT)\n", - accountEquity*1.5, accountEquity*6)) - sb.WriteString("- 止损幅度:**-2%**(保持纪律,不要因为盈利而放松)\n") - sb.WriteString("- 选币标准:**正常**(信心度≥75%)\n") - sb.WriteString("- 持仓数量:**最多3个**\n") - sb.WriteString("- **核心原则:保持纪律,不要过度自信**\n\n") - sb.WriteString("**风险提示**:\n") - sb.WriteString("- 即使表现优异,也要保持风险管理纪律\n") - sb.WriteString("- 市场环境会变化,不要因短期成功而冒进\n") - sb.WriteString("- 继续严格执行止损,保护已有收益\n\n") - } - - return sb.String() -} diff --git a/market/ai_signal.go b/market/ai_signal.go deleted file mode 100644 index b6dcad2d..00000000 --- a/market/ai_signal.go +++ /dev/null @@ -1,605 +0,0 @@ -package market - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" -) - -// SignalType 交易信号类型 -type SignalType string - -const ( - SignalOpenLong SignalType = "OPEN_LONG" // 开多仓 - SignalOpenShort SignalType = "OPEN_SHORT" // 开空仓 - SignalCloseLong SignalType = "CLOSE_LONG" // 平多仓 - SignalCloseShort SignalType = "CLOSE_SHORT" // 平空仓 - SignalHold SignalType = "HOLD" // 持仓不动 - SignalWait SignalType = "WAIT" // 观望 -) - -// TradingSignal AI返回的交易信号 -type TradingSignal struct { - Symbol string `json:"symbol"` // 币种符号 - Signal SignalType `json:"signal"` // 信号类型 - Confidence float64 `json:"confidence"` // 信心度 (0-100) - Reasoning string `json:"reasoning"` // 分析理由 - EntryPrice float64 `json:"entry_price"` // 建议入场价格 - StopLoss float64 `json:"stop_loss"` // 建议止损价格 - TakeProfit float64 `json:"take_profit"` // 建议止盈价格 - Timestamp time.Time `json:"timestamp"` // 信号生成时间 -} - -// AIProvider AI提供商类型 -type AIProvider string - -const ( - ProviderDeepSeek AIProvider = "deepseek" - ProviderQwen AIProvider = "qwen" -) - -// AIConfig AI API配置 -type AIConfig struct { - Provider AIProvider - APIKey string - SecretKey string // 阿里云需要 - BaseURL string - Model string - Timeout time.Duration -} - -// 默认配置 -var defaultConfig = AIConfig{ - Provider: ProviderDeepSeek, - BaseURL: "https://api.deepseek.com/v1", - Model: "deepseek-chat", - Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 -} - -// SetDeepSeekAPIKey 设置DeepSeek API密钥 -func SetDeepSeekAPIKey(apiKey string) { - defaultConfig.Provider = ProviderDeepSeek - defaultConfig.APIKey = apiKey - defaultConfig.BaseURL = "https://api.deepseek.com/v1" - defaultConfig.Model = "deepseek-chat" -} - -// SetQwenAPIKey 设置阿里云Qwen API密钥 -func SetQwenAPIKey(apiKey, secretKey string) { - defaultConfig.Provider = ProviderQwen - defaultConfig.APIKey = apiKey - defaultConfig.SecretKey = secretKey - defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max -} - -// SetAIConfig 设置完整的AI配置(高级用户) -func SetAIConfig(config AIConfig) { - if config.Timeout == 0 { - config.Timeout = 30 * time.Second - } - defaultConfig = config -} - -// DeepSeekConfig 兼容旧代码 -type DeepSeekConfig = AIConfig - -// SetDeepSeekConfig 兼容旧代码 -func SetDeepSeekConfig(config DeepSeekConfig) { - SetAIConfig(config) -} - -// GetAITradingSignal 获取AI交易信号 -func GetAITradingSignal(symbol string) (*TradingSignal, error) { - // 1. 获取市场数据 - marketData, err := GetMarketData(symbol) - if err != nil { - return nil, fmt.Errorf("获取市场数据失败: %w", err) - } - - // 2. 格式化为AI提示 - prompt := formatMarketDataForAI(marketData) - - // 3. 调用DeepSeek API - aiResponse, err := callDeepSeekAPI(prompt) - if err != nil { - return nil, fmt.Errorf("调用DeepSeek API失败: %w", err) - } - - // 4. 解析AI响应 - signal, err := parseAIResponse(aiResponse, marketData) - if err != nil { - return nil, fmt.Errorf("解析AI响应失败: %w", err) - } - - signal.Symbol = marketData.Symbol - signal.Timestamp = time.Now() - - return signal, nil -} - -// formatMarketDataForAI 将市场数据格式化为AI提示 -func formatMarketDataForAI(data *MarketData) string { - var sb strings.Builder - - sb.WriteString("你是一位专业的加密货币交易员,请根据以下市场数据分析并给出交易建议。\n\n") - sb.WriteString(fmt.Sprintf("【币种】%s\n\n", data.Symbol)) - - // 当前指标 - sb.WriteString("【当前实时指标】(基于3分钟K线)\n") - sb.WriteString(fmt.Sprintf("• 当前价格: %.4f USDT\n", data.CurrentPrice)) - sb.WriteString(fmt.Sprintf("• EMA20: %.4f (价格%s均线)\n", data.CurrentEMA20, - pricePosition(data.CurrentPrice, data.CurrentEMA20))) - sb.WriteString(fmt.Sprintf("• MACD: %.4f (%s)\n", data.CurrentMACD, macdTrend(data.CurrentMACD))) - sb.WriteString(fmt.Sprintf("• RSI(7期): %.2f (%s)\n\n", data.CurrentRSI7, rsiStatus(data.CurrentRSI7))) - - // 持仓量和资金费率 - if data.OpenInterest != nil { - oiChange := ((data.OpenInterest.Latest - data.OpenInterest.Average) / data.OpenInterest.Average) * 100 - sb.WriteString("【持仓量与资金费率】\n") - sb.WriteString(fmt.Sprintf("• 当前持仓量: %.2f (较平均%+.2f%%)\n", - data.OpenInterest.Latest, oiChange)) - sb.WriteString(fmt.Sprintf("• 资金费率: %.6f (%s)\n\n", - data.FundingRate, fundingRateStatus(data.FundingRate))) - } - - // 日内趋势 - if data.IntradaySeries != nil && len(data.IntradaySeries.MACDValues) > 0 { - sb.WriteString("【日内趋势】(3分钟K线最近10个点)\n") - sb.WriteString(fmt.Sprintf("• 价格序列: %s\n", formatFloatArray(data.IntradaySeries.MidPrices))) - sb.WriteString(fmt.Sprintf("• MACD序列: %s (%s)\n", - formatFloatArray(data.IntradaySeries.MACDValues), - seriesTrend(data.IntradaySeries.MACDValues))) - sb.WriteString(fmt.Sprintf("• RSI(7期)序列: %s (%s)\n\n", - formatFloatArray(data.IntradaySeries.RSI7Values), - rsiSeriesTrend(data.IntradaySeries.RSI7Values))) - } - - // 长期背景 - if data.LongerTermContext != nil { - sb.WriteString("【长期背景】(4小时K线)\n") - sb.WriteString(fmt.Sprintf("• EMA20: %.2f vs EMA50: %.2f (%s)\n", - data.LongerTermContext.EMA20, data.LongerTermContext.EMA50, - emaCross(data.LongerTermContext.EMA20, data.LongerTermContext.EMA50))) - sb.WriteString(fmt.Sprintf("• ATR(3期): %.2f vs ATR(14期): %.2f (波动率%s)\n", - data.LongerTermContext.ATR3, data.LongerTermContext.ATR14, - atrStatus(data.LongerTermContext.ATR3, data.LongerTermContext.ATR14))) - sb.WriteString(fmt.Sprintf("• 当前成交量: %.2f vs 平均成交量: %.2f (%s)\n", - data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume, - volumeStatus(data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume))) - - if len(data.LongerTermContext.RSI14Values) > 0 { - sb.WriteString(fmt.Sprintf("• 4小时RSI(14期): %.2f (%s)\n\n", - data.LongerTermContext.RSI14Values[len(data.LongerTermContext.RSI14Values)-1], - rsiStatus(data.LongerTermContext.RSI14Values[len(data.LongerTermContext.RSI14Values)-1]))) - } - } - - // AI指令 - sb.WriteString("【交易建议要求】\n") - sb.WriteString("你是一位**激进型交易员**,善于捕捉市场机会。请基于以上数据,给出一个**明确的交易信号**。\n\n") - sb.WriteString("**重要原则:**\n") - sb.WriteString("1. 优先给出 OPEN_LONG 或 OPEN_SHORT 信号,而不是观望\n") - sb.WriteString("2. 即使信号不完美,也要找出最可能的方向\n") - sb.WriteString("3. RSI超买可能是强势延续,RSI超卖可能是抄底机会\n") - sb.WriteString("4. MACD负值转正 = 买入信号,正值转负 = 卖出信号\n") - sb.WriteString("5. 价格突破EMA20 = 趋势确认\n") - sb.WriteString("6. 持仓量增加 + 价格上涨 = 多头强势\n") - sb.WriteString("7. 只有在多空完全平衡、无法判断时才给 WAIT\n\n") - sb.WriteString("请严格按照以下JSON格式返回:\n\n") - sb.WriteString("```json\n") - sb.WriteString("{\n") - sb.WriteString(" \"signal\": \"OPEN_LONG | OPEN_SHORT | CLOSE_LONG | CLOSE_SHORT | HOLD | WAIT\",\n") - sb.WriteString(" \"confidence\": 85.5,\n") - sb.WriteString(" \"reasoning\": \"详细分析理由(200字以内)\",\n") - sb.WriteString(" \"entry_price\": 1.234,\n") - sb.WriteString(" \"stop_loss\": 1.100,\n") - sb.WriteString(" \"take_profit\": 1.450\n") - sb.WriteString("}\n") - sb.WriteString("```\n\n") - sb.WriteString("注意:\n") - sb.WriteString("1. signal必须是以下之一: OPEN_LONG(开多), OPEN_SHORT(开空), CLOSE_LONG(平多), CLOSE_SHORT(平空), HOLD(持有), WAIT(观望)\n") - sb.WriteString("2. confidence是信心度(0-100),即使是中等信号也应该给出\n") - sb.WriteString("3. reasoning要简洁有力,说明最关键的交易依据\n") - sb.WriteString("4. entry_price是建议入场价格(可以略高于或低于当前价)\n") - sb.WriteString("5. stop_loss和take_profit要合理,建议风险回报比至少1:2\n") - - return sb.String() -} - -// callDeepSeekAPI 调用AI API(支持DeepSeek和Qwen),带重试机制 -// 兼容旧代码:只传user prompt -func callDeepSeekAPI(prompt string) (string, error) { - return callAIWithMessages("", prompt) -} - -// callAIWithMessages 使用 system + user prompt 调用AI API(推荐) -func callAIWithMessages(systemPrompt, userPrompt string) (string, error) { - if defaultConfig.APIKey == "" { - return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") - } - - // 重试配置 - maxRetries := 3 - var lastErr error - - for attempt := 1; attempt <= maxRetries; attempt++ { - if attempt > 1 { - fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) - } - - result, err := callAIWithMessagesOnce(systemPrompt, userPrompt) - if err == nil { - if attempt > 1 { - fmt.Printf("✓ AI API重试成功\n") - } - return result, nil - } - - lastErr = err - // 如果不是网络错误,不重试 - if !isRetryableError(err) { - return "", err - } - - // 重试前等待 - if attempt < maxRetries { - waitTime := time.Duration(attempt) * 2 * time.Second - fmt.Printf("⏳ 等待%v后重试...\n", waitTime) - time.Sleep(waitTime) - } - } - - return "", fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr) -} - -// callDeepSeekAPIOnce 单次调用AI API(兼容旧代码) -func callDeepSeekAPIOnce(prompt string) (string, error) { - return callAIWithMessagesOnce("", prompt) -} - -// callAIWithMessagesOnce 单次调用AI API(支持 system + user prompt) -func callAIWithMessagesOnce(systemPrompt, userPrompt string) (string, error) { - // 构建 messages 数组 - messages := []map[string]string{} - - // 如果有 system prompt,添加 system message - if systemPrompt != "" { - messages = append(messages, map[string]string{ - "role": "system", - "content": systemPrompt, - }) - } - - // 添加 user message - messages = append(messages, map[string]string{ - "role": "user", - "content": userPrompt, - }) - - // 构建请求体 - requestBody := map[string]interface{}{ - "model": defaultConfig.Model, - "messages": messages, - "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 - "max_tokens": 2000, - } - - // 注意:response_format 参数仅 OpenAI 支持,DeepSeek/Qwen 不支持 - // 我们通过强化 prompt 和后处理来确保 JSON 格式正确 - - jsonData, err := json.Marshal(requestBody) - if err != nil { - return "", fmt.Errorf("序列化请求失败: %w", err) - } - - // 创建HTTP请求 - url := fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - return "", fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - - // 根据不同的Provider设置认证方式 - switch defaultConfig.Provider { - case ProviderDeepSeek: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) - case ProviderQwen: - // 阿里云Qwen使用API-Key认证 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) - // 注意:如果使用的不是兼容模式,可能需要不同的认证方式 - default: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) - } - - // 发送请求 - client := &http.Client{Timeout: defaultConfig.Timeout} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - // 读取响应 - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("读取响应失败: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body)) - } - - // 解析响应 - var result struct { - Choices []struct { - Message struct { - Content string `json:"content"` - } `json:"message"` - } `json:"choices"` - } - - if err := json.Unmarshal(body, &result); err != nil { - return "", fmt.Errorf("解析响应失败: %w", err) - } - - if len(result.Choices) == 0 { - return "", fmt.Errorf("API返回空响应") - } - - return result.Choices[0].Message.Content, nil -} - -// isRetryableError 判断错误是否可重试 -func isRetryableError(err error) bool { - errStr := err.Error() - // 网络错误、超时、EOF等可以重试 - retryableErrors := []string{ - "EOF", - "timeout", - "connection reset", - "connection refused", - "temporary failure", - "no such host", - } - for _, retryable := range retryableErrors { - if strings.Contains(errStr, retryable) { - return true - } - } - return false -} - -// parseAIResponse 解析AI响应 -func parseAIResponse(aiResponse string, marketData *MarketData) (*TradingSignal, error) { - // 尝试从响应中提取JSON - jsonStart := strings.Index(aiResponse, "```json") - jsonEnd := strings.Index(aiResponse, "```\n") - - // 如果没找到结束标记,尝试找第二个``` - if jsonEnd == -1 || jsonEnd <= jsonStart { - // 从jsonStart之后找第一个``` - jsonEnd = strings.Index(aiResponse[jsonStart+7:], "```") - if jsonEnd != -1 { - jsonEnd += jsonStart + 7 - } - } - - var jsonContent string - if jsonStart != -1 && jsonEnd != -1 && jsonEnd > jsonStart { - jsonContent = aiResponse[jsonStart+7 : jsonEnd] - } else { - // 如果没有markdown代码块,尝试查找第一个完整的JSON对象 - jsonStart = strings.Index(aiResponse, "{") - if jsonStart == -1 { - return nil, fmt.Errorf("无法从AI响应中提取JSON: %s", aiResponse) - } - - // 找到匹配的右括号 - braceCount := 0 - jsonEnd = -1 - for i := jsonStart; i < len(aiResponse); i++ { - if aiResponse[i] == '{' { - braceCount++ - } else if aiResponse[i] == '}' { - braceCount-- - if braceCount == 0 { - jsonEnd = i + 1 - break - } - } - } - - if jsonEnd == -1 { - return nil, fmt.Errorf("无法找到完整的JSON对象") - } - jsonContent = aiResponse[jsonStart:jsonEnd] - } - - // 解析JSON - var signal TradingSignal - if err := json.Unmarshal([]byte(jsonContent), &signal); err != nil { - return nil, fmt.Errorf("解析JSON失败: %w, JSON内容: %s", err, jsonContent) - } - - // 验证信号类型 - validSignals := map[SignalType]bool{ - SignalOpenLong: true, - SignalOpenShort: true, - SignalCloseLong: true, - SignalCloseShort: true, - SignalHold: true, - SignalWait: true, - } - - if !validSignals[signal.Signal] { - return nil, fmt.Errorf("无效的信号类型: %s", signal.Signal) - } - - // 验证信心度范围 - if signal.Confidence < 0 || signal.Confidence > 100 { - signal.Confidence = 50 // 默认值 - } - - return &signal, nil -} - -// 辅助函数:价格与均线位置 -func pricePosition(price, ema float64) string { - if price > ema { - return "位于上方" - } - return "位于下方" -} - -// 辅助函数:MACD趋势 -func macdTrend(macd float64) string { - if macd > 0 { - return "多头" - } - return "空头" -} - -// 辅助函数:RSI状态 -func rsiStatus(rsi float64) string { - if rsi >= 70 { - return "超买" - } else if rsi <= 30 { - return "超卖" - } - return "中性" -} - -// 辅助函数:价格趋势(基于1h和4h变化) -func priceTrend(change1h, change4h float64) string { - if change1h > 2 && change4h > 5 { - return "强势上涨" - } else if change1h > 0 && change4h > 0 { - return "温和上涨" - } else if change1h < -2 && change4h < -5 { - return "强势下跌" - } else if change1h < 0 && change4h < 0 { - return "温和下跌" - } else { - return "震荡" - } -} - -// 辅助函数:资金费率信号(交易机会解读) -func fundingRateSignal(rate float64) string { - if rate > 0.001 { - return "多头拥挤,考虑做空" - } else if rate > 0.0005 { - return "多头占优" - } else if rate < -0.001 { - return "空头拥挤,考虑做多" - } else if rate < -0.0005 { - return "空头占优" - } - return "中性" -} - -// 辅助函数:资金费率状态 -func fundingRateStatus(rate float64) string { - if rate > 0.0005 { - return "多头占优,费率偏高" - } else if rate < -0.0005 { - return "空头占优,费率为负" - } - return "费率中性" -} - -// 辅助函数:EMA交叉状态 -func emaCross(ema20, ema50 float64) string { - if ema20 > ema50 { - return "金叉,多头趋势" - } - return "死叉,空头趋势" -} - -// 辅助函数:ATR状态 -func atrStatus(atr3, atr14 float64) string { - if atr3 > atr14*1.2 { - return "急剧上升" - } else if atr3 < atr14*0.8 { - return "逐渐下降" - } - return "稳定" -} - -// 辅助函数:成交量状态 -func volumeStatus(current, average float64) string { - ratio := current / average - if ratio > 1.5 { - return "放量明显" - } else if ratio < 0.5 { - return "缩量明显" - } - return "正常水平" -} - -// 辅助函数:序列趋势 -func seriesTrend(values []float64) string { - if len(values) < 2 { - return "数据不足" - } - - recent := values[len(values)-1] - prev := values[len(values)-2] - - if recent > prev*1.1 { - return "强势上升" - } else if recent > prev { - return "小幅上升" - } else if recent < prev*0.9 { - return "快速下降" - } else if recent < prev { - return "小幅下降" - } - return "横盘整理" -} - -// 辅助函数:RSI序列趋势 -func rsiSeriesTrend(values []float64) string { - if len(values) < 2 { - return "数据不足" - } - - recent := values[len(values)-1] - prev := values[len(values)-2] - - if recent > 70 && prev > 70 { - return "持续超买" - } else if recent < 30 && prev < 30 { - return "持续超卖" - } else if recent > prev { - return "强度上升" - } else if recent < prev { - return "强度下降" - } - return "稳定" -} - -// 辅助函数:格式化浮点数组 -func formatFloatArray(values []float64) string { - if len(values) == 0 { - return "[]" - } - - var sb strings.Builder - sb.WriteString("[") - for i, v := range values { - if i > 0 { - sb.WriteString(", ") - } - sb.WriteString(fmt.Sprintf("%.3f", v)) - } - sb.WriteString("]") - return sb.String() -} diff --git a/market/market_data.go b/market/market_data.go deleted file mode 100644 index 14b40785..00000000 --- a/market/market_data.go +++ /dev/null @@ -1,552 +0,0 @@ -package market - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "math" - "net/http" - "strconv" - "strings" -) - -// MarketData 市场数据结构 -type MarketData struct { - Symbol string - CurrentPrice float64 - PriceChange1h float64 // 1小时价格变化百分比 - PriceChange4h float64 // 4小时价格变化百分比 - CurrentEMA20 float64 - CurrentMACD float64 - CurrentRSI7 float64 - OpenInterest *OIData - FundingRate float64 - IntradaySeries *IntradayData - LongerTermContext *LongerTermData -} - -// OIData Open Interest数据 -type OIData struct { - Latest float64 - Average float64 -} - -// IntradayData 日内数据(3分钟间隔) -type IntradayData struct { - MidPrices []float64 - EMA20Values []float64 - MACDValues []float64 - RSI7Values []float64 - RSI14Values []float64 -} - -// LongerTermData 长期数据(4小时时间框架) -type LongerTermData struct { - EMA20 float64 - EMA50 float64 - ATR3 float64 - ATR14 float64 - CurrentVolume float64 - AverageVolume float64 - MACDValues []float64 - RSI14Values []float64 -} - -// Kline K线数据 -type Kline struct { - OpenTime int64 - Open float64 - High float64 - Low float64 - Close float64 - Volume float64 - CloseTime int64 -} - -// GetMarketData 获取指定代币的市场数据 -func GetMarketData(symbol string) (*MarketData, error) { - // 标准化symbol - symbol = NormalizeSymbol(symbol) - - // 获取3分钟K线数据 (最近10个) - klines3m, err := getKlines(symbol, "3m", 40) // 多获取一些用于计算 - if err != nil { - return nil, fmt.Errorf("获取3分钟K线失败: %v", err) - } - - // 获取4小时K线数据 (最近10个) - klines4h, err := getKlines(symbol, "4h", 60) // 多获取用于计算指标 - if err != nil { - return nil, fmt.Errorf("获取4小时K线失败: %v", err) - } - - // 计算当前指标 (基于3分钟最新数据) - currentPrice := klines3m[len(klines3m)-1].Close - currentEMA20 := calculateEMA(klines3m, 20) - currentMACD := calculateMACD(klines3m) - currentRSI7 := calculateRSI(klines3m, 7) - - // 计算价格变化百分比 - // 1小时价格变化 = 20个3分钟K线前的价格 - priceChange1h := 0.0 - if len(klines3m) >= 21 { // 至少需要21根K线 (当前 + 20根前) - price1hAgo := klines3m[len(klines3m)-21].Close - if price1hAgo > 0 { - priceChange1h = ((currentPrice - price1hAgo) / price1hAgo) * 100 - } - } - - // 4小时价格变化 = 1个4小时K线前的价格 - priceChange4h := 0.0 - if len(klines4h) >= 2 { - price4hAgo := klines4h[len(klines4h)-2].Close - if price4hAgo > 0 { - priceChange4h = ((currentPrice - price4hAgo) / price4hAgo) * 100 - } - } - - // 获取OI数据 - oiData, err := getOpenInterestData(symbol) - if err != nil { - // OI失败不影响整体,使用默认值 - oiData = &OIData{Latest: 0, Average: 0} - } - - // 获取Funding Rate - fundingRate, _ := getFundingRate(symbol) - - // 计算日内系列数据 - intradayData := calculateIntradaySeries(klines3m) - - // 计算长期数据 - longerTermData := calculateLongerTermData(klines4h) - - return &MarketData{ - Symbol: symbol, - CurrentPrice: currentPrice, - PriceChange1h: priceChange1h, - PriceChange4h: priceChange4h, - CurrentEMA20: currentEMA20, - CurrentMACD: currentMACD, - CurrentRSI7: currentRSI7, - OpenInterest: oiData, - FundingRate: fundingRate, - IntradaySeries: intradayData, - LongerTermContext: longerTermData, - }, nil -} - -// getKlines 从Binance获取K线数据 -func getKlines(symbol, interval string, limit int) ([]Kline, error) { - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=%d", - symbol, interval, limit) - - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var rawData [][]interface{} - if err := json.Unmarshal(body, &rawData); err != nil { - return nil, err - } - - klines := make([]Kline, len(rawData)) - for i, item := range rawData { - openTime := int64(item[0].(float64)) - open, _ := parseFloat(item[1]) - high, _ := parseFloat(item[2]) - low, _ := parseFloat(item[3]) - close, _ := parseFloat(item[4]) - volume, _ := parseFloat(item[5]) - closeTime := int64(item[6].(float64)) - - klines[i] = Kline{ - OpenTime: openTime, - Open: open, - High: high, - Low: low, - Close: close, - Volume: volume, - CloseTime: closeTime, - } - } - - return klines, nil -} - -// calculateEMA 计算EMA -func calculateEMA(klines []Kline, period int) float64 { - if len(klines) < period { - return 0 - } - - // 计算SMA作为初始EMA - sum := 0.0 - for i := 0; i < period; i++ { - sum += klines[i].Close - } - ema := sum / float64(period) - - // 计算EMA - multiplier := 2.0 / float64(period+1) - for i := period; i < len(klines); i++ { - ema = (klines[i].Close-ema)*multiplier + ema - } - - return ema -} - -// calculateMACD 计算MACD -func calculateMACD(klines []Kline) float64 { - if len(klines) < 26 { - return 0 - } - - // 计算12期和26期EMA - ema12 := calculateEMA(klines, 12) - ema26 := calculateEMA(klines, 26) - - // MACD = EMA12 - EMA26 - return ema12 - ema26 -} - -// calculateRSI 计算RSI -func calculateRSI(klines []Kline, period int) float64 { - if len(klines) <= period { - return 0 - } - - gains := 0.0 - losses := 0.0 - - // 计算初始平均涨跌幅 - for i := 1; i <= period; i++ { - change := klines[i].Close - klines[i-1].Close - if change > 0 { - gains += change - } else { - losses += -change - } - } - - avgGain := gains / float64(period) - avgLoss := losses / float64(period) - - // 使用Wilder平滑方法计算后续RSI - for i := period + 1; i < len(klines); i++ { - change := klines[i].Close - klines[i-1].Close - if change > 0 { - avgGain = (avgGain*float64(period-1) + change) / float64(period) - avgLoss = (avgLoss * float64(period-1)) / float64(period) - } else { - avgGain = (avgGain * float64(period-1)) / float64(period) - avgLoss = (avgLoss*float64(period-1) + (-change)) / float64(period) - } - } - - if avgLoss == 0 { - return 100 - } - - rs := avgGain / avgLoss - rsi := 100 - (100 / (1 + rs)) - - return rsi -} - -// calculateATR 计算ATR -func calculateATR(klines []Kline, period int) float64 { - if len(klines) <= period { - return 0 - } - - trs := make([]float64, len(klines)) - for i := 1; i < len(klines); i++ { - high := klines[i].High - low := klines[i].Low - prevClose := klines[i-1].Close - - tr1 := high - low - tr2 := math.Abs(high - prevClose) - tr3 := math.Abs(low - prevClose) - - trs[i] = math.Max(tr1, math.Max(tr2, tr3)) - } - - // 计算初始ATR - sum := 0.0 - for i := 1; i <= period; i++ { - sum += trs[i] - } - atr := sum / float64(period) - - // Wilder平滑 - for i := period + 1; i < len(klines); i++ { - atr = (atr*float64(period-1) + trs[i]) / float64(period) - } - - return atr -} - -// calculateIntradaySeries 计算日内系列数据 -func calculateIntradaySeries(klines []Kline) *IntradayData { - data := &IntradayData{ - MidPrices: make([]float64, 0, 10), - EMA20Values: make([]float64, 0, 10), - MACDValues: make([]float64, 0, 10), - RSI7Values: make([]float64, 0, 10), - RSI14Values: make([]float64, 0, 10), - } - - // 获取最近10个数据点 - start := len(klines) - 10 - if start < 0 { - start = 0 - } - - for i := start; i < len(klines); i++ { - data.MidPrices = append(data.MidPrices, klines[i].Close) - - // 计算每个点的EMA20 - if i >= 19 { - ema20 := calculateEMA(klines[:i+1], 20) - data.EMA20Values = append(data.EMA20Values, ema20) - } - - // 计算每个点的MACD - if i >= 25 { - macd := calculateMACD(klines[:i+1]) - data.MACDValues = append(data.MACDValues, macd) - } - - // 计算每个点的RSI - if i >= 7 { - rsi7 := calculateRSI(klines[:i+1], 7) - data.RSI7Values = append(data.RSI7Values, rsi7) - } - if i >= 14 { - rsi14 := calculateRSI(klines[:i+1], 14) - data.RSI14Values = append(data.RSI14Values, rsi14) - } - } - - return data -} - -// calculateLongerTermData 计算长期数据 -func calculateLongerTermData(klines []Kline) *LongerTermData { - data := &LongerTermData{ - MACDValues: make([]float64, 0, 10), - RSI14Values: make([]float64, 0, 10), - } - - // 计算EMA - data.EMA20 = calculateEMA(klines, 20) - data.EMA50 = calculateEMA(klines, 50) - - // 计算ATR - data.ATR3 = calculateATR(klines, 3) - data.ATR14 = calculateATR(klines, 14) - - // 计算成交量 - if len(klines) > 0 { - data.CurrentVolume = klines[len(klines)-1].Volume - // 计算平均成交量 - sum := 0.0 - for _, k := range klines { - sum += k.Volume - } - data.AverageVolume = sum / float64(len(klines)) - } - - // 计算MACD和RSI序列 - start := len(klines) - 10 - if start < 0 { - start = 0 - } - - for i := start; i < len(klines); i++ { - if i >= 25 { - macd := calculateMACD(klines[:i+1]) - data.MACDValues = append(data.MACDValues, macd) - } - if i >= 14 { - rsi14 := calculateRSI(klines[:i+1], 14) - data.RSI14Values = append(data.RSI14Values, rsi14) - } - } - - return data -} - -// getOpenInterestData 获取OI数据 -func getOpenInterestData(symbol string) (*OIData, error) { - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/openInterest?symbol=%s", symbol) - - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var result struct { - OpenInterest string `json:"openInterest"` - Symbol string `json:"symbol"` - Time int64 `json:"time"` - } - - if err := json.Unmarshal(body, &result); err != nil { - return nil, err - } - - oi, _ := strconv.ParseFloat(result.OpenInterest, 64) - - return &OIData{ - Latest: oi, - Average: oi * 0.999, // 近似平均值 - }, nil -} - -// getFundingRate 获取资金费率 -func getFundingRate(symbol string) (float64, error) { - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=%s", symbol) - - resp, err := http.Get(url) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return 0, err - } - - var result struct { - Symbol string `json:"symbol"` - MarkPrice string `json:"markPrice"` - IndexPrice string `json:"indexPrice"` - LastFundingRate string `json:"lastFundingRate"` - NextFundingTime int64 `json:"nextFundingTime"` - InterestRate string `json:"interestRate"` - Time int64 `json:"time"` - } - - if err := json.Unmarshal(body, &result); err != nil { - return 0, err - } - - rate, _ := strconv.ParseFloat(result.LastFundingRate, 64) - return rate, nil -} - -// FormatMarketData 格式化输出市场数据 -func FormatMarketData(data *MarketData) string { - var sb strings.Builder - - sb.WriteString(fmt.Sprintf("current_price = %.2f, current_ema20 = %.3f, current_macd = %.3f, current_rsi (7 period) = %.3f\n\n", - data.CurrentPrice, data.CurrentEMA20, data.CurrentMACD, data.CurrentRSI7)) - - sb.WriteString(fmt.Sprintf("In addition, here is the latest %s open interest and funding rate for perps:\n\n", - data.Symbol)) - - if data.OpenInterest != nil { - sb.WriteString(fmt.Sprintf("Open Interest: Latest: %.2f Average: %.2f\n\n", - data.OpenInterest.Latest, data.OpenInterest.Average)) - } - - sb.WriteString(fmt.Sprintf("Funding Rate: %.2e\n\n", data.FundingRate)) - - if data.IntradaySeries != nil { - sb.WriteString("Intraday series (3‑minute intervals, oldest → latest):\n\n") - - if len(data.IntradaySeries.MidPrices) > 0 { - sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.IntradaySeries.MidPrices))) - } - - if len(data.IntradaySeries.EMA20Values) > 0 { - sb.WriteString(fmt.Sprintf("EMA indicators (20‑period): %s\n\n", formatFloatSlice(data.IntradaySeries.EMA20Values))) - } - - if len(data.IntradaySeries.MACDValues) > 0 { - sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.IntradaySeries.MACDValues))) - } - - if len(data.IntradaySeries.RSI7Values) > 0 { - sb.WriteString(fmt.Sprintf("RSI indicators (7‑Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI7Values))) - } - - if len(data.IntradaySeries.RSI14Values) > 0 { - sb.WriteString(fmt.Sprintf("RSI indicators (14‑Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI14Values))) - } - } - - if data.LongerTermContext != nil { - sb.WriteString("Longer‑term context (4‑hour timeframe):\n\n") - - sb.WriteString(fmt.Sprintf("20‑Period EMA: %.3f vs. 50‑Period EMA: %.3f\n\n", - data.LongerTermContext.EMA20, data.LongerTermContext.EMA50)) - - sb.WriteString(fmt.Sprintf("3‑Period ATR: %.3f vs. 14‑Period ATR: %.3f\n\n", - data.LongerTermContext.ATR3, data.LongerTermContext.ATR14)) - - sb.WriteString(fmt.Sprintf("Current Volume: %.3f vs. Average Volume: %.3f\n\n", - data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume)) - - if len(data.LongerTermContext.MACDValues) > 0 { - sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.LongerTermContext.MACDValues))) - } - - if len(data.LongerTermContext.RSI14Values) > 0 { - sb.WriteString(fmt.Sprintf("RSI indicators (14‑Period): %s\n\n", formatFloatSlice(data.LongerTermContext.RSI14Values))) - } - } - - return sb.String() -} - -// formatFloatSlice 格式化float64切片为字符串 -func formatFloatSlice(values []float64) string { - strValues := make([]string, len(values)) - for i, v := range values { - strValues[i] = fmt.Sprintf("%.3f", v) - } - return "[" + strings.Join(strValues, ", ") + "]" -} - -// NormalizeSymbol 标准化symbol,确保是USDT交易对 -func NormalizeSymbol(symbol string) string { - symbol = strings.ToUpper(symbol) - if strings.HasSuffix(symbol, "USDT") { - return symbol - } - return symbol + "USDT" -} - -// parseFloat 解析float值 -func parseFloat(v interface{}) (float64, error) { - switch val := v.(type) { - case string: - return strconv.ParseFloat(val, 64) - case float64: - return val, nil - case int: - return float64(val), nil - case int64: - return float64(val), nil - default: - return 0, fmt.Errorf("unsupported type: %T", v) - } -} diff --git a/scanner/ai_scanner.go b/scanner/ai_scanner.go deleted file mode 100644 index 77eb815e..00000000 --- a/scanner/ai_scanner.go +++ /dev/null @@ -1,361 +0,0 @@ -package scanner - -import ( - "fmt" - "log" - "nofx/market" - "sort" - "sync" - "time" -) - -// TradingOpportunity AI识别的交易机会 -type TradingOpportunity struct { - Symbol string - Signal market.SignalType - Confidence float64 - Reasoning string - EntryPrice float64 - StopLoss float64 - TakeProfit float64 - CurrentPrice float64 - Priority int - RiskRewardRatio float64 - AnalyzedAt time.Time -} - -// ScanConfig 扫描配置 -type ScanConfig struct { - MinConfidence float64 // 最小信心度 - MaxConcurrent int // 最大并发数 - Timeout time.Duration // 超时时间 - MinPriority int // 最小优先级 - EnableLong bool // 允许做多 - EnableShort bool // 允许做空 - MinRiskRewardRatio float64 // 最小风险回报比 -} - -var defaultScanConfig = ScanConfig{ - MinConfidence: 65.0, - MaxConcurrent: 10, - Timeout: 60 * time.Second, - MinPriority: 60, - EnableLong: true, - EnableShort: true, - MinRiskRewardRatio: 1.5, -} - -// SetScanConfig 设置扫描配置 -func SetScanConfig(config ScanConfig) { - defaultScanConfig = config -} - -// ScanMarket 扫描市场寻找交易机会 -func ScanMarket(symbols []string) ([]*TradingOpportunity, error) { - if len(symbols) == 0 { - return nil, fmt.Errorf("币种列表为空") - } - - log.Printf("🔍 开始扫描 %d 个币种...", len(symbols)) - startTime := time.Now() - - // 结果channel - oppChan := make(chan *TradingOpportunity, len(symbols)) - errChan := make(chan error, len(symbols)) - - // 并发控制 - semaphore := make(chan struct{}, defaultScanConfig.MaxConcurrent) - var wg sync.WaitGroup - - // 并发扫描 - for _, symbol := range symbols { - wg.Add(1) - go func(sym string) { - defer wg.Done() - - semaphore <- struct{}{} - defer func() { <-semaphore }() - - opp, err := scanSymbol(sym) - if err != nil { - errChan <- fmt.Errorf("%s: %v", sym, err) - return - } - - if opp != nil { - oppChan <- opp - } - }(symbol) - } - - // 等待完成 - go func() { - wg.Wait() - close(oppChan) - close(errChan) - }() - - // 收集结果 - var opportunities []*TradingOpportunity - var errorCount int - - for { - select { - case opp, ok := <-oppChan: - if !ok { - oppChan = nil - } else { - opportunities = append(opportunities, opp) - } - case err, ok := <-errChan: - if !ok { - errChan = nil - } else { - errorCount++ - if errorCount <= 3 { - log.Printf("⚠ %v", err) - } - } - } - - if oppChan == nil && errChan == nil { - break - } - } - - if errorCount > 3 { - log.Printf("⚠ 还有 %d 个错误...", errorCount-3) - } - - // 排序 - sort.Slice(opportunities, func(i, j int) bool { - return opportunities[i].Priority > opportunities[j].Priority - }) - - elapsed := time.Since(startTime) - log.Printf("✓ 扫描完成,耗时 %.1fs,找到 %d 个交易机会", elapsed.Seconds(), len(opportunities)) - - return opportunities, nil -} - -// scanSymbol 扫描单个币种 -func scanSymbol(symbol string) (*TradingOpportunity, error) { - // 1. 获取市场数据 - marketData, err := market.GetMarketData(symbol) - if err != nil { - return nil, err - } - - // 2. 获取AI信号 - signal, err := market.GetAITradingSignal(symbol) - if err != nil { - return nil, err - } - - // 3. 验证信号 - if !isValidTradingSignal(signal) { - return nil, nil - } - - // 4. 计算指标 - priority := calculatePriorityScore(signal, marketData) - rrr := calculateRiskReward(signal) - - // 5. 过滤 - if priority < defaultScanConfig.MinPriority { - return nil, nil - } - - if rrr < defaultScanConfig.MinRiskRewardRatio { - return nil, nil - } - - return &TradingOpportunity{ - Symbol: symbol, - Signal: signal.Signal, - Confidence: signal.Confidence, - Reasoning: signal.Reasoning, - EntryPrice: signal.EntryPrice, - StopLoss: signal.StopLoss, - TakeProfit: signal.TakeProfit, - CurrentPrice: marketData.CurrentPrice, - Priority: priority, - RiskRewardRatio: rrr, - AnalyzedAt: time.Now(), - }, nil -} - -// isValidTradingSignal 验证信号有效性 -func isValidTradingSignal(signal *market.TradingSignal) bool { - // 1. 信心度检查 - if signal.Confidence < defaultScanConfig.MinConfidence { - return false - } - - // 2. 信号类型检查 - switch signal.Signal { - case market.SignalOpenLong: - if !defaultScanConfig.EnableLong { - return false - } - // 做多:止损<入场<止盈 - if signal.StopLoss >= signal.EntryPrice || signal.TakeProfit <= signal.EntryPrice { - return false - } - case market.SignalOpenShort: - if !defaultScanConfig.EnableShort { - return false - } - // 做空:止盈<入场<止损 - if signal.TakeProfit >= signal.EntryPrice || signal.StopLoss <= signal.EntryPrice { - return false - } - default: - // 其他信号类型不用于开仓 - return false - } - - // 3. 价格合理性 - if signal.EntryPrice <= 0 || signal.StopLoss <= 0 || signal.TakeProfit <= 0 { - return false - } - - return true -} - -// calculateRiskReward 计算风险回报比 -func calculateRiskReward(signal *market.TradingSignal) float64 { - var risk, reward float64 - - if signal.Signal == market.SignalOpenLong { - risk = signal.EntryPrice - signal.StopLoss - reward = signal.TakeProfit - signal.EntryPrice - } else if signal.Signal == market.SignalOpenShort { - risk = signal.StopLoss - signal.EntryPrice - reward = signal.EntryPrice - signal.TakeProfit - } - - if risk > 0 { - return reward / risk - } - return 0 -} - -// calculatePriorityScore 计算优先级评分 -func calculatePriorityScore(signal *market.TradingSignal, data *market.MarketData) int { - score := 0 - - // 1. 信心度 (0-40分) - score += int(signal.Confidence * 0.4) - - // 2. 风险回报比 (0-25分) - rrr := calculateRiskReward(signal) - if rrr >= 3.0 { - score += 25 - } else if rrr >= 2.5 { - score += 20 - } else if rrr >= 2.0 { - score += 15 - } else if rrr >= 1.5 { - score += 10 - } - - // 3. 技术指标确认 (0-25分) - techScore := 0 - - // RSI - if signal.Signal == market.SignalOpenLong && data.CurrentRSI7 < 35 { - techScore += 7 // 超卖做多 - } else if signal.Signal == market.SignalOpenShort && data.CurrentRSI7 > 65 { - techScore += 7 // 超买做空 - } else if signal.Signal == market.SignalOpenLong && data.CurrentRSI7 < 45 { - techScore += 3 - } else if signal.Signal == market.SignalOpenShort && data.CurrentRSI7 > 55 { - techScore += 3 - } - - // MACD - if signal.Signal == market.SignalOpenLong && data.CurrentMACD > 0 { - techScore += 6 - } else if signal.Signal == market.SignalOpenShort && data.CurrentMACD < 0 { - techScore += 6 - } - - // EMA趋势 - if signal.Signal == market.SignalOpenLong && data.CurrentPrice > data.CurrentEMA20 { - techScore += 6 - } else if signal.Signal == market.SignalOpenShort && data.CurrentPrice < data.CurrentEMA20 { - techScore += 6 - } - - // 资金费率 - if data.FundingRate != 0 { - if signal.Signal == market.SignalOpenLong && data.FundingRate < -0.0001 { - techScore += 6 - } else if signal.Signal == market.SignalOpenShort && data.FundingRate > 0.0001 { - techScore += 6 - } - } - - score += techScore - - // 4. 成交量 (0-10分) - if data.LongerTermContext != nil && data.LongerTermContext.AverageVolume > 0 { - volumeRatio := data.LongerTermContext.CurrentVolume / data.LongerTermContext.AverageVolume - if volumeRatio > 2.0 { - score += 10 - } else if volumeRatio > 1.5 { - score += 7 - } else if volumeRatio > 1.2 { - score += 4 - } - } - - return score -} - -// FilterTopN 筛选前N个机会 -func FilterTopN(opportunities []*TradingOpportunity, n int) []*TradingOpportunity { - if len(opportunities) <= n { - return opportunities - } - return opportunities[:n] -} - -// PrintOpportunity 打印交易机会 -func PrintOpportunity(opp *TradingOpportunity, index int) { - fmt.Printf("\n【机会 #%d】%s\n", index+1, opp.Symbol) - fmt.Printf(" 信号: %s\n", GetSignalText(opp.Signal)) - fmt.Printf(" 信心度: %.1f%% | 优先级: %d/100\n", opp.Confidence, opp.Priority) - fmt.Printf(" 当前价: %.4f USDT\n", opp.CurrentPrice) - fmt.Printf(" 入场价: %.4f USDT\n", opp.EntryPrice) - fmt.Printf(" 止损价: %.4f USDT (风险: %.2f%%)\n", opp.StopLoss, calculateRiskPercent(opp)) - fmt.Printf(" 止盈价: %.4f USDT (收益: %.2f%%)\n", opp.TakeProfit, calculateRewardPercent(opp)) - fmt.Printf(" 风险回报比: 1:%.2f\n", opp.RiskRewardRatio) - fmt.Printf(" 分析: %s\n", opp.Reasoning) -} - -func GetSignalText(signal market.SignalType) string { - switch signal { - case market.SignalOpenLong: - return "开多 🟢" - case market.SignalOpenShort: - return "开空 🔴" - default: - return string(signal) - } -} - -func calculateRiskPercent(opp *TradingOpportunity) float64 { - if opp.Signal == market.SignalOpenLong { - return ((opp.EntryPrice - opp.StopLoss) / opp.EntryPrice) * 100 - } - return ((opp.StopLoss - opp.EntryPrice) / opp.EntryPrice) * 100 -} - -func calculateRewardPercent(opp *TradingOpportunity) float64 { - if opp.Signal == market.SignalOpenLong { - return ((opp.TakeProfit - opp.EntryPrice) / opp.EntryPrice) * 100 - } - return ((opp.EntryPrice - opp.TakeProfit) / opp.EntryPrice) * 100 -}