Files
nofx/scanner/ai_scanner.go
T
tinkle-community 5aa50d35d7 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
2025-10-28 15:47:34 +08:00

362 lines
8.8 KiB
Go

package scanner
import (
"fmt"
"log"
"nofx/market"
"sort"
"sync"
"time"
)
// TradingOpportunity AI识别的交易机会
type TradingOpportunity struct {
Symbol string
Signal market.SignalType
Confidence float64
Reasoning string
EntryPrice float64
StopLoss float64
TakeProfit float64
CurrentPrice float64
Priority int
RiskRewardRatio float64
AnalyzedAt time.Time
}
// ScanConfig 扫描配置
type ScanConfig struct {
MinConfidence float64 // 最小信心度
MaxConcurrent int // 最大并发数
Timeout time.Duration // 超时时间
MinPriority int // 最小优先级
EnableLong bool // 允许做多
EnableShort bool // 允许做空
MinRiskRewardRatio float64 // 最小风险回报比
}
var defaultScanConfig = ScanConfig{
MinConfidence: 65.0,
MaxConcurrent: 10,
Timeout: 60 * time.Second,
MinPriority: 60,
EnableLong: true,
EnableShort: true,
MinRiskRewardRatio: 1.5,
}
// SetScanConfig 设置扫描配置
func SetScanConfig(config ScanConfig) {
defaultScanConfig = config
}
// ScanMarket 扫描市场寻找交易机会
func ScanMarket(symbols []string) ([]*TradingOpportunity, error) {
if len(symbols) == 0 {
return nil, fmt.Errorf("币种列表为空")
}
log.Printf("🔍 开始扫描 %d 个币种...", len(symbols))
startTime := time.Now()
// 结果channel
oppChan := make(chan *TradingOpportunity, len(symbols))
errChan := make(chan error, len(symbols))
// 并发控制
semaphore := make(chan struct{}, defaultScanConfig.MaxConcurrent)
var wg sync.WaitGroup
// 并发扫描
for _, symbol := range symbols {
wg.Add(1)
go func(sym string) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
opp, err := scanSymbol(sym)
if err != nil {
errChan <- fmt.Errorf("%s: %v", sym, err)
return
}
if opp != nil {
oppChan <- opp
}
}(symbol)
}
// 等待完成
go func() {
wg.Wait()
close(oppChan)
close(errChan)
}()
// 收集结果
var opportunities []*TradingOpportunity
var errorCount int
for {
select {
case opp, ok := <-oppChan:
if !ok {
oppChan = nil
} else {
opportunities = append(opportunities, opp)
}
case err, ok := <-errChan:
if !ok {
errChan = nil
} else {
errorCount++
if errorCount <= 3 {
log.Printf("⚠ %v", err)
}
}
}
if oppChan == nil && errChan == nil {
break
}
}
if errorCount > 3 {
log.Printf("⚠ 还有 %d 个错误...", errorCount-3)
}
// 排序
sort.Slice(opportunities, func(i, j int) bool {
return opportunities[i].Priority > opportunities[j].Priority
})
elapsed := time.Since(startTime)
log.Printf("✓ 扫描完成,耗时 %.1fs,找到 %d 个交易机会", elapsed.Seconds(), len(opportunities))
return opportunities, nil
}
// scanSymbol 扫描单个币种
func scanSymbol(symbol string) (*TradingOpportunity, error) {
// 1. 获取市场数据
marketData, err := market.GetMarketData(symbol)
if err != nil {
return nil, err
}
// 2. 获取AI信号
signal, err := market.GetAITradingSignal(symbol)
if err != nil {
return nil, err
}
// 3. 验证信号
if !isValidTradingSignal(signal) {
return nil, nil
}
// 4. 计算指标
priority := calculatePriorityScore(signal, marketData)
rrr := calculateRiskReward(signal)
// 5. 过滤
if priority < defaultScanConfig.MinPriority {
return nil, nil
}
if rrr < defaultScanConfig.MinRiskRewardRatio {
return nil, nil
}
return &TradingOpportunity{
Symbol: symbol,
Signal: signal.Signal,
Confidence: signal.Confidence,
Reasoning: signal.Reasoning,
EntryPrice: signal.EntryPrice,
StopLoss: signal.StopLoss,
TakeProfit: signal.TakeProfit,
CurrentPrice: marketData.CurrentPrice,
Priority: priority,
RiskRewardRatio: rrr,
AnalyzedAt: time.Now(),
}, nil
}
// isValidTradingSignal 验证信号有效性
func isValidTradingSignal(signal *market.TradingSignal) bool {
// 1. 信心度检查
if signal.Confidence < defaultScanConfig.MinConfidence {
return false
}
// 2. 信号类型检查
switch signal.Signal {
case market.SignalOpenLong:
if !defaultScanConfig.EnableLong {
return false
}
// 做多:止损<入场<止盈
if signal.StopLoss >= signal.EntryPrice || signal.TakeProfit <= signal.EntryPrice {
return false
}
case market.SignalOpenShort:
if !defaultScanConfig.EnableShort {
return false
}
// 做空:止盈<入场<止损
if signal.TakeProfit >= signal.EntryPrice || signal.StopLoss <= signal.EntryPrice {
return false
}
default:
// 其他信号类型不用于开仓
return false
}
// 3. 价格合理性
if signal.EntryPrice <= 0 || signal.StopLoss <= 0 || signal.TakeProfit <= 0 {
return false
}
return true
}
// calculateRiskReward 计算风险回报比
func calculateRiskReward(signal *market.TradingSignal) float64 {
var risk, reward float64
if signal.Signal == market.SignalOpenLong {
risk = signal.EntryPrice - signal.StopLoss
reward = signal.TakeProfit - signal.EntryPrice
} else if signal.Signal == market.SignalOpenShort {
risk = signal.StopLoss - signal.EntryPrice
reward = signal.EntryPrice - signal.TakeProfit
}
if risk > 0 {
return reward / risk
}
return 0
}
// calculatePriorityScore 计算优先级评分
func calculatePriorityScore(signal *market.TradingSignal, data *market.MarketData) int {
score := 0
// 1. 信心度 (0-40分)
score += int(signal.Confidence * 0.4)
// 2. 风险回报比 (0-25分)
rrr := calculateRiskReward(signal)
if rrr >= 3.0 {
score += 25
} else if rrr >= 2.5 {
score += 20
} else if rrr >= 2.0 {
score += 15
} else if rrr >= 1.5 {
score += 10
}
// 3. 技术指标确认 (0-25分)
techScore := 0
// RSI
if signal.Signal == market.SignalOpenLong && data.CurrentRSI7 < 35 {
techScore += 7 // 超卖做多
} else if signal.Signal == market.SignalOpenShort && data.CurrentRSI7 > 65 {
techScore += 7 // 超买做空
} else if signal.Signal == market.SignalOpenLong && data.CurrentRSI7 < 45 {
techScore += 3
} else if signal.Signal == market.SignalOpenShort && data.CurrentRSI7 > 55 {
techScore += 3
}
// MACD
if signal.Signal == market.SignalOpenLong && data.CurrentMACD > 0 {
techScore += 6
} else if signal.Signal == market.SignalOpenShort && data.CurrentMACD < 0 {
techScore += 6
}
// EMA趋势
if signal.Signal == market.SignalOpenLong && data.CurrentPrice > data.CurrentEMA20 {
techScore += 6
} else if signal.Signal == market.SignalOpenShort && data.CurrentPrice < data.CurrentEMA20 {
techScore += 6
}
// 资金费率
if data.FundingRate != 0 {
if signal.Signal == market.SignalOpenLong && data.FundingRate < -0.0001 {
techScore += 6
} else if signal.Signal == market.SignalOpenShort && data.FundingRate > 0.0001 {
techScore += 6
}
}
score += techScore
// 4. 成交量 (0-10分)
if data.LongerTermContext != nil && data.LongerTermContext.AverageVolume > 0 {
volumeRatio := data.LongerTermContext.CurrentVolume / data.LongerTermContext.AverageVolume
if volumeRatio > 2.0 {
score += 10
} else if volumeRatio > 1.5 {
score += 7
} else if volumeRatio > 1.2 {
score += 4
}
}
return score
}
// FilterTopN 筛选前N个机会
func FilterTopN(opportunities []*TradingOpportunity, n int) []*TradingOpportunity {
if len(opportunities) <= n {
return opportunities
}
return opportunities[:n]
}
// PrintOpportunity 打印交易机会
func PrintOpportunity(opp *TradingOpportunity, index int) {
fmt.Printf("\n【机会 #%d】%s\n", index+1, opp.Symbol)
fmt.Printf(" 信号: %s\n", GetSignalText(opp.Signal))
fmt.Printf(" 信心度: %.1f%% | 优先级: %d/100\n", opp.Confidence, opp.Priority)
fmt.Printf(" 当前价: %.4f USDT\n", opp.CurrentPrice)
fmt.Printf(" 入场价: %.4f USDT\n", opp.EntryPrice)
fmt.Printf(" 止损价: %.4f USDT (风险: %.2f%%)\n", opp.StopLoss, calculateRiskPercent(opp))
fmt.Printf(" 止盈价: %.4f USDT (收益: %.2f%%)\n", opp.TakeProfit, calculateRewardPercent(opp))
fmt.Printf(" 风险回报比: 1:%.2f\n", opp.RiskRewardRatio)
fmt.Printf(" 分析: %s\n", opp.Reasoning)
}
func GetSignalText(signal market.SignalType) string {
switch signal {
case market.SignalOpenLong:
return "开多 🟢"
case market.SignalOpenShort:
return "开空 🔴"
default:
return string(signal)
}
}
func calculateRiskPercent(opp *TradingOpportunity) float64 {
if opp.Signal == market.SignalOpenLong {
return ((opp.EntryPrice - opp.StopLoss) / opp.EntryPrice) * 100
}
return ((opp.StopLoss - opp.EntryPrice) / opp.EntryPrice) * 100
}
func calculateRewardPercent(opp *TradingOpportunity) float64 {
if opp.Signal == market.SignalOpenLong {
return ((opp.TakeProfit - opp.EntryPrice) / opp.EntryPrice) * 100
}
return ((opp.EntryPrice - opp.TakeProfit) / opp.EntryPrice) * 100
}