mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
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 <tinklefund@gmail.com>
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user