mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 01:44:38 +08:00
feat(market): Add 15m/1h timeframes for comprehensive trend analysis
## Problem Merged WebSocket architecture (nofxaios/dev) only supported 3m/4h intervals, but adaptive.txt v5.5.6.1 requires 15m/1h data for: - BTC state evaluation (line 105-107) - Multi-confirmation checklist (line 146, 159) - False breakout detection (line 176, 182) - Confidence scoring (line 197) ## Solution Add 15m and 1h timeframe support across WebSocket pipeline: ### 1. market/monitor.go (+64/-40 lines) - Add klineDataMap15m and klineDataMap1h to WSMonitor struct - Update subKlineTime: ["3m", "4h"] → ["3m", "15m", "1h", "4h"] - Extend initializeHistoricalData() to load 15m/1h historical klines - Update getKlineDataMap() switch to handle all 4 timeframes - Fix bug: line 109 used wrong variable (klines vs klines4h) ### 2. market/types.go (+26/-1 lines) - Add MidTermData15m struct (15-minute short-term trend filtering) - Add MidTermData1h struct (1-hour mid-term trend confirmation) - Update Data struct to include: - MidTermSeries15m *MidTermData15m - MidTermSeries1h *MidTermData1h ### 3. market/data.go (+171/-2 lines) - Update Get() to fetch klines15m and klines1h via WebSocket - Implement calculateMidTermSeries15m() - computes EMA20, MACD, RSI7/14 for 15m - Implement calculateMidTermSeries1h() - computes EMA20, MACD, RSI7/14 for 1h - Update Format() to output 15m/1h series data for AI prompt context ## Impact Assessment ### WebSocket Load (Binance limit: 1024 streams/connection) - 8 coins × 4 timeframes = 32 streams (3% usage) ✅ - 100 coins × 4 timeframes = 400 streams (39% usage) ✅ - 250 coins × 4 timeframes = 1000 streams (98% usage) ⚠️ ### Benefits - Enables adaptive.txt standard mode: 15m/1h/4h multi-timeframe confirmation - Restores false breakout detection: 15m RSI vs 1h RSI divergence checks - Improves confidence scoring: 15m/1h/4h MACD alignment validation - Zero REST API calls (WebSocket cache, no rate limit risk) ## Testing Notes - Monitor initial subscription logs for "已加载 X 的历史K线数据-15m/1h" - Verify AI prompts contain "Mid-term series (15-minute intervals)" section - Check decision logs reference 15m/1h indicators in reasoning Related: adaptive.txt v5.5.6.1 requirements, NoFxAiOS/dev WebSocket merge
This commit is contained in:
171
market/data.go
171
market/data.go
@@ -12,18 +12,31 @@ import (
|
|||||||
|
|
||||||
// Get 获取指定代币的市场数据
|
// Get 获取指定代币的市场数据
|
||||||
func Get(symbol string) (*Data, error) {
|
func Get(symbol string) (*Data, error) {
|
||||||
var klines3m, klines4h []Kline
|
var klines3m, klines15m, klines1h, klines4h []Kline
|
||||||
var err error
|
var err error
|
||||||
// 标准化symbol
|
// 标准化symbol
|
||||||
symbol = Normalize(symbol)
|
symbol = Normalize(symbol)
|
||||||
|
|
||||||
// 获取3分钟K线数据 (最近10个)
|
// 获取3分钟K线数据 (最近10个)
|
||||||
klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m") // 多获取一些用于计算
|
klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取3分钟K线失败: %v", err)
|
return nil, fmt.Errorf("获取3分钟K线失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取4小时K线数据 (最近10个)
|
// 获取15分钟K线数据 (最近40个) - 短期趋势
|
||||||
klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h") // 多获取用于计算指标
|
klines15m, err = WSMonitorCli.GetCurrentKlines(symbol, "15m")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取15分钟K线失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取1小时K线数据 (最近60个) - 中期趋势
|
||||||
|
klines1h, err = WSMonitorCli.GetCurrentKlines(symbol, "1h")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取1小时K线失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取4小时K线数据 (最近60个) - 长期趋势
|
||||||
|
klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取4小时K线失败: %v", err)
|
return nil, fmt.Errorf("获取4小时K线失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -63,10 +76,16 @@ func Get(symbol string) (*Data, error) {
|
|||||||
// 获取Funding Rate
|
// 获取Funding Rate
|
||||||
fundingRate, _ := getFundingRate(symbol)
|
fundingRate, _ := getFundingRate(symbol)
|
||||||
|
|
||||||
// 计算日内系列数据
|
// 计算日内系列数据 (3分钟)
|
||||||
intradayData := calculateIntradaySeries(klines3m)
|
intradayData := calculateIntradaySeries(klines3m)
|
||||||
|
|
||||||
// 计算长期数据
|
// 计算15分钟系列数据
|
||||||
|
midTermData15m := calculateMidTermSeries15m(klines15m)
|
||||||
|
|
||||||
|
// 计算1小时系列数据
|
||||||
|
midTermData1h := calculateMidTermSeries1h(klines1h)
|
||||||
|
|
||||||
|
// 计算长期数据 (4小时)
|
||||||
longerTermData := calculateLongerTermData(klines4h)
|
longerTermData := calculateLongerTermData(klines4h)
|
||||||
|
|
||||||
return &Data{
|
return &Data{
|
||||||
@@ -80,6 +99,8 @@ func Get(symbol string) (*Data, error) {
|
|||||||
OpenInterest: oiData,
|
OpenInterest: oiData,
|
||||||
FundingRate: fundingRate,
|
FundingRate: fundingRate,
|
||||||
IntradaySeries: intradayData,
|
IntradaySeries: intradayData,
|
||||||
|
MidTermSeries15m: midTermData15m,
|
||||||
|
MidTermSeries1h: midTermData1h,
|
||||||
LongerTermContext: longerTermData,
|
LongerTermContext: longerTermData,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -243,6 +264,96 @@ func calculateIntradaySeries(klines []Kline) *IntradayData {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculateMidTermSeries15m 计算15分钟系列数据
|
||||||
|
func calculateMidTermSeries15m(klines []Kline) *MidTermData15m {
|
||||||
|
data := &MidTermData15m{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateMidTermSeries1h 计算1小时系列数据
|
||||||
|
func calculateMidTermSeries1h(klines []Kline) *MidTermData1h {
|
||||||
|
data := &MidTermData1h{
|
||||||
|
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 计算长期数据
|
// calculateLongerTermData 计算长期数据
|
||||||
func calculateLongerTermData(klines []Kline) *LongerTermData {
|
func calculateLongerTermData(klines []Kline) *LongerTermData {
|
||||||
data := &LongerTermData{
|
data := &LongerTermData{
|
||||||
@@ -396,6 +507,54 @@ func Format(data *Data) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data.MidTermSeries15m != nil {
|
||||||
|
sb.WriteString("Mid‑term series (15‑minute intervals, oldest → latest):\n\n")
|
||||||
|
|
||||||
|
if len(data.MidTermSeries15m.MidPrices) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidTermSeries15m.MidPrices)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries15m.EMA20Values) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("EMA indicators (20‑period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.EMA20Values)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries15m.MACDValues) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MidTermSeries15m.MACDValues)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries15m.RSI7Values) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("RSI indicators (7‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.RSI7Values)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries15m.RSI14Values) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("RSI indicators (14‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.RSI14Values)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.MidTermSeries1h != nil {
|
||||||
|
sb.WriteString("Mid‑term series (1‑hour intervals, oldest → latest):\n\n")
|
||||||
|
|
||||||
|
if len(data.MidTermSeries1h.MidPrices) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidTermSeries1h.MidPrices)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries1h.EMA20Values) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("EMA indicators (20‑period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.EMA20Values)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries1h.MACDValues) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MidTermSeries1h.MACDValues)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries1h.RSI7Values) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("RSI indicators (7‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.RSI7Values)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.MidTermSeries1h.RSI14Values) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("RSI indicators (14‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.RSI14Values)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if data.LongerTermContext != nil {
|
if data.LongerTermContext != nil {
|
||||||
sb.WriteString("Longer‑term context (4‑hour timeframe):\n\n")
|
sb.WriteString("Longer‑term context (4‑hour timeframe):\n\n")
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ type WSMonitor struct {
|
|||||||
featuresMap sync.Map
|
featuresMap sync.Map
|
||||||
alertsChan chan Alert
|
alertsChan chan Alert
|
||||||
klineDataMap3m sync.Map // 存储每个交易对的K线历史数据
|
klineDataMap3m sync.Map // 存储每个交易对的K线历史数据
|
||||||
|
klineDataMap15m sync.Map // 存储每个交易对的15分钟K线历史数据
|
||||||
|
klineDataMap1h sync.Map // 存储每个交易对的1小时K线历史数据
|
||||||
klineDataMap4h sync.Map // 存储每个交易对的K线历史数据
|
klineDataMap4h sync.Map // 存储每个交易对的K线历史数据
|
||||||
tickerDataMap sync.Map // 存储每个交易对的ticker数据
|
tickerDataMap sync.Map // 存储每个交易对的ticker数据
|
||||||
batchSize int
|
batchSize int
|
||||||
@@ -32,7 +34,7 @@ type SymbolStats struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var WSMonitorCli *WSMonitor
|
var WSMonitorCli *WSMonitor
|
||||||
var subKlineTime = []string{"3m", "4h"} // 管理订阅流的K线周期
|
var subKlineTime = []string{"3m", "15m", "1h", "4h"} // 管理订阅流的K线周期
|
||||||
|
|
||||||
func NewWSMonitor(batchSize int) *WSMonitor {
|
func NewWSMonitor(batchSize int) *WSMonitor {
|
||||||
WSMonitorCli = &WSMonitor{
|
WSMonitorCli = &WSMonitor{
|
||||||
@@ -89,25 +91,40 @@ func (m *WSMonitor) initializeHistoricalData() error {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer func() { <-semaphore }()
|
defer func() { <-semaphore }()
|
||||||
|
|
||||||
// 获取历史K线数据
|
// 获取3分钟历史K线数据
|
||||||
klines, err := apiClient.GetKlines(s, "3m", 100)
|
klines3m, err := apiClient.GetKlines(s, "3m", 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("获取 %s 历史数据失败: %v", s, err)
|
log.Printf("获取 %s 3m历史数据失败: %v", s, err)
|
||||||
return
|
} else if len(klines3m) > 0 {
|
||||||
|
m.klineDataMap3m.Store(s, klines3m)
|
||||||
|
log.Printf("已加载 %s 的历史K线数据-3m: %d 条", s, len(klines3m))
|
||||||
}
|
}
|
||||||
if len(klines) > 0 {
|
|
||||||
m.klineDataMap3m.Store(s, klines)
|
// 获取15分钟历史K线数据
|
||||||
log.Printf("已加载 %s 的历史K线数据-3m: %d 条", s, len(klines))
|
klines15m, err := apiClient.GetKlines(s, "15m", 100)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取 %s 15m历史数据失败: %v", s, err)
|
||||||
|
} else if len(klines15m) > 0 {
|
||||||
|
m.klineDataMap15m.Store(s, klines15m)
|
||||||
|
log.Printf("已加载 %s 的历史K线数据-15m: %d 条", s, len(klines15m))
|
||||||
}
|
}
|
||||||
// 获取历史K线数据
|
|
||||||
|
// 获取1小时历史K线数据
|
||||||
|
klines1h, err := apiClient.GetKlines(s, "1h", 100)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取 %s 1h历史数据失败: %v", s, err)
|
||||||
|
} else if len(klines1h) > 0 {
|
||||||
|
m.klineDataMap1h.Store(s, klines1h)
|
||||||
|
log.Printf("已加载 %s 的历史K线数据-1h: %d 条", s, len(klines1h))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取4小时历史K线数据
|
||||||
klines4h, err := apiClient.GetKlines(s, "4h", 100)
|
klines4h, err := apiClient.GetKlines(s, "4h", 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("获取 %s 历史数据失败: %v", s, err)
|
log.Printf("获取 %s 4h历史数据失败: %v", s, err)
|
||||||
return
|
} else if len(klines4h) > 0 {
|
||||||
}
|
m.klineDataMap4h.Store(s, klines4h)
|
||||||
if len(klines4h) > 0 {
|
log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines4h))
|
||||||
m.klineDataMap4h.Store(s, klines)
|
|
||||||
log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines))
|
|
||||||
}
|
}
|
||||||
}(symbol)
|
}(symbol)
|
||||||
}
|
}
|
||||||
@@ -180,11 +197,16 @@ func (m *WSMonitor) handleKlineData(symbol string, ch <-chan []byte, _time strin
|
|||||||
|
|
||||||
func (m *WSMonitor) getKlineDataMap(_time string) *sync.Map {
|
func (m *WSMonitor) getKlineDataMap(_time string) *sync.Map {
|
||||||
var klineDataMap *sync.Map
|
var klineDataMap *sync.Map
|
||||||
if _time == "3m" {
|
switch _time {
|
||||||
|
case "3m":
|
||||||
klineDataMap = &m.klineDataMap3m
|
klineDataMap = &m.klineDataMap3m
|
||||||
} else if _time == "4h" {
|
case "15m":
|
||||||
|
klineDataMap = &m.klineDataMap15m
|
||||||
|
case "1h":
|
||||||
|
klineDataMap = &m.klineDataMap1h
|
||||||
|
case "4h":
|
||||||
klineDataMap = &m.klineDataMap4h
|
klineDataMap = &m.klineDataMap4h
|
||||||
} else {
|
default:
|
||||||
klineDataMap = &sync.Map{}
|
klineDataMap = &sync.Map{}
|
||||||
}
|
}
|
||||||
return klineDataMap
|
return klineDataMap
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ type Data struct {
|
|||||||
CurrentRSI7 float64
|
CurrentRSI7 float64
|
||||||
OpenInterest *OIData
|
OpenInterest *OIData
|
||||||
FundingRate float64
|
FundingRate float64
|
||||||
IntradaySeries *IntradayData
|
IntradaySeries *IntradayData // 3分钟数据 - 实时价格
|
||||||
LongerTermContext *LongerTermData
|
MidTermSeries15m *MidTermData15m // 15分钟数据 - 短期趋势
|
||||||
|
MidTermSeries1h *MidTermData1h // 1小时数据 - 中期趋势
|
||||||
|
LongerTermContext *LongerTermData // 4小时数据 - 长期趋势
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIData Open Interest数据
|
// OIData Open Interest数据
|
||||||
@@ -23,7 +25,7 @@ type OIData struct {
|
|||||||
Average float64
|
Average float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntradayData 日内数据(3分钟间隔)
|
// IntradayData 日内数据(3分钟间隔) - 主要用于获取实时价格
|
||||||
type IntradayData struct {
|
type IntradayData struct {
|
||||||
MidPrices []float64
|
MidPrices []float64
|
||||||
EMA20Values []float64
|
EMA20Values []float64
|
||||||
@@ -32,6 +34,24 @@ type IntradayData struct {
|
|||||||
RSI14Values []float64
|
RSI14Values []float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MidTermData15m 15分钟时间框架数据 - 短期趋势过滤
|
||||||
|
type MidTermData15m struct {
|
||||||
|
MidPrices []float64
|
||||||
|
EMA20Values []float64
|
||||||
|
MACDValues []float64
|
||||||
|
RSI7Values []float64
|
||||||
|
RSI14Values []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MidTermData1h 1小时时间框架数据 - 中期趋势确认
|
||||||
|
type MidTermData1h struct {
|
||||||
|
MidPrices []float64
|
||||||
|
EMA20Values []float64
|
||||||
|
MACDValues []float64
|
||||||
|
RSI7Values []float64
|
||||||
|
RSI14Values []float64
|
||||||
|
}
|
||||||
|
|
||||||
// LongerTermData 长期数据(4小时时间框架)
|
// LongerTermData 长期数据(4小时时间框架)
|
||||||
type LongerTermData struct {
|
type LongerTermData struct {
|
||||||
EMA20 float64
|
EMA20 float64
|
||||||
|
|||||||
Reference in New Issue
Block a user