mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
5aa50d35d7
- Multi-AI competition mode (Qwen vs DeepSeek) - Binance Futures integration - AI self-learning mechanism - Professional web dashboard - Complete risk management system
362 lines
8.8 KiB
Go
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
|
|
}
|