Initial commit: NOFX AI Trading System

- Multi-AI competition mode (Qwen vs DeepSeek)
- Binance Futures integration
- AI self-learning mechanism
- Professional web dashboard
- Complete risk management system
This commit is contained in:
tinkle-community
2025-10-28 15:47:34 +08:00
parent 976d6d74f6
commit 8d3981fd3e
12369 changed files with 1739210 additions and 0 deletions

View File

@@ -0,0 +1,641 @@
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"`
Reasoning string `json:"reasoning"`
}
// AIFullDecision AI的完整决策包含思维链
type AIFullDecision struct {
CoTTrace string `json:"cot_trace"` // 思维链分析
Decisions []TradingDecision `json:"decisions"` // 具体决策列表
Timestamp time.Time `json:"timestamp"`
}
// GetFullTradingDecision 获取AI的完整交易决策批量分析所有币种和持仓
func GetFullTradingDecision(ctx *TradingContext) (*AIFullDecision, error) {
// 1. 为所有币种获取市场数据
if err := fetchMarketDataForContext(ctx); err != nil {
return nil, fmt.Errorf("获取市场数据失败: %w", err)
}
// 2. 构建AI提示
prompt := buildFullDecisionPrompt(ctx)
// 3. 调用AI API
aiResponse, err := callDeepSeekAPI(prompt)
if err != nil {
return nil, fmt.Errorf("调用AI API失败: %w", err)
}
// 4. 解析AI响应
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity)
if err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
decision.Timestamp = time.Now()
return decision, nil
}
// fetchMarketDataForContext 为上下文中的所有币种获取市场数据和OI数据
func fetchMarketDataForContext(ctx *TradingContext) error {
ctx.MarketDataMap = make(map[string]*MarketData)
ctx.OITopDataMap = make(map[string]*OITopData)
// 收集所有需要获取数据的币种
symbolSet := make(map[string]bool)
// 1. 优先获取持仓币种的数据(这是必须的)
for _, pos := range ctx.Positions {
symbolSet[pos.Symbol] = true
}
// 2. 候选币种数量根据账户状态动态调整
maxCandidates := calculateMaxCandidates(ctx)
for i, coin := range ctx.CandidateCoins {
if i >= maxCandidates {
break
}
symbolSet[coin.Symbol] = true
}
// 并发获取市场数据
// 持仓币种集合用于判断是否跳过OI检查
positionSymbols := make(map[string]bool)
for _, pos := range ctx.Positions {
positionSymbols[pos.Symbol] = true
}
for symbol := range symbolSet {
data, err := GetMarketData(symbol)
if err != nil {
// 单个币种失败不影响整体,只记录错误
continue
}
// ⚠️ 流动性过滤持仓价值低于15M USD的币种不做多空都不做
// 持仓价值 = 持仓量 × 当前价格
// 但现有持仓必须保留(需要决策是否平仓)
isExistingPosition := positionSymbols[symbol]
if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 {
// 计算持仓价值USD= 持仓量 × 当前价格
oiValue := data.OpenInterest.Latest * data.CurrentPrice
oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位
if oiValueInMillions < 15 {
log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < 15M),跳过此币种 [持仓量:%.0f × 价格:%.4f]",
symbol, oiValueInMillions, data.OpenInterest.Latest, data.CurrentPrice)
continue
}
}
ctx.MarketDataMap[symbol] = data
}
// 加载OI Top数据不影响主流程
oiPositions, err := pool.GetOITopPositions()
if err == nil {
for _, pos := range oiPositions {
// 标准化符号匹配
symbol := pos.Symbol
ctx.OITopDataMap[symbol] = &OITopData{
Rank: pos.Rank,
OIDeltaPercent: pos.OIDeltaPercent,
OIDeltaValue: pos.OIDeltaValue,
PriceDeltaPercent: pos.PriceDeltaPercent,
NetLong: pos.NetLong,
NetShort: pos.NetShort,
}
}
}
return nil
}
// calculateMaxCandidates 根据账户状态计算需要分析的候选币种数量
func calculateMaxCandidates(ctx *TradingContext) int {
// 直接返回候选池的全部币种数量
// 因为候选池已经在 auto_trader.go 中筛选过了
// 固定分析前20个评分最高的币种来自AI500
return len(ctx.CandidateCoins)
}
// buildFullDecisionPrompt 构建完整的AI决策提示
func buildFullDecisionPrompt(ctx *TradingContext) string {
var sb strings.Builder
sb.WriteString("# 🤖 加密货币交易AI竞赛系统\n\n")
sb.WriteString("你是专业的加密货币交易AI根据市场数据自主决策做多做空均可。\n\n")
// 添加BTC市场趋势
sb.WriteString("## 🌍 BTC市场趋势\n")
if btcData, hasBTC := ctx.MarketDataMap["BTCUSDT"]; hasBTC {
sb.WriteString(fmt.Sprintf("- 价格: %.2f | 1h: %+.2f%% | 4h: %+.2f%%\n",
btcData.CurrentPrice, btcData.PriceChange1h, btcData.PriceChange4h))
sb.WriteString(fmt.Sprintf("- MACD: %.4f | RSI: %.2f | 资金费率: %.6f\n\n",
btcData.CurrentMACD, btcData.CurrentRSI7, btcData.FundingRate))
} else {
sb.WriteString("BTC数据暂无\n\n")
}
// 系统状态
sb.WriteString("## 📊 系统状态\n")
sb.WriteString(fmt.Sprintf("- **当前时间**: %s\n", ctx.CurrentTime))
sb.WriteString(fmt.Sprintf("- **运行时长**: %d 分钟\n", ctx.RuntimeMinutes))
sb.WriteString(fmt.Sprintf("- **调用次数**: 第 %d 次\n\n", ctx.CallCount))
// 账户信息
sb.WriteString("## 💰 账户信息\n")
sb.WriteString(fmt.Sprintf("- **账户净值**: %.2f USDT\n", ctx.Account.TotalEquity))
sb.WriteString(fmt.Sprintf("- **可用余额**: %.2f USDT (%.1f%%)\n",
ctx.Account.AvailableBalance,
(ctx.Account.AvailableBalance/ctx.Account.TotalEquity)*100))
sb.WriteString(fmt.Sprintf("- **总盈亏**: %.2f USDT (%+.2f%%)\n",
ctx.Account.TotalPnL, ctx.Account.TotalPnLPct))
sb.WriteString(fmt.Sprintf("- **已用保证金**: %.2f USDT (%.1f%%)\n",
ctx.Account.MarginUsed, ctx.Account.MarginUsedPct))
sb.WriteString(fmt.Sprintf("- **持仓数量**: %d\n\n", ctx.Account.PositionCount))
// 当前持仓详情
if len(ctx.Positions) > 0 {
sb.WriteString("## 📈 当前持仓\n")
for i, pos := range ctx.Positions {
sb.WriteString(fmt.Sprintf("\n### 持仓 #%d: %s %s\n", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
sb.WriteString(fmt.Sprintf("- **入场价**: %.4f USDT\n", pos.EntryPrice))
sb.WriteString(fmt.Sprintf("- **当前价**: %.4f USDT\n", pos.MarkPrice))
sb.WriteString(fmt.Sprintf("- **数量**: %.4f\n", pos.Quantity))
sb.WriteString(fmt.Sprintf("- **杠杆**: %dx\n", pos.Leverage))
sb.WriteString(fmt.Sprintf("- **未实现盈亏**: %.2f USDT (%+.2f%%)\n",
pos.UnrealizedPnL, pos.UnrealizedPnLPct))
sb.WriteString(fmt.Sprintf("- **强平价**: %.4f USDT\n", pos.LiquidationPrice))
sb.WriteString(fmt.Sprintf("- **占用保证金**: %.2f USDT\n", pos.MarginUsed))
// 添加市场数据
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(formatMarketDataBrief(marketData))
}
}
sb.WriteString("\n")
} else {
sb.WriteString("## 📈 当前持仓\n")
sb.WriteString("暂无持仓\n\n")
}
// 候选币种池
sb.WriteString("## 🎯 候选币种池\n")
sb.WriteString(fmt.Sprintf("共 %d 个币种(已过滤持仓价值<15M USD的低流动性币种\n\n", len(ctx.MarketDataMap)))
displayedCount := 0
for _, coin := range ctx.CandidateCoins {
// 只显示已获取市场数据的币种
marketData, hasData := ctx.MarketDataMap[coin.Symbol]
if !hasData {
continue
}
displayedCount++
// 显示币种来源标签 - 使用圆括号避免与JSON混淆
sourceTags := ""
hasAI500 := false
hasOITop := false
for _, source := range coin.Sources {
if source == "ai500" {
hasAI500 = true
} else if source == "oi_top" {
hasOITop = true
}
}
if hasAI500 && hasOITop {
sourceTags = "(AI500+OI_Top双重信号)"
} else if hasAI500 {
sourceTags = "(AI500高评分)"
} else if hasOITop {
sourceTags = "(OI_Top持仓增长)"
}
sb.WriteString(fmt.Sprintf("\n### 币种 #%d: %s %s\n", displayedCount, coin.Symbol, sourceTags))
sb.WriteString(formatMarketDataBrief(marketData))
// 如果有OI Top数据也显示出来
if oiTopData, hasOI := ctx.OITopDataMap[coin.Symbol]; hasOI {
sb.WriteString(fmt.Sprintf("**市场热度数据** (OI Top排名 #%d):\n", oiTopData.Rank))
sb.WriteString(fmt.Sprintf(" - 持仓量1h变化: %+.2f%% (价值: $%.0f)\n",
oiTopData.OIDeltaPercent, oiTopData.OIDeltaValue))
sb.WriteString(fmt.Sprintf(" - 价格1h变化: %+.2f%% | 净多仓: %.0f | 净空仓: %.0f\n",
oiTopData.PriceDeltaPercent, oiTopData.NetLong, oiTopData.NetShort))
}
}
// 添加历史表现反馈(如果有)
if ctx.Performance != nil {
sb.WriteString(formatPerformanceFeedback(ctx.Performance))
}
// AI决策要求
sb.WriteString("## 🎯 任务\n\n")
sb.WriteString("分析市场数据,自主决策:\n")
sb.WriteString("1. 评估现有持仓 → 持有或平仓\n")
sb.WriteString(fmt.Sprintf("2. 从%d个候选币种中找交易机会\n", len(ctx.MarketDataMap)))
sb.WriteString("3. 开新仓(如果有机会)\n\n")
sb.WriteString("## 📋 规则\n\n")
sb.WriteString(fmt.Sprintf("1. **单币种仓位上限**: 山寨币≤%.0f USDT | BTC/ETH≤%.0f USDT\n", ctx.Account.TotalEquity*1.5, ctx.Account.TotalEquity*10))
sb.WriteString("2. **杠杆**: 山寨币=20倍 | BTC/ETH=50倍\n")
sb.WriteString("3. **保证金上限**: 总使用率≤90%%\n")
sb.WriteString("4. **风险回报比**: ≥1:2\n\n")
sb.WriteString("### 📤 输出格式\n\n")
sb.WriteString("**思维链分析** (纯文本)\n")
sb.WriteString("- 分析持仓 → 找新机会 → 账户检查\n")
sb.WriteString("- **最后必须列出最终决策摘要**例如持有XX平仓XX开多XX开空XX\n\n")
sb.WriteString("---\n\n")
sb.WriteString("**决策JSON** (不要用```标记)\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": 15000, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"突破做多\"},\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"hold\", \"reasoning\": \"持续观察\"}\n")
sb.WriteString("]\n\n")
sb.WriteString("**action类型**: open_long | open_short | close_long | close_short | hold | wait\n")
sb.WriteString("**开仓必填**: leverage, position_size_usd, stop_loss, take_profit\n")
sb.WriteString("**position_size_usd**: 仓位价值(非保证金),保证金=position_size_usd/leverage\n\n")
sb.WriteString("### 📝 完整示例\n\n")
// 简化示例仓位(使用新的仓位上限)
btcSize := ctx.Account.TotalEquity * 8 // BTC示例8倍净值不超过10倍上限
altSize := ctx.Account.TotalEquity * 1 // 山寨币示例1倍净值不超过1.5倍上限)
sb.WriteString("**思维链**:\n")
sb.WriteString("当前持仓ETHUSDT多头盈利+2.3%,趋势良好继续持有。\n")
sb.WriteString("新机会BTC突破上涨MACD金叉资金费率低做多信号强。\n")
sb.WriteString(" SOLUSDT回调至支撑位出现反弹信号可小仓位做多。\n")
sb.WriteString("账户可用余额充足保证金使用率32%,可分散开仓。\n")
sb.WriteString("**最终决策**持有ETHUSDT开多BTCUSDT(8倍净值)开多SOLUSDT(1倍净值)。\n\n")
sb.WriteString("---\n\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"hold\", \"reasoning\": \"盈利良好,趋势延续\"},\n")
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"突破做多\"},\n", btcSize))
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"SOLUSDT\", \"action\": \"open_long\", \"leverage\": 20, \"position_size_usd\": %.0f, \"stop_loss\": 180, \"take_profit\": 210, \"reasoning\": \"支撑位反弹\"}\n", altSize))
sb.WriteString("]\n\n")
sb.WriteString("现在请开始分析并给出你的决策!\n")
return sb.String()
}
// formatPerformanceFeedback 格式化历史表现反馈
func formatPerformanceFeedback(perfInterface interface{}) string {
// 类型断言避免循环依赖使用interface{}
type TradeOutcome struct {
Symbol string
Side string
OpenPrice float64
ClosePrice float64
PnL float64
PnLPct float64
Duration string
}
type SymbolPerformance struct {
Symbol string
TotalTrades int
WinningTrades int
LosingTrades int
WinRate float64
TotalPnL float64
AvgPnL float64
}
type PerformanceAnalysis struct {
TotalTrades int
WinningTrades int
LosingTrades int
WinRate float64
AvgWin float64
AvgLoss float64
ProfitFactor float64
RecentTrades []TradeOutcome
SymbolStats map[string]*SymbolPerformance
BestSymbol string
WorstSymbol string
}
// 使用JSON转换进行类型转换避免直接类型断言
jsonData, _ := json.Marshal(perfInterface)
var perf PerformanceAnalysis
if err := json.Unmarshal(jsonData, &perf); err != nil {
return ""
}
var sb strings.Builder
sb.WriteString("## 📊 历史表现反馈\n\n")
if perf.TotalTrades == 0 {
sb.WriteString("暂无历史交易数据\n\n")
return sb.String()
}
// 整体统计
sb.WriteString("### 整体表现\n")
sb.WriteString(fmt.Sprintf("- **总交易数**: %d 笔 (盈利: %d | 亏损: %d)\n",
perf.TotalTrades, perf.WinningTrades, perf.LosingTrades))
sb.WriteString(fmt.Sprintf("- **胜率**: %.1f%%\n", perf.WinRate))
sb.WriteString(fmt.Sprintf("- **平均盈利**: +%.2f%% | 平均亏损: %.2f%%\n",
perf.AvgWin, perf.AvgLoss))
if perf.ProfitFactor > 0 {
sb.WriteString(fmt.Sprintf("- **盈亏比**: %.2f:1\n", perf.ProfitFactor))
}
sb.WriteString("\n")
// 最近交易
if len(perf.RecentTrades) > 0 {
sb.WriteString("### 最近交易\n")
displayCount := len(perf.RecentTrades)
if displayCount > 5 {
displayCount = 5
}
for i := 0; i < displayCount; i++ {
trade := perf.RecentTrades[i]
outcome := "✓"
if trade.PnL < 0 {
outcome = "✗"
}
sb.WriteString(fmt.Sprintf("%d. %s %s: %.4f → %.4f = %+.2f%% %s\n",
i+1, trade.Symbol, strings.ToUpper(trade.Side),
trade.OpenPrice, trade.ClosePrice,
trade.PnLPct, outcome))
}
sb.WriteString("\n")
}
// 币种表现显示前3个最好和最差
if len(perf.SymbolStats) > 0 {
sb.WriteString("### 币种表现\n")
if perf.BestSymbol != "" {
if stats, exists := perf.SymbolStats[perf.BestSymbol]; exists {
sb.WriteString(fmt.Sprintf("- **最佳**: %s (胜率%.0f%%, 平均%+.2f%%)\n",
stats.Symbol, stats.WinRate, stats.AvgPnL))
}
}
if perf.WorstSymbol != "" {
if stats, exists := perf.SymbolStats[perf.WorstSymbol]; exists {
sb.WriteString(fmt.Sprintf("- **最差**: %s (胜率%.0f%%, 平均%+.2f%%)\n",
stats.Symbol, stats.WinRate, stats.AvgPnL))
}
}
sb.WriteString("\n")
}
return sb.String()
}
// formatMarketDataBrief 格式化市场数据(简洁版)
func formatMarketDataBrief(data *MarketData) string {
var sb strings.Builder
sb.WriteString("**市场数据** (3分钟线):\n")
sb.WriteString(fmt.Sprintf(" - 价格: %.4f | 1h变化: %+.2f%% | 4h变化: %+.2f%% (%s)\n",
data.CurrentPrice, data.PriceChange1h, data.PriceChange4h, priceTrend(data.PriceChange1h, data.PriceChange4h)))
sb.WriteString(fmt.Sprintf(" - EMA20: %.4f (%s) | MACD: %.4f (%s) | RSI(7): %.2f (%s)\n",
data.CurrentEMA20, pricePosition(data.CurrentPrice, data.CurrentEMA20),
data.CurrentMACD, macdTrend(data.CurrentMACD), data.CurrentRSI7, rsiStatus(data.CurrentRSI7)))
if data.OpenInterest != nil {
oiChange := ((data.OpenInterest.Latest - data.OpenInterest.Average) / data.OpenInterest.Average) * 100
fundingSignal := fundingRateSignal(data.FundingRate)
sb.WriteString(fmt.Sprintf(" - 持仓量: %+.2f%% | 资金费率: %.6f (%s)\n", oiChange, data.FundingRate, fundingSignal))
}
return sb.String()
}
// parseFullDecisionResponse 解析AI的完整决策响应
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*AIFullDecision, error) {
// 1. 提取 cot_trace思维链
cotTrace := extractCoTTrace(aiResponse)
// 2. 提取 JSON 决策列表
decisions, err := extractDecisions(aiResponse)
if err != nil {
// 即使JSON解析失败也返回思维链
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
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
}
// validateDecisions 验证所有决策(需要账户信息)
func validateDecisions(decisions []TradingDecision, accountEquity float64) error {
for i, decision := range decisions {
if err := validateDecision(&decision, accountEquity); err != nil {
return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
}
}
return nil
}
// findMatchingBracket 查找匹配的右括号
func findMatchingBracket(s string, start int) int {
if start >= len(s) || s[start] != '[' {
return -1
}
depth := 0
for i := start; i < len(s); i++ {
switch s[i] {
case '[':
depth++
case ']':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// validateDecision 验证单个决策的有效性
func validateDecision(d *TradingDecision, accountEquity float64) error {
// 验证action
validActions := map[string]bool{
"open_long": true,
"open_short": true,
"close_long": true,
"close_short": true,
"hold": true,
"wait": true,
}
if !validActions[d.Action] {
return fmt.Errorf("无效的action: %s", d.Action)
}
// 开仓操作必须提供完整参数
if d.Action == "open_long" || d.Action == "open_short" {
// 根据币种判断杠杆上限和仓位价值上限
maxLeverage := 20 // 山寨币固定20倍
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
maxLeverage = 50 // BTC和ETH固定50倍
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
}
if d.Leverage <= 0 || d.Leverage > maxLeverage {
return fmt.Errorf("杠杆必须在1-%d之间%s: %d", maxLeverage, d.Symbol, d.Leverage)
}
if d.PositionSizeUSD <= 0 {
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
}
// 验证仓位价值上限加1%容差以避免浮点数精度问题)
tolerance := maxPositionValue * 0.01 // 1%容差
if d.PositionSizeUSD > maxPositionValue+tolerance {
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
return fmt.Errorf("BTC/ETH单币种仓位价值不能超过%.0f USDT10倍账户净值实际: %.0f", maxPositionValue, d.PositionSizeUSD)
} else {
return fmt.Errorf("山寨币单币种仓位价值不能超过%.0f USDT1.5倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD)
}
}
if d.StopLoss <= 0 || d.TakeProfit <= 0 {
return fmt.Errorf("止损和止盈必须大于0")
}
// 验证止损止盈的合理性
if d.Action == "open_long" {
if d.StopLoss >= d.TakeProfit {
return fmt.Errorf("做多时止损价必须小于止盈价")
}
} else {
if d.StopLoss <= d.TakeProfit {
return fmt.Errorf("做空时止损价必须大于止盈价")
}
}
}
return nil
}

579
market/ai_signal.go Normal file
View File

@@ -0,0 +1,579 @@
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带重试机制
func callDeepSeekAPI(prompt 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 := callDeepSeekAPIOnce(prompt)
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) {
// 构建请求体
requestBody := map[string]interface{}{
"model": defaultConfig.Model,
"messages": []map[string]string{
{
"role": "user",
"content": prompt,
},
},
"temperature": 0.7,
"max_tokens": 2000,
}
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()
}

552
market/market_data.go Normal file
View File

@@ -0,0 +1,552 @@
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 (3minute 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 (20period): %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 (7Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI7Values)))
}
if len(data.IntradaySeries.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI14Values)))
}
}
if data.LongerTermContext != nil {
sb.WriteString("Longerterm context (4hour timeframe):\n\n")
sb.WriteString(fmt.Sprintf("20Period EMA: %.3f vs. 50Period EMA: %.3f\n\n",
data.LongerTermContext.EMA20, data.LongerTermContext.EMA50))
sb.WriteString(fmt.Sprintf("3Period ATR: %.3f vs. 14Period 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 (14Period): %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)
}
}