mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
refactor: standardize code comments
This commit is contained in:
@@ -26,7 +26,7 @@ func NewAPIClient() *APIClient {
|
||||
|
||||
hookRes := hook.HookExec[hook.SetHttpClientResult](hook.SET_HTTP_CLIENT, client)
|
||||
if hookRes != nil && hookRes.Error() == nil {
|
||||
log.Printf("使用Hook设置的HTTP客户端")
|
||||
log.Printf("Using HTTP client set by Hook")
|
||||
client = hookRes.GetResult()
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (c *APIClient) GetKlines(symbol, interval string, limit int) ([]Kline, erro
|
||||
var klineResponses []KlineResponse
|
||||
err = json.Unmarshal(body, &klineResponses)
|
||||
if err != nil {
|
||||
log.Printf("获取K线数据失败,响应内容: %s", string(body))
|
||||
log.Printf("Failed to get K-line data, response content: %s", string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (c *APIClient) GetKlines(symbol, interval string, limit int) ([]Kline, erro
|
||||
for _, kr := range klineResponses {
|
||||
kline, err := parseKline(kr)
|
||||
if err != nil {
|
||||
log.Printf("解析K线数据失败: %v", err)
|
||||
log.Printf("Failed to parse K-line data: %v", err)
|
||||
continue
|
||||
}
|
||||
klines = append(klines, kline)
|
||||
@@ -107,7 +107,7 @@ func parseKline(kr KlineResponse) (Kline, error) {
|
||||
return kline, fmt.Errorf("invalid kline data")
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
// Parse each field
|
||||
kline.OpenTime = int64(kr[0].(float64))
|
||||
kline.Open, _ = strconv.ParseFloat(kr[1].(string), 64)
|
||||
kline.High, _ = strconv.ParseFloat(kr[2].(string), 64)
|
||||
|
||||
@@ -17,7 +17,7 @@ type CombinedStreamsClient struct {
|
||||
subscribers map[string]chan []byte
|
||||
reconnect bool
|
||||
done chan struct{}
|
||||
batchSize int // 每批订阅的流数量
|
||||
batchSize int // Number of streams per batch subscription
|
||||
}
|
||||
|
||||
func NewCombinedStreamsClient(batchSize int) *CombinedStreamsClient {
|
||||
@@ -34,29 +34,29 @@ func (c *CombinedStreamsClient) Connect() error {
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// 组合流使用不同的端点
|
||||
// Combined streams use a different endpoint
|
||||
conn, _, err := dialer.Dial("wss://fstream.binance.com/stream", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("组合流WebSocket连接失败: %v", err)
|
||||
return fmt.Errorf("Combined stream WebSocket connection failed: %v", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.conn = conn
|
||||
c.mu.Unlock()
|
||||
|
||||
log.Println("组合流WebSocket连接成功")
|
||||
log.Println("Combined stream WebSocket connected successfully")
|
||||
go c.readMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchSubscribeKlines 批量订阅K线
|
||||
// BatchSubscribeKlines subscribes to K-lines in batches
|
||||
func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval string) error {
|
||||
// 将symbols分批处理
|
||||
// Split symbols into batches
|
||||
batches := c.splitIntoBatches(symbols, c.batchSize)
|
||||
|
||||
for i, batch := range batches {
|
||||
log.Printf("订阅第 %d 批, 数量: %d", i+1, len(batch))
|
||||
log.Printf("Subscribing batch %d, count: %d", i+1, len(batch))
|
||||
|
||||
streams := make([]string, len(batch))
|
||||
for j, symbol := range batch {
|
||||
@@ -64,10 +64,10 @@ func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval
|
||||
}
|
||||
|
||||
if err := c.subscribeStreams(streams); err != nil {
|
||||
return fmt.Errorf("第 %d 批订阅失败: %v", i+1, err)
|
||||
return fmt.Errorf("Batch %d subscription failed: %v", i+1, err)
|
||||
}
|
||||
|
||||
// 批次间延迟,避免被限制
|
||||
// Delay between batches to avoid rate limiting
|
||||
if i < len(batches)-1 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitIntoBatches 将切片分成指定大小的批次
|
||||
// splitIntoBatches splits a slice into batches of specified size
|
||||
func (c *CombinedStreamsClient) splitIntoBatches(symbols []string, batchSize int) [][]string {
|
||||
var batches [][]string
|
||||
|
||||
@@ -91,7 +91,7 @@ func (c *CombinedStreamsClient) splitIntoBatches(symbols []string, batchSize int
|
||||
return batches
|
||||
}
|
||||
|
||||
// subscribeStreams 订阅多个流
|
||||
// subscribeStreams subscribes to multiple streams
|
||||
func (c *CombinedStreamsClient) subscribeStreams(streams []string) error {
|
||||
subscribeMsg := map[string]interface{}{
|
||||
"method": "SUBSCRIBE",
|
||||
@@ -103,10 +103,10 @@ func (c *CombinedStreamsClient) subscribeStreams(streams []string) error {
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("WebSocket未连接")
|
||||
return fmt.Errorf("WebSocket not connected")
|
||||
}
|
||||
|
||||
log.Printf("订阅流: %v", streams)
|
||||
log.Printf("Subscribing to streams: %v", streams)
|
||||
return c.conn.WriteJSON(subscribeMsg)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (c *CombinedStreamsClient) readMessages() {
|
||||
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("读取组合流消息失败: %v", err)
|
||||
log.Printf("Failed to read combined stream message: %v", err)
|
||||
c.handleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (c *CombinedStreamsClient) handleCombinedMessage(message []byte) {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &combinedMsg); err != nil {
|
||||
log.Printf("解析组合消息失败: %v", err)
|
||||
log.Printf("Failed to parse combined message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func (c *CombinedStreamsClient) handleCombinedMessage(message []byte) {
|
||||
select {
|
||||
case ch <- combinedMsg.Data:
|
||||
default:
|
||||
log.Printf("订阅者通道已满: %s", combinedMsg.Stream)
|
||||
log.Printf("Subscriber channel is full: %s", combinedMsg.Stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,11 +174,11 @@ func (c *CombinedStreamsClient) handleReconnect() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("组合流尝试重新连接...")
|
||||
log.Println("Combined stream attempting to reconnect...")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Printf("组合流重新连接失败: %v", err)
|
||||
log.Printf("Combined stream reconnection failed: %v", err)
|
||||
go c.handleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
220
market/data.go
220
market/data.go
@@ -12,8 +12,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// FundingRateCache 资金费率缓存结构
|
||||
// Binance Funding Rate 每 8 小时才更新一次,使用 1 小时缓存可显著减少 API 调用
|
||||
// FundingRateCache is the funding rate cache structure
|
||||
// Binance Funding Rate only updates every 8 hours, using 1-hour cache can significantly reduce API calls
|
||||
type FundingRateCache struct {
|
||||
Rate float64
|
||||
UpdatedAt time.Time
|
||||
@@ -24,16 +24,16 @@ var (
|
||||
frCacheTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
// Get 获取指定代币的市场数据
|
||||
// Get retrieves market data for the specified token
|
||||
func Get(symbol string) (*Data, error) {
|
||||
var klines3m, klines4h []Kline
|
||||
var err error
|
||||
// 标准化symbol
|
||||
// Normalize symbol
|
||||
symbol = Normalize(symbol)
|
||||
// 获取3分钟K线数据 (最近10个)
|
||||
klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m") // 多获取一些用于计算
|
||||
// Get 3-minute K-line data (latest 10)
|
||||
klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m") // Get more for calculation
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取3分钟K线失败: %v", err)
|
||||
return nil, fmt.Errorf("Failed to get 3-minute K-line: %v", err)
|
||||
}
|
||||
|
||||
// Data staleness detection: Prevent DOGEUSDT-style price freeze issues
|
||||
@@ -42,37 +42,37 @@ func Get(symbol string) (*Data, error) {
|
||||
return nil, fmt.Errorf("%s data is stale, possible cache failure", symbol)
|
||||
}
|
||||
|
||||
// 获取4小时K线数据 (最近10个)
|
||||
klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h") // 多获取用于计算指标
|
||||
// Get 4-hour K-line data (latest 10)
|
||||
klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h") // Get more for indicator calculation
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取4小时K线失败: %v", err)
|
||||
return nil, fmt.Errorf("Failed to get 4-hour K-line: %v", err)
|
||||
}
|
||||
|
||||
// 检查数据是否为空
|
||||
// Check if data is empty
|
||||
if len(klines3m) == 0 {
|
||||
return nil, fmt.Errorf("3分钟K线数据为空")
|
||||
return nil, fmt.Errorf("3-minute K-line data is empty")
|
||||
}
|
||||
if len(klines4h) == 0 {
|
||||
return nil, fmt.Errorf("4小时K线数据为空")
|
||||
return nil, fmt.Errorf("4-hour K-line data is empty")
|
||||
}
|
||||
|
||||
// 计算当前指标 (基于3分钟最新数据)
|
||||
// Calculate current indicators (based on 3-minute latest data)
|
||||
currentPrice := klines3m[len(klines3m)-1].Close
|
||||
currentEMA20 := calculateEMA(klines3m, 20)
|
||||
currentMACD := calculateMACD(klines3m)
|
||||
currentRSI7 := calculateRSI(klines3m, 7)
|
||||
|
||||
// 计算价格变化百分比
|
||||
// 1小时价格变化 = 20个3分钟K线前的价格
|
||||
// Calculate price change percentage
|
||||
// 1-hour price change = price from 20 3-minute K-lines ago
|
||||
priceChange1h := 0.0
|
||||
if len(klines3m) >= 21 { // 至少需要21根K线 (当前 + 20根前)
|
||||
if len(klines3m) >= 21 { // Need at least 21 K-lines (current + 20 previous)
|
||||
price1hAgo := klines3m[len(klines3m)-21].Close
|
||||
if price1hAgo > 0 {
|
||||
priceChange1h = ((currentPrice - price1hAgo) / price1hAgo) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// 4小时价格变化 = 1个4小时K线前的价格
|
||||
// 4-hour price change = price from 1 4-hour K-line ago
|
||||
priceChange4h := 0.0
|
||||
if len(klines4h) >= 2 {
|
||||
price4hAgo := klines4h[len(klines4h)-2].Close
|
||||
@@ -81,20 +81,20 @@ func Get(symbol string) (*Data, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取OI数据
|
||||
// Get OI data
|
||||
oiData, err := getOpenInterestData(symbol)
|
||||
if err != nil {
|
||||
// OI失败不影响整体,使用默认值
|
||||
// OI failure doesn't affect overall result, use default values
|
||||
oiData = &OIData{Latest: 0, Average: 0}
|
||||
}
|
||||
|
||||
// 获取Funding Rate
|
||||
// Get Funding Rate
|
||||
fundingRate, _ := getFundingRate(symbol)
|
||||
|
||||
// 计算日内系列数据
|
||||
// Calculate intraday series data
|
||||
intradayData := calculateIntradaySeries(klines3m)
|
||||
|
||||
// 计算长期数据
|
||||
// Calculate longer-term data
|
||||
longerTermData := calculateLongerTermData(klines4h)
|
||||
|
||||
return &Data{
|
||||
@@ -112,23 +112,23 @@ func Get(symbol string) (*Data, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWithTimeframes 获取指定多个时间周期的市场数据
|
||||
// timeframes: 时间周期列表,如 ["5m", "15m", "1h", "4h"]
|
||||
// primaryTimeframe: 主时间周期(用于计算当前指标),默认使用 timeframes[0]
|
||||
// count: 每个时间周期的 K 线数量
|
||||
// GetWithTimeframes retrieves market data for specified multiple timeframes
|
||||
// timeframes: list of timeframes, e.g. ["5m", "15m", "1h", "4h"]
|
||||
// primaryTimeframe: primary timeframe (used for calculating current indicators), defaults to timeframes[0]
|
||||
// count: number of K-lines for each timeframe
|
||||
func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe string, count int) (*Data, error) {
|
||||
symbol = Normalize(symbol)
|
||||
|
||||
if len(timeframes) == 0 {
|
||||
return nil, fmt.Errorf("至少需要一个时间周期")
|
||||
return nil, fmt.Errorf("at least one timeframe is required")
|
||||
}
|
||||
|
||||
// 如果未指定主周期,使用第一个
|
||||
// If primary timeframe is not specified, use the first one
|
||||
if primaryTimeframe == "" {
|
||||
primaryTimeframe = timeframes[0]
|
||||
}
|
||||
|
||||
// 确保主周期在列表中
|
||||
// Ensure primary timeframe is in the list
|
||||
hasPrimary := false
|
||||
for _, tf := range timeframes {
|
||||
if tf == primaryTimeframe {
|
||||
@@ -140,36 +140,36 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri
|
||||
timeframes = append([]string{primaryTimeframe}, timeframes...)
|
||||
}
|
||||
|
||||
// 存储所有时间周期的数据
|
||||
// Store data for all timeframes
|
||||
timeframeData := make(map[string]*TimeframeSeriesData)
|
||||
var primaryKlines []Kline
|
||||
|
||||
// 获取每个时间周期的 K 线数据
|
||||
// Get K-line data for each timeframe
|
||||
for _, tf := range timeframes {
|
||||
klines, err := WSMonitorCli.GetCurrentKlines(symbol, tf)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 %s %s K线失败: %v", symbol, tf, err)
|
||||
logger.Infof("⚠️ Failed to get %s %s K-line: %v", symbol, tf, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(klines) == 0 {
|
||||
logger.Infof("⚠️ %s %s K线数据为空", symbol, tf)
|
||||
logger.Infof("⚠️ %s %s K-line data is empty", symbol, tf)
|
||||
continue
|
||||
}
|
||||
|
||||
// 保存主周期的 K 线用于计算基础指标
|
||||
// Save primary timeframe K-lines for calculating base indicators
|
||||
if tf == primaryTimeframe {
|
||||
primaryKlines = klines
|
||||
}
|
||||
|
||||
// 计算该时间周期的系列数据
|
||||
// Calculate series data for this timeframe
|
||||
seriesData := calculateTimeframeSeries(klines, tf)
|
||||
timeframeData[tf] = seriesData
|
||||
}
|
||||
|
||||
// 如果主周期数据为空,返回错误
|
||||
// If primary timeframe data is empty, return error
|
||||
if len(primaryKlines) == 0 {
|
||||
return nil, fmt.Errorf("主时间周期 %s K线数据为空", primaryTimeframe)
|
||||
return nil, fmt.Errorf("Primary timeframe %s K-line data is empty", primaryTimeframe)
|
||||
}
|
||||
|
||||
// Data staleness detection
|
||||
@@ -178,23 +178,23 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri
|
||||
return nil, fmt.Errorf("%s data is stale, possible cache failure", symbol)
|
||||
}
|
||||
|
||||
// 计算当前指标 (基于主周期最新数据)
|
||||
// Calculate current indicators (based on primary timeframe latest data)
|
||||
currentPrice := primaryKlines[len(primaryKlines)-1].Close
|
||||
currentEMA20 := calculateEMA(primaryKlines, 20)
|
||||
currentMACD := calculateMACD(primaryKlines)
|
||||
currentRSI7 := calculateRSI(primaryKlines, 7)
|
||||
|
||||
// 计算价格变化
|
||||
priceChange1h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 60) // 1小时
|
||||
priceChange4h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 240) // 4小时
|
||||
// Calculate price changes
|
||||
priceChange1h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 60) // 1 hour
|
||||
priceChange4h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 240) // 4 hours
|
||||
|
||||
// 获取OI数据
|
||||
// Get OI data
|
||||
oiData, err := getOpenInterestData(symbol)
|
||||
if err != nil {
|
||||
oiData = &OIData{Latest: 0, Average: 0}
|
||||
}
|
||||
|
||||
// 获取Funding Rate
|
||||
// Get Funding Rate
|
||||
fundingRate, _ := getFundingRate(symbol)
|
||||
|
||||
return &Data{
|
||||
@@ -211,7 +211,7 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateTimeframeSeries 计算单个时间周期的系列数据
|
||||
// calculateTimeframeSeries calculates series data for a single timeframe
|
||||
func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeriesData {
|
||||
data := &TimeframeSeriesData{
|
||||
Timeframe: timeframe,
|
||||
@@ -224,7 +224,7 @@ func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeries
|
||||
Volume: make([]float64, 0, 10),
|
||||
}
|
||||
|
||||
// 获取最近10个数据点
|
||||
// Get latest 10 data points
|
||||
start := len(klines) - 10
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -234,25 +234,25 @@ func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeries
|
||||
data.MidPrices = append(data.MidPrices, klines[i].Close)
|
||||
data.Volume = append(data.Volume, klines[i].Volume)
|
||||
|
||||
// 计算每个点的 EMA20
|
||||
// Calculate EMA20 for each point
|
||||
if i >= 19 {
|
||||
ema20 := calculateEMA(klines[:i+1], 20)
|
||||
data.EMA20Values = append(data.EMA20Values, ema20)
|
||||
}
|
||||
|
||||
// 计算每个点的 EMA50
|
||||
// Calculate EMA50 for each point
|
||||
if i >= 49 {
|
||||
ema50 := calculateEMA(klines[:i+1], 50)
|
||||
data.EMA50Values = append(data.EMA50Values, ema50)
|
||||
}
|
||||
|
||||
// 计算每个点的 MACD
|
||||
// Calculate MACD for each point
|
||||
if i >= 25 {
|
||||
macd := calculateMACD(klines[:i+1])
|
||||
data.MACDValues = append(data.MACDValues, macd)
|
||||
}
|
||||
|
||||
// 计算每个点的 RSI
|
||||
// Calculate RSI for each point
|
||||
if i >= 7 {
|
||||
rsi7 := calculateRSI(klines[:i+1], 7)
|
||||
data.RSI7Values = append(data.RSI7Values, rsi7)
|
||||
@@ -263,25 +263,25 @@ func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeries
|
||||
}
|
||||
}
|
||||
|
||||
// 计算 ATR14
|
||||
// Calculate ATR14
|
||||
data.ATR14 = calculateATR(klines, 14)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// calculatePriceChangeByBars 根据时间周期计算需要回溯多少根 K 线来计算价格变化
|
||||
// calculatePriceChangeByBars calculates how many K-lines to look back for price change based on timeframe
|
||||
func calculatePriceChangeByBars(klines []Kline, timeframe string, targetMinutes int) float64 {
|
||||
if len(klines) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 解析时间周期为分钟数
|
||||
// Parse timeframe to minutes
|
||||
tfMinutes := parseTimeframeToMinutes(timeframe)
|
||||
if tfMinutes <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算需要回溯多少根 K 线
|
||||
// Calculate how many K-lines to look back
|
||||
barsBack := targetMinutes / tfMinutes
|
||||
if barsBack < 1 {
|
||||
barsBack = 1
|
||||
@@ -300,7 +300,7 @@ func calculatePriceChangeByBars(klines []Kline, timeframe string, targetMinutes
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseTimeframeToMinutes 将时间周期字符串解析为分钟数
|
||||
// parseTimeframeToMinutes parses timeframe string to minutes
|
||||
func parseTimeframeToMinutes(tf string) int {
|
||||
switch tf {
|
||||
case "1m":
|
||||
@@ -336,20 +336,20 @@ func parseTimeframeToMinutes(tf string) int {
|
||||
}
|
||||
}
|
||||
|
||||
// calculateEMA 计算EMA
|
||||
// calculateEMA calculates EMA
|
||||
func calculateEMA(klines []Kline, period int) float64 {
|
||||
if len(klines) < period {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算SMA作为初始EMA
|
||||
// Calculate SMA as initial EMA
|
||||
sum := 0.0
|
||||
for i := 0; i < period; i++ {
|
||||
sum += klines[i].Close
|
||||
}
|
||||
ema := sum / float64(period)
|
||||
|
||||
// 计算EMA
|
||||
// Calculate EMA
|
||||
multiplier := 2.0 / float64(period+1)
|
||||
for i := period; i < len(klines); i++ {
|
||||
ema = (klines[i].Close-ema)*multiplier + ema
|
||||
@@ -358,13 +358,13 @@ func calculateEMA(klines []Kline, period int) float64 {
|
||||
return ema
|
||||
}
|
||||
|
||||
// calculateMACD 计算MACD
|
||||
// calculateMACD calculates MACD
|
||||
func calculateMACD(klines []Kline) float64 {
|
||||
if len(klines) < 26 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算12期和26期EMA
|
||||
// Calculate 12-period and 26-period EMA
|
||||
ema12 := calculateEMA(klines, 12)
|
||||
ema26 := calculateEMA(klines, 26)
|
||||
|
||||
@@ -372,7 +372,7 @@ func calculateMACD(klines []Kline) float64 {
|
||||
return ema12 - ema26
|
||||
}
|
||||
|
||||
// calculateRSI 计算RSI
|
||||
// calculateRSI calculates RSI
|
||||
func calculateRSI(klines []Kline, period int) float64 {
|
||||
if len(klines) <= period {
|
||||
return 0
|
||||
@@ -381,7 +381,7 @@ func calculateRSI(klines []Kline, period int) float64 {
|
||||
gains := 0.0
|
||||
losses := 0.0
|
||||
|
||||
// 计算初始平均涨跌幅
|
||||
// Calculate initial average gain/loss
|
||||
for i := 1; i <= period; i++ {
|
||||
change := klines[i].Close - klines[i-1].Close
|
||||
if change > 0 {
|
||||
@@ -394,7 +394,7 @@ func calculateRSI(klines []Kline, period int) float64 {
|
||||
avgGain := gains / float64(period)
|
||||
avgLoss := losses / float64(period)
|
||||
|
||||
// 使用Wilder平滑方法计算后续RSI
|
||||
// Use Wilder smoothing method to calculate subsequent RSI
|
||||
for i := period + 1; i < len(klines); i++ {
|
||||
change := klines[i].Close - klines[i-1].Close
|
||||
if change > 0 {
|
||||
@@ -416,7 +416,7 @@ func calculateRSI(klines []Kline, period int) float64 {
|
||||
return rsi
|
||||
}
|
||||
|
||||
// calculateATR 计算ATR
|
||||
// calculateATR calculates ATR
|
||||
func calculateATR(klines []Kline, period int) float64 {
|
||||
if len(klines) <= period {
|
||||
return 0
|
||||
@@ -435,14 +435,14 @@ func calculateATR(klines []Kline, period int) float64 {
|
||||
trs[i] = math.Max(tr1, math.Max(tr2, tr3))
|
||||
}
|
||||
|
||||
// 计算初始ATR
|
||||
// Calculate initial ATR
|
||||
sum := 0.0
|
||||
for i := 1; i <= period; i++ {
|
||||
sum += trs[i]
|
||||
}
|
||||
atr := sum / float64(period)
|
||||
|
||||
// Wilder平滑
|
||||
// Wilder smoothing
|
||||
for i := period + 1; i < len(klines); i++ {
|
||||
atr = (atr*float64(period-1) + trs[i]) / float64(period)
|
||||
}
|
||||
@@ -450,7 +450,7 @@ func calculateATR(klines []Kline, period int) float64 {
|
||||
return atr
|
||||
}
|
||||
|
||||
// calculateIntradaySeries 计算日内系列数据
|
||||
// calculateIntradaySeries calculates intraday series data
|
||||
func calculateIntradaySeries(klines []Kline) *IntradayData {
|
||||
data := &IntradayData{
|
||||
MidPrices: make([]float64, 0, 10),
|
||||
@@ -461,7 +461,7 @@ func calculateIntradaySeries(klines []Kline) *IntradayData {
|
||||
Volume: make([]float64, 0, 10),
|
||||
}
|
||||
|
||||
// 获取最近10个数据点
|
||||
// Get latest 10 data points
|
||||
start := len(klines) - 10
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -471,19 +471,19 @@ func calculateIntradaySeries(klines []Kline) *IntradayData {
|
||||
data.MidPrices = append(data.MidPrices, klines[i].Close)
|
||||
data.Volume = append(data.Volume, klines[i].Volume)
|
||||
|
||||
// 计算每个点的EMA20
|
||||
// Calculate EMA20 for each point
|
||||
if i >= 19 {
|
||||
ema20 := calculateEMA(klines[:i+1], 20)
|
||||
data.EMA20Values = append(data.EMA20Values, ema20)
|
||||
}
|
||||
|
||||
// 计算每个点的MACD
|
||||
// Calculate MACD for each point
|
||||
if i >= 25 {
|
||||
macd := calculateMACD(klines[:i+1])
|
||||
data.MACDValues = append(data.MACDValues, macd)
|
||||
}
|
||||
|
||||
// 计算每个点的RSI
|
||||
// Calculate RSI for each point
|
||||
if i >= 7 {
|
||||
rsi7 := calculateRSI(klines[:i+1], 7)
|
||||
data.RSI7Values = append(data.RSI7Values, rsi7)
|
||||
@@ -494,31 +494,31 @@ func calculateIntradaySeries(klines []Kline) *IntradayData {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算3m ATR14
|
||||
// Calculate 3m ATR14
|
||||
data.ATR14 = calculateATR(klines, 14)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// calculateLongerTermData 计算长期数据
|
||||
// calculateLongerTermData calculates longer-term data
|
||||
func calculateLongerTermData(klines []Kline) *LongerTermData {
|
||||
data := &LongerTermData{
|
||||
MACDValues: make([]float64, 0, 10),
|
||||
RSI14Values: make([]float64, 0, 10),
|
||||
}
|
||||
|
||||
// 计算EMA
|
||||
// Calculate EMA
|
||||
data.EMA20 = calculateEMA(klines, 20)
|
||||
data.EMA50 = calculateEMA(klines, 50)
|
||||
|
||||
// 计算ATR
|
||||
// Calculate ATR
|
||||
data.ATR3 = calculateATR(klines, 3)
|
||||
data.ATR14 = calculateATR(klines, 14)
|
||||
|
||||
// 计算成交量
|
||||
// Calculate volume
|
||||
if len(klines) > 0 {
|
||||
data.CurrentVolume = klines[len(klines)-1].Volume
|
||||
// 计算平均成交量
|
||||
// Calculate average volume
|
||||
sum := 0.0
|
||||
for _, k := range klines {
|
||||
sum += k.Volume
|
||||
@@ -526,7 +526,7 @@ func calculateLongerTermData(klines []Kline) *LongerTermData {
|
||||
data.AverageVolume = sum / float64(len(klines))
|
||||
}
|
||||
|
||||
// 计算MACD和RSI序列
|
||||
// Calculate MACD and RSI series
|
||||
start := len(klines) - 10
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -546,7 +546,7 @@ func calculateLongerTermData(klines []Kline) *LongerTermData {
|
||||
return data
|
||||
}
|
||||
|
||||
// getOpenInterestData 获取OI数据
|
||||
// getOpenInterestData retrieves OI data
|
||||
func getOpenInterestData(symbol string) (*OIData, error) {
|
||||
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/openInterest?symbol=%s", symbol)
|
||||
|
||||
@@ -576,23 +576,23 @@ func getOpenInterestData(symbol string) (*OIData, error) {
|
||||
|
||||
return &OIData{
|
||||
Latest: oi,
|
||||
Average: oi * 0.999, // 近似平均值
|
||||
Average: oi * 0.999, // Approximate average
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getFundingRate 获取资金费率(优化:使用 1 小时缓存)
|
||||
// getFundingRate retrieves funding rate (optimized: uses 1-hour cache)
|
||||
func getFundingRate(symbol string) (float64, error) {
|
||||
// 检查缓存(有效期 1 小时)
|
||||
// Funding Rate 每 8 小时才更新,1 小时缓存非常合理
|
||||
// Check cache (1-hour validity)
|
||||
// Funding Rate only updates every 8 hours, 1-hour cache is very reasonable
|
||||
if cached, ok := fundingRateMap.Load(symbol); ok {
|
||||
cache := cached.(*FundingRateCache)
|
||||
if time.Since(cache.UpdatedAt) < frCacheTTL {
|
||||
// 缓存命中,直接返回
|
||||
// Cache hit, return directly
|
||||
return cache.Rate, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存过期或不存在,调用 API
|
||||
// Cache expired or doesn't exist, call API
|
||||
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=%s", symbol)
|
||||
|
||||
apiClient := NewAPIClient()
|
||||
@@ -623,7 +623,7 @@ func getFundingRate(symbol string) (float64, error) {
|
||||
|
||||
rate, _ := strconv.ParseFloat(result.LastFundingRate, 64)
|
||||
|
||||
// 更新缓存
|
||||
// Update cache
|
||||
fundingRateMap.Store(symbol, &FundingRateCache{
|
||||
Rate: rate,
|
||||
UpdatedAt: time.Now(),
|
||||
@@ -632,11 +632,11 @@ func getFundingRate(symbol string) (float64, error) {
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// Format 格式化输出市场数据
|
||||
// Format formats and outputs market data
|
||||
func Format(data *Data) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 使用动态精度格式化价格
|
||||
// Format price with dynamic precision
|
||||
priceStr := formatPriceWithDynamicPrecision(data.CurrentPrice)
|
||||
sb.WriteString(fmt.Sprintf("current_price = %s, current_ema20 = %.3f, current_macd = %.3f, current_rsi (7 period) = %.3f\n\n",
|
||||
priceStr, data.CurrentEMA20, data.CurrentMACD, data.CurrentRSI7))
|
||||
@@ -645,7 +645,7 @@ func Format(data *Data) string {
|
||||
data.Symbol))
|
||||
|
||||
if data.OpenInterest != nil {
|
||||
// 使用动态精度格式化 OI 数据
|
||||
// Format OI data with dynamic precision
|
||||
oiLatestStr := formatPriceWithDynamicPrecision(data.OpenInterest.Latest)
|
||||
oiAverageStr := formatPriceWithDynamicPrecision(data.OpenInterest.Average)
|
||||
sb.WriteString(fmt.Sprintf("Open Interest: Latest: %s Average: %s\n\n",
|
||||
@@ -705,9 +705,9 @@ func Format(data *Data) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 多时间周期数据(新增)
|
||||
// Multi-timeframe data (new)
|
||||
if len(data.TimeframeData) > 0 {
|
||||
// 按时间周期排序输出
|
||||
// Output sorted by timeframe
|
||||
timeframeOrder := []string{"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w"}
|
||||
for _, tf := range timeframeOrder {
|
||||
if tfData, ok := data.TimeframeData[tf]; ok {
|
||||
@@ -720,7 +720,7 @@ func Format(data *Data) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatTimeframeData 格式化单个时间周期的数据
|
||||
// formatTimeframeData formats data for a single timeframe
|
||||
func formatTimeframeData(sb *strings.Builder, data *TimeframeSeriesData) {
|
||||
if len(data.MidPrices) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidPrices)))
|
||||
@@ -753,38 +753,38 @@ func formatTimeframeData(sb *strings.Builder, data *TimeframeSeriesData) {
|
||||
sb.WriteString(fmt.Sprintf("ATR (14‑period): %.3f\n\n", data.ATR14))
|
||||
}
|
||||
|
||||
// formatPriceWithDynamicPrecision 根据价格区间动态选择精度
|
||||
// 这样可以完美支持从超低价 meme coin (< 0.0001) 到 BTC/ETH 的所有币种
|
||||
// formatPriceWithDynamicPrecision dynamically selects precision based on price range
|
||||
// This perfectly supports all coins from ultra-low price meme coins (< 0.0001) to BTC/ETH
|
||||
func formatPriceWithDynamicPrecision(price float64) string {
|
||||
switch {
|
||||
case price < 0.0001:
|
||||
// 超低价 meme coin: 1000SATS, 1000WHY, DOGS
|
||||
// 0.00002070 → "0.00002070" (8位小数)
|
||||
// Ultra-low price meme coins: 1000SATS, 1000WHY, DOGS
|
||||
// 0.00002070 → "0.00002070" (8 decimal places)
|
||||
return fmt.Sprintf("%.8f", price)
|
||||
case price < 0.001:
|
||||
// 低价 meme coin: NEIRO, HMSTR, HOT, NOT
|
||||
// 0.00015060 → "0.000151" (6位小数)
|
||||
// Low price meme coins: NEIRO, HMSTR, HOT, NOT
|
||||
// 0.00015060 → "0.000151" (6 decimal places)
|
||||
return fmt.Sprintf("%.6f", price)
|
||||
case price < 0.01:
|
||||
// 中低价币: PEPE, SHIB, MEME
|
||||
// 0.00556800 → "0.005568" (6位小数)
|
||||
// Mid-low price coins: PEPE, SHIB, MEME
|
||||
// 0.00556800 → "0.005568" (6 decimal places)
|
||||
return fmt.Sprintf("%.6f", price)
|
||||
case price < 1.0:
|
||||
// 低价币: ASTER, DOGE, ADA, TRX
|
||||
// 0.9954 → "0.9954" (4位小数)
|
||||
// Low price coins: ASTER, DOGE, ADA, TRX
|
||||
// 0.9954 → "0.9954" (4 decimal places)
|
||||
return fmt.Sprintf("%.4f", price)
|
||||
case price < 100:
|
||||
// 中价币: SOL, AVAX, LINK, MATIC
|
||||
// 23.4567 → "23.4567" (4位小数)
|
||||
// Mid price coins: SOL, AVAX, LINK, MATIC
|
||||
// 23.4567 → "23.4567" (4 decimal places)
|
||||
return fmt.Sprintf("%.4f", price)
|
||||
default:
|
||||
// 高价币: BTC, ETH (节省 Token)
|
||||
// 45678.9123 → "45678.91" (2位小数)
|
||||
// High price coins: BTC, ETH (save tokens)
|
||||
// 45678.9123 → "45678.91" (2 decimal places)
|
||||
return fmt.Sprintf("%.2f", price)
|
||||
}
|
||||
}
|
||||
|
||||
// formatFloatSlice 格式化float64切片为字符串(使用动态精度)
|
||||
// formatFloatSlice formats float64 slice to string (using dynamic precision)
|
||||
func formatFloatSlice(values []float64) string {
|
||||
strValues := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
@@ -793,7 +793,7 @@ func formatFloatSlice(values []float64) string {
|
||||
return "[" + strings.Join(strValues, ", ") + "]"
|
||||
}
|
||||
|
||||
// Normalize 标准化symbol,确保是USDT交易对
|
||||
// Normalize normalizes symbol, ensures it's a USDT trading pair
|
||||
func Normalize(symbol string) string {
|
||||
symbol = strings.ToUpper(symbol)
|
||||
if strings.HasSuffix(symbol, "USDT") {
|
||||
@@ -802,7 +802,7 @@ func Normalize(symbol string) string {
|
||||
return symbol + "USDT"
|
||||
}
|
||||
|
||||
// parseFloat 解析float值
|
||||
// parseFloat parses float value
|
||||
func parseFloat(v interface{}) (float64, error) {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
@@ -818,7 +818,7 @@ func parseFloat(v interface{}) (float64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// BuildDataFromKlines 根据预加载的K线序列构造市场数据快照(用于回测/模拟)。
|
||||
// BuildDataFromKlines constructs market data snapshot from preloaded K-line series (for backtesting/simulation).
|
||||
func BuildDataFromKlines(symbol string, primary []Kline, longer []Kline) (*Data, error) {
|
||||
if len(primary) == 0 {
|
||||
return nil, fmt.Errorf("primary series is empty")
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// generateTestKlines 生成测试用的 K线数据
|
||||
// generateTestKlines generates test K-line data
|
||||
func generateTestKlines(count int) []Kline {
|
||||
klines := make([]Kline, count)
|
||||
for i := 0; i < count; i++ {
|
||||
// 生成模拟的价格数据,有一定的波动
|
||||
// Generate simulated price data with some fluctuation
|
||||
basePrice := 100.0
|
||||
variance := float64(i%10) * 0.5
|
||||
open := basePrice + variance
|
||||
@@ -19,7 +19,7 @@ func generateTestKlines(count int) []Kline {
|
||||
volume := 1000.0 + float64(i*100)
|
||||
|
||||
klines[i] = Kline{
|
||||
OpenTime: int64(i * 180000), // 3分钟间隔
|
||||
OpenTime: int64(i * 180000), // 3-minute interval
|
||||
Open: open,
|
||||
High: high,
|
||||
Low: low,
|
||||
@@ -31,7 +31,7 @@ func generateTestKlines(count int) []Kline {
|
||||
return klines
|
||||
}
|
||||
|
||||
// TestCalculateIntradaySeries_VolumeCollection 测试 Volume 数据收集
|
||||
// TestCalculateIntradaySeries_VolumeCollection tests Volume data collection
|
||||
func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -39,24 +39,24 @@ func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) {
|
||||
expectedVolLen int
|
||||
}{
|
||||
{
|
||||
name: "正常情况 - 20个K线",
|
||||
name: "Normal case - 20 K-lines",
|
||||
klineCount: 20,
|
||||
expectedVolLen: 10, // 应该收集最近10个
|
||||
expectedVolLen: 10, // Should collect latest 10
|
||||
},
|
||||
{
|
||||
name: "刚好10个K线",
|
||||
name: "Exactly 10 K-lines",
|
||||
klineCount: 10,
|
||||
expectedVolLen: 10,
|
||||
},
|
||||
{
|
||||
name: "少于10个K线",
|
||||
name: "Less than 10 K-lines",
|
||||
klineCount: 5,
|
||||
expectedVolLen: 5, // 应该返回所有5个
|
||||
expectedVolLen: 5, // Should return all 5
|
||||
},
|
||||
{
|
||||
name: "超过10个K线",
|
||||
name: "More than 10 K-lines",
|
||||
klineCount: 30,
|
||||
expectedVolLen: 10, // 应该只返回最近10个
|
||||
expectedVolLen: 10, // Should only return latest 10
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,21 +73,21 @@ func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) {
|
||||
t.Errorf("Volume length = %d, want %d", len(data.Volume), tt.expectedVolLen)
|
||||
}
|
||||
|
||||
// 验证 Volume 数据正确性
|
||||
// Verify Volume data correctness
|
||||
if len(data.Volume) > 0 {
|
||||
// 计算期望的起始索引
|
||||
// Calculate expected start index
|
||||
start := tt.klineCount - 10
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
// 验证第一个 Volume 值
|
||||
// Verify first Volume value
|
||||
expectedFirstVolume := klines[start].Volume
|
||||
if data.Volume[0] != expectedFirstVolume {
|
||||
t.Errorf("First volume = %.2f, want %.2f", data.Volume[0], expectedFirstVolume)
|
||||
}
|
||||
|
||||
// 验证最后一个 Volume 值
|
||||
// Verify last Volume value
|
||||
expectedLastVolume := klines[tt.klineCount-1].Volume
|
||||
lastVolume := data.Volume[len(data.Volume)-1]
|
||||
if lastVolume != expectedLastVolume {
|
||||
@@ -98,7 +98,7 @@ func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateIntradaySeries_VolumeValues 测试 Volume 值的正确性
|
||||
// TestCalculateIntradaySeries_VolumeValues tests Volume value correctness
|
||||
func TestCalculateIntradaySeries_VolumeValues(t *testing.T) {
|
||||
klines := []Kline{
|
||||
{Close: 100.0, Volume: 1000.0, High: 101.0, Low: 99.0, Open: 100.0},
|
||||
@@ -128,7 +128,7 @@ func TestCalculateIntradaySeries_VolumeValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateIntradaySeries_ATR14 测试 ATR14 计算
|
||||
// TestCalculateIntradaySeries_ATR14 tests ATR14 calculation
|
||||
func TestCalculateIntradaySeries_ATR14(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -137,27 +137,27 @@ func TestCalculateIntradaySeries_ATR14(t *testing.T) {
|
||||
expectNonZero bool
|
||||
}{
|
||||
{
|
||||
name: "足够数据 - 20个K线",
|
||||
name: "Sufficient data - 20 K-lines",
|
||||
klineCount: 20,
|
||||
expectNonZero: true,
|
||||
},
|
||||
{
|
||||
name: "刚好15个K线(ATR14需要至少15个)",
|
||||
name: "Exactly 15 K-lines (ATR14 requires at least 15)",
|
||||
klineCount: 15,
|
||||
expectNonZero: true,
|
||||
},
|
||||
{
|
||||
name: "数据不足 - 14个K线",
|
||||
name: "Insufficient data - 14 K-lines",
|
||||
klineCount: 14,
|
||||
expectZero: true,
|
||||
},
|
||||
{
|
||||
name: "数据不足 - 10个K线",
|
||||
name: "Insufficient data - 10 K-lines",
|
||||
klineCount: 10,
|
||||
expectZero: true,
|
||||
},
|
||||
{
|
||||
name: "数据不足 - 5个K线",
|
||||
name: "Insufficient data - 5 K-lines",
|
||||
klineCount: 5,
|
||||
expectZero: true,
|
||||
},
|
||||
@@ -183,7 +183,7 @@ func TestCalculateIntradaySeries_ATR14(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateATR 测试 ATR 计算函数
|
||||
// TestCalculateATR tests ATR calculation function
|
||||
func TestCalculateATR(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -192,7 +192,7 @@ func TestCalculateATR(t *testing.T) {
|
||||
expectZero bool
|
||||
}{
|
||||
{
|
||||
name: "正常计算 - 足够数据",
|
||||
name: "Normal calculation - sufficient data",
|
||||
klines: []Kline{
|
||||
{High: 102.0, Low: 100.0, Close: 101.0},
|
||||
{High: 103.0, Low: 101.0, Close: 102.0},
|
||||
@@ -214,7 +214,7 @@ func TestCalculateATR(t *testing.T) {
|
||||
expectZero: false,
|
||||
},
|
||||
{
|
||||
name: "数据不足 - 等于period",
|
||||
name: "Insufficient data - equal to period",
|
||||
klines: []Kline{
|
||||
{High: 102.0, Low: 100.0, Close: 101.0},
|
||||
{High: 103.0, Low: 101.0, Close: 102.0},
|
||||
@@ -223,7 +223,7 @@ func TestCalculateATR(t *testing.T) {
|
||||
expectZero: true,
|
||||
},
|
||||
{
|
||||
name: "数据不足 - 少于period",
|
||||
name: "Insufficient data - less than period",
|
||||
klines: []Kline{
|
||||
{High: 102.0, Low: 100.0, Close: 101.0},
|
||||
},
|
||||
@@ -249,9 +249,9 @@ func TestCalculateATR(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateATR_TrueRange 测试 ATR 的 True Range 计算正确性
|
||||
// TestCalculateATR_TrueRange tests ATR True Range calculation correctness
|
||||
func TestCalculateATR_TrueRange(t *testing.T) {
|
||||
// 创建一个简单的测试用例,手动计算期望的 ATR
|
||||
// Create a simple test case, manually calculate expected ATR
|
||||
klines := []Kline{
|
||||
{High: 50.0, Low: 48.0, Close: 49.0}, // TR = 2.0
|
||||
{High: 51.0, Low: 49.0, Close: 50.0}, // TR = max(2.0, 2.0, 1.0) = 2.0
|
||||
@@ -262,28 +262,28 @@ func TestCalculateATR_TrueRange(t *testing.T) {
|
||||
|
||||
atr := calculateATR(klines, 3)
|
||||
|
||||
// 期望的计算:
|
||||
// Expected calculation:
|
||||
// TR[1] = max(51-49, |51-49|, |49-49|) = 2.0
|
||||
// TR[2] = max(52-50, |52-50|, |50-50|) = 2.0
|
||||
// TR[3] = max(53-51, |53-51|, |51-51|) = 2.0
|
||||
// 初始 ATR = (2.0 + 2.0 + 2.0) / 3 = 2.0
|
||||
// Initial ATR = (2.0 + 2.0 + 2.0) / 3 = 2.0
|
||||
// TR[4] = max(54-52, |54-52|, |52-52|) = 2.0
|
||||
// 平滑 ATR = (2.0*2 + 2.0) / 3 = 2.0
|
||||
// Smoothed ATR = (2.0*2 + 2.0) / 3 = 2.0
|
||||
|
||||
expectedATR := 2.0
|
||||
tolerance := 0.01 // 允许小的浮点误差
|
||||
tolerance := 0.01 // Allow small floating point error
|
||||
|
||||
if math.Abs(atr-expectedATR) > tolerance {
|
||||
t.Errorf("calculateATR() = %.3f, want approximately %.3f", atr, expectedATR)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateIntradaySeries_ConsistencyWithOtherIndicators 测试 Volume 和其他指标的一致性
|
||||
// TestCalculateIntradaySeries_ConsistencyWithOtherIndicators tests Volume and other indicators consistency
|
||||
func TestCalculateIntradaySeries_ConsistencyWithOtherIndicators(t *testing.T) {
|
||||
klines := generateTestKlines(30)
|
||||
data := calculateIntradaySeries(klines)
|
||||
|
||||
// 所有数组应该存在
|
||||
// All arrays should exist
|
||||
if data.MidPrices == nil {
|
||||
t.Error("MidPrices should not be nil")
|
||||
}
|
||||
@@ -291,13 +291,13 @@ func TestCalculateIntradaySeries_ConsistencyWithOtherIndicators(t *testing.T) {
|
||||
t.Error("Volume should not be nil")
|
||||
}
|
||||
|
||||
// MidPrices 和 Volume 应该有相同的长度(都是最近10个)
|
||||
// MidPrices and Volume should have the same length (both latest 10)
|
||||
if len(data.MidPrices) != len(data.Volume) {
|
||||
t.Errorf("MidPrices length (%d) should equal Volume length (%d)",
|
||||
len(data.MidPrices), len(data.Volume))
|
||||
}
|
||||
|
||||
// 所有 Volume 值应该大于 0
|
||||
// All Volume values should be > 0
|
||||
for i, vol := range data.Volume {
|
||||
if vol <= 0 {
|
||||
t.Errorf("Volume[%d] = %.2f, should be > 0", i, vol)
|
||||
@@ -305,7 +305,7 @@ func TestCalculateIntradaySeries_ConsistencyWithOtherIndicators(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateIntradaySeries_EmptyKlines 测试空 K线数据
|
||||
// TestCalculateIntradaySeries_EmptyKlines tests empty K-line data
|
||||
func TestCalculateIntradaySeries_EmptyKlines(t *testing.T) {
|
||||
klines := []Kline{}
|
||||
data := calculateIntradaySeries(klines)
|
||||
@@ -314,7 +314,7 @@ func TestCalculateIntradaySeries_EmptyKlines(t *testing.T) {
|
||||
t.Fatal("calculateIntradaySeries should not return nil for empty klines")
|
||||
}
|
||||
|
||||
// 所有切片应该为空
|
||||
// All slices should be empty
|
||||
if len(data.MidPrices) != 0 {
|
||||
t.Errorf("MidPrices length = %d, want 0", len(data.MidPrices))
|
||||
}
|
||||
@@ -322,13 +322,13 @@ func TestCalculateIntradaySeries_EmptyKlines(t *testing.T) {
|
||||
t.Errorf("Volume length = %d, want 0", len(data.Volume))
|
||||
}
|
||||
|
||||
// ATR14 应该为 0(数据不足)
|
||||
// ATR14 should be 0 (insufficient data)
|
||||
if data.ATR14 != 0 {
|
||||
t.Errorf("ATR14 = %.3f, want 0", data.ATR14)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateIntradaySeries_VolumePrecision 测试 Volume 精度保持
|
||||
// TestCalculateIntradaySeries_VolumePrecision tests Volume precision preservation
|
||||
func TestCalculateIntradaySeries_VolumePrecision(t *testing.T) {
|
||||
klines := []Kline{
|
||||
{Close: 100.0, Volume: 1234.5678, High: 101.0, Low: 99.0},
|
||||
|
||||
@@ -13,7 +13,7 @@ const (
|
||||
binanceMaxKlineLimit = 1500
|
||||
)
|
||||
|
||||
// GetKlinesRange 拉取指定时间范围内的 K 线序列(闭区间),返回按时间升序排列的数据。
|
||||
// GetKlinesRange fetches K-line series within specified time range (closed interval), returns data sorted by time in ascending order.
|
||||
func GetKlinesRange(symbol string, timeframe string, start, end time.Time) ([]Kline, error) {
|
||||
symbol = Normalize(symbol)
|
||||
normTF, err := NormalizeTimeframe(timeframe)
|
||||
@@ -94,7 +94,7 @@ func GetKlinesRange(symbol string, timeframe string, start, end time.Time) ([]Kl
|
||||
last := batch[len(batch)-1]
|
||||
cursor = last.CloseTime + 1
|
||||
|
||||
// 若返回数量少于请求上限,说明已到达末尾,可提前退出。
|
||||
// If returned quantity is less than request limit, reached the end, can exit early.
|
||||
if len(batch) < binanceMaxKlineLimit {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -15,24 +15,24 @@ type WSMonitor struct {
|
||||
symbols []string
|
||||
featuresMap sync.Map
|
||||
alertsChan chan Alert
|
||||
klineDataMap3m sync.Map // 存储每个交易对的K线历史数据
|
||||
klineDataMap4h sync.Map // 存储每个交易对的K线历史数据
|
||||
tickerDataMap sync.Map // 存储每个交易对的ticker数据
|
||||
klineDataMap3m sync.Map // Store K-line historical data for each trading pair
|
||||
klineDataMap4h sync.Map // Store K-line historical data for each trading pair
|
||||
tickerDataMap sync.Map // Store ticker data for each trading pair
|
||||
batchSize int
|
||||
filterSymbols sync.Map // 使用sync.Map来存储需要监控的币种和其状态
|
||||
symbolStats sync.Map // 存储币种统计信息
|
||||
FilterSymbol []string //经过筛选的币种
|
||||
filterSymbols sync.Map // Use sync.Map to store monitored coins and their status
|
||||
symbolStats sync.Map // Store symbol statistics
|
||||
FilterSymbol []string // Filtered symbols
|
||||
}
|
||||
type SymbolStats struct {
|
||||
LastActiveTime time.Time
|
||||
AlertCount int
|
||||
VolumeSpikeCount int
|
||||
LastAlertTime time.Time
|
||||
Score float64 // 综合评分
|
||||
Score float64 // Composite score
|
||||
}
|
||||
|
||||
var WSMonitorCli *WSMonitor
|
||||
var subKlineTime = []string{"3m", "4h"} // 管理订阅流的K线周期
|
||||
var subKlineTime = []string{"3m", "4h"} // Manage K-line periods for subscription streams
|
||||
|
||||
func NewWSMonitor(batchSize int) *WSMonitor {
|
||||
WSMonitorCli = &WSMonitor{
|
||||
@@ -45,16 +45,16 @@ func NewWSMonitor(batchSize int) *WSMonitor {
|
||||
}
|
||||
|
||||
func (m *WSMonitor) Initialize(coins []string) error {
|
||||
log.Println("初始化WebSocket监控器...")
|
||||
// 获取交易对信息
|
||||
log.Println("Initializing WebSocket monitor...")
|
||||
// Get trading pair information
|
||||
apiClient := NewAPIClient()
|
||||
// 如果不指定交易对,则使用market市场的所有交易对币种
|
||||
// If trading pairs are not specified, use all trading pairs from the market
|
||||
if len(coins) == 0 {
|
||||
exchangeInfo, err := apiClient.GetExchangeInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 筛选永续合约交易对 --仅测试时使用
|
||||
// Filter perpetual contract trading pairs -- only use for testing
|
||||
//exchangeInfo.Symbols = exchangeInfo.Symbols[0:2]
|
||||
for _, symbol := range exchangeInfo.Symbols {
|
||||
if symbol.Status == "TRADING" && symbol.ContractType == "PERPETUAL" && strings.ToUpper(symbol.Symbol[len(symbol.Symbol)-4:]) == "USDT" {
|
||||
@@ -66,10 +66,10 @@ func (m *WSMonitor) Initialize(coins []string) error {
|
||||
m.symbols = coins
|
||||
}
|
||||
|
||||
log.Printf("找到 %d 个交易对", len(m.symbols))
|
||||
// 初始化历史数据
|
||||
log.Printf("Found %d trading pairs", len(m.symbols))
|
||||
// Initialize historical data
|
||||
if err := m.initializeHistoricalData(); err != nil {
|
||||
log.Printf("初始化历史数据失败: %v", err)
|
||||
log.Printf("Failed to initialize historical data: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -79,7 +79,7 @@ func (m *WSMonitor) initializeHistoricalData() error {
|
||||
apiClient := NewAPIClient()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
semaphore := make(chan struct{}, 5) // 限制并发数
|
||||
semaphore := make(chan struct{}, 5) // Limit concurrency
|
||||
|
||||
for _, symbol := range m.symbols {
|
||||
wg.Add(1)
|
||||
@@ -89,25 +89,25 @@ func (m *WSMonitor) initializeHistoricalData() error {
|
||||
defer wg.Done()
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// 获取历史K线数据
|
||||
// Get historical K-line data
|
||||
klines, err := apiClient.GetKlines(s, "3m", 100)
|
||||
if err != nil {
|
||||
log.Printf("获取 %s 历史数据失败: %v", s, err)
|
||||
log.Printf("Failed to get %s historical data: %v", s, err)
|
||||
return
|
||||
}
|
||||
if len(klines) > 0 {
|
||||
m.klineDataMap3m.Store(s, klines)
|
||||
log.Printf("已加载 %s 的历史K线数据-3m: %d 条", s, len(klines))
|
||||
log.Printf("Loaded %s historical K-line data-3m: %d entries", s, len(klines))
|
||||
}
|
||||
// 获取历史K线数据
|
||||
// Get historical K-line data
|
||||
klines4h, err := apiClient.GetKlines(s, "4h", 100)
|
||||
if err != nil {
|
||||
log.Printf("获取 %s 历史数据失败: %v", s, err)
|
||||
log.Printf("Failed to get %s historical data: %v", s, err)
|
||||
return
|
||||
}
|
||||
if len(klines4h) > 0 {
|
||||
m.klineDataMap4h.Store(s, klines4h)
|
||||
log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines4h))
|
||||
log.Printf("Loaded %s historical K-line data-4h: %d entries", s, len(klines4h))
|
||||
}
|
||||
}(symbol)
|
||||
}
|
||||
@@ -117,28 +117,28 @@ func (m *WSMonitor) initializeHistoricalData() error {
|
||||
}
|
||||
|
||||
func (m *WSMonitor) Start(coins []string) {
|
||||
log.Printf("启动WebSocket实时监控...")
|
||||
// 初始化交易对
|
||||
log.Printf("Starting WebSocket real-time monitoring...")
|
||||
// Initialize trading pairs
|
||||
err := m.Initialize(coins)
|
||||
if err != nil {
|
||||
log.Printf("❌ 初始化币种失败: %v", err)
|
||||
log.Printf("❌ Failed to initialize coins: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = m.combinedClient.Connect()
|
||||
if err != nil {
|
||||
log.Printf("❌ 批量订阅流失败: %v", err)
|
||||
log.Printf("❌ Failed to batch subscribe to streams: %v", err)
|
||||
return
|
||||
}
|
||||
// 订阅所有交易对
|
||||
// Subscribe to all trading pairs
|
||||
err = m.subscribeAll()
|
||||
if err != nil {
|
||||
log.Printf("❌ 订阅币种交易对失败: %v", err)
|
||||
log.Printf("❌ Failed to subscribe to coin trading pairs: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// subscribeSymbol 注册监听
|
||||
// subscribeSymbol registers listener
|
||||
func (m *WSMonitor) subscribeSymbol(symbol, st string) []string {
|
||||
var streams []string
|
||||
stream := fmt.Sprintf("%s@kline_%s", strings.ToLower(symbol), st)
|
||||
@@ -149,8 +149,8 @@ func (m *WSMonitor) subscribeSymbol(symbol, st string) []string {
|
||||
return streams
|
||||
}
|
||||
func (m *WSMonitor) subscribeAll() error {
|
||||
// 执行批量订阅
|
||||
log.Println("开始订阅所有交易对...")
|
||||
// Execute batch subscription
|
||||
log.Println("Starting to subscribe to all trading pairs...")
|
||||
for _, symbol := range m.symbols {
|
||||
for _, st := range subKlineTime {
|
||||
m.subscribeSymbol(symbol, st)
|
||||
@@ -159,11 +159,11 @@ func (m *WSMonitor) subscribeAll() error {
|
||||
for _, st := range subKlineTime {
|
||||
err := m.combinedClient.BatchSubscribeKlines(m.symbols, st)
|
||||
if err != nil {
|
||||
log.Printf("❌ 订阅 %s K线失败: %v", st, err)
|
||||
log.Printf("❌ Failed to subscribe to %s K-line: %v", st, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Println("所有交易对订阅完成")
|
||||
log.Println("All trading pair subscriptions completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func (m *WSMonitor) handleKlineData(symbol string, ch <-chan []byte, _time strin
|
||||
for data := range ch {
|
||||
var klineData KlineWSData
|
||||
if err := json.Unmarshal(data, &klineData); err != nil {
|
||||
log.Printf("解析Kline数据失败: %v", err)
|
||||
log.Printf("Failed to parse Kline data: %v", err)
|
||||
continue
|
||||
}
|
||||
m.processKlineUpdate(symbol, klineData, _time)
|
||||
@@ -190,7 +190,7 @@ func (m *WSMonitor) getKlineDataMap(_time string) *sync.Map {
|
||||
return klineDataMap
|
||||
}
|
||||
func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time string) {
|
||||
// 转换WebSocket数据为Kline结构
|
||||
// Convert WebSocket data to Kline structure
|
||||
kline := Kline{
|
||||
OpenTime: wsData.Kline.StartTime,
|
||||
CloseTime: wsData.Kline.CloseTime,
|
||||
@@ -205,22 +205,22 @@ func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time
|
||||
kline.QuoteVolume, _ = parseFloat(wsData.Kline.QuoteVolume)
|
||||
kline.TakerBuyBaseVolume, _ = parseFloat(wsData.Kline.TakerBuyBaseVolume)
|
||||
kline.TakerBuyQuoteVolume, _ = parseFloat(wsData.Kline.TakerBuyQuoteVolume)
|
||||
// 更新K线数据
|
||||
// Update K-line data
|
||||
var klineDataMap = m.getKlineDataMap(_time)
|
||||
value, exists := klineDataMap.Load(symbol)
|
||||
var klines []Kline
|
||||
if exists {
|
||||
klines = value.([]Kline)
|
||||
|
||||
// 检查是否是新的K线
|
||||
// Check if it's a new K-line
|
||||
if len(klines) > 0 && klines[len(klines)-1].OpenTime == kline.OpenTime {
|
||||
// 更新当前K线
|
||||
// Update current K-line
|
||||
klines[len(klines)-1] = kline
|
||||
} else {
|
||||
// 添加新K线
|
||||
// Add new K-line
|
||||
klines = append(klines, kline)
|
||||
|
||||
// 保持数据长度
|
||||
// Maintain data length
|
||||
if len(klines) > 100 {
|
||||
klines = klines[1:]
|
||||
}
|
||||
@@ -233,34 +233,34 @@ func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time
|
||||
}
|
||||
|
||||
func (m *WSMonitor) GetCurrentKlines(symbol string, duration string) ([]Kline, error) {
|
||||
// 对每一个进来的symbol检测是否存在内类 是否的话就订阅它
|
||||
// Check if each incoming symbol exists internally, if not subscribe to it
|
||||
value, exists := m.getKlineDataMap(duration).Load(symbol)
|
||||
if !exists {
|
||||
// 如果Ws数据未初始化完成时,单独使用api获取 - 兼容性代码 (防止在未初始化完成是,已经有交易员运行)
|
||||
// If WS data is not initialized, use API separately - compatibility code (prevents trader from running when not initialized)
|
||||
apiClient := NewAPIClient()
|
||||
klines, err := apiClient.GetKlines(symbol, duration, 100)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取%v分钟K线失败: %v", duration, err)
|
||||
return nil, fmt.Errorf("Failed to get %v-minute K-line: %v", duration, err)
|
||||
}
|
||||
|
||||
// 动态缓存进缓存
|
||||
// Dynamically cache into cache
|
||||
m.getKlineDataMap(duration).Store(strings.ToUpper(symbol), klines)
|
||||
|
||||
// 订阅 WebSocket 流
|
||||
// Subscribe to WebSocket stream
|
||||
subStr := m.subscribeSymbol(symbol, duration)
|
||||
subErr := m.combinedClient.subscribeStreams(subStr)
|
||||
log.Printf("动态订阅流: %v", subStr)
|
||||
log.Printf("Dynamic subscription to stream: %v", subStr)
|
||||
if subErr != nil {
|
||||
log.Printf("警告: 动态订阅%v分钟K线失败: %v (使用API数据)", duration, subErr)
|
||||
log.Printf("Warning: Failed to dynamically subscribe to %v-minute K-line: %v (using API data)", duration, subErr)
|
||||
}
|
||||
|
||||
// ✅ FIX: 返回深拷贝而非引用
|
||||
// ✅ FIX: Return deep copy instead of reference
|
||||
result := make([]Kline, len(klines))
|
||||
copy(result, klines)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ✅ FIX: 返回深拷贝而非引用,避免并发竞态条件
|
||||
// ✅ FIX: Return deep copy instead of reference, avoid concurrent race conditions
|
||||
klines := value.([]Kline)
|
||||
result := make([]Kline, len(klines))
|
||||
copy(result, klines)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// supportedTimeframes 定义支持的时间周期与其对应的分钟数。
|
||||
// supportedTimeframes defines supported timeframes and their corresponding durations.
|
||||
var supportedTimeframes = map[string]time.Duration{
|
||||
"1m": time.Minute,
|
||||
"3m": 3 * time.Minute,
|
||||
@@ -22,7 +22,7 @@ var supportedTimeframes = map[string]time.Duration{
|
||||
"1d": 24 * time.Hour,
|
||||
}
|
||||
|
||||
// NormalizeTimeframe 规范化传入的时间周期字符串(大小写、不带空格),并校验是否受支持。
|
||||
// NormalizeTimeframe normalizes the incoming timeframe string (case-insensitive, no spaces), and validates if it's supported.
|
||||
func NormalizeTimeframe(tf string) (string, error) {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(tf))
|
||||
if trimmed == "" {
|
||||
@@ -34,7 +34,7 @@ func NormalizeTimeframe(tf string) (string, error) {
|
||||
return trimmed, nil
|
||||
}
|
||||
|
||||
// TFDuration 返回给定周期对应的时间长度。
|
||||
// TFDuration returns the time duration corresponding to the given timeframe.
|
||||
func TFDuration(tf string) (time.Duration, error) {
|
||||
norm, err := NormalizeTimeframe(tf)
|
||||
if err != nil {
|
||||
@@ -43,7 +43,7 @@ func TFDuration(tf string) (time.Duration, error) {
|
||||
return supportedTimeframes[norm], nil
|
||||
}
|
||||
|
||||
// MustNormalizeTimeframe 与 NormalizeTimeframe 类似,但在不受支持时 panic。
|
||||
// MustNormalizeTimeframe is similar to NormalizeTimeframe, but panics when unsupported.
|
||||
func MustNormalizeTimeframe(tf string) string {
|
||||
norm, err := NormalizeTimeframe(tf)
|
||||
if err != nil {
|
||||
@@ -52,7 +52,7 @@ func MustNormalizeTimeframe(tf string) string {
|
||||
return norm
|
||||
}
|
||||
|
||||
// SupportedTimeframes 返回所有受支持的时间周期(已排序的切片)。
|
||||
// SupportedTimeframes returns all supported timeframes (sorted slice).
|
||||
func SupportedTimeframes() []string {
|
||||
keys := make([]string, 0, len(supportedTimeframes))
|
||||
for k := range supportedTimeframes {
|
||||
|
||||
@@ -2,12 +2,12 @@ package market
|
||||
|
||||
import "time"
|
||||
|
||||
// Data 市场数据结构
|
||||
// Data market data structure
|
||||
type Data struct {
|
||||
Symbol string
|
||||
CurrentPrice float64
|
||||
PriceChange1h float64 // 1小时价格变化百分比
|
||||
PriceChange4h float64 // 4小时价格变化百分比
|
||||
PriceChange1h float64 // 1-hour price change percentage
|
||||
PriceChange4h float64 // 4-hour price change percentage
|
||||
CurrentEMA20 float64
|
||||
CurrentMACD float64
|
||||
CurrentRSI7 float64
|
||||
@@ -15,30 +15,30 @@ type Data struct {
|
||||
FundingRate float64
|
||||
IntradaySeries *IntradayData
|
||||
LongerTermContext *LongerTermData
|
||||
// 多时间周期数据(新增)
|
||||
// Multi-timeframe data (new)
|
||||
TimeframeData map[string]*TimeframeSeriesData `json:"timeframe_data,omitempty"`
|
||||
}
|
||||
|
||||
// TimeframeSeriesData 单个时间周期的序列数据
|
||||
// TimeframeSeriesData series data for a single timeframe
|
||||
type TimeframeSeriesData struct {
|
||||
Timeframe string `json:"timeframe"` // 时间周期标识,如 "5m", "15m", "1h"
|
||||
MidPrices []float64 `json:"mid_prices"` // 价格序列
|
||||
EMA20Values []float64 `json:"ema20_values"` // EMA20 序列
|
||||
EMA50Values []float64 `json:"ema50_values"` // EMA50 序列
|
||||
MACDValues []float64 `json:"macd_values"` // MACD 序列
|
||||
RSI7Values []float64 `json:"rsi7_values"` // RSI7 序列
|
||||
RSI14Values []float64 `json:"rsi14_values"` // RSI14 序列
|
||||
Volume []float64 `json:"volume"` // 成交量序列
|
||||
Timeframe string `json:"timeframe"` // Timeframe identifier, e.g. "5m", "15m", "1h"
|
||||
MidPrices []float64 `json:"mid_prices"` // Price series
|
||||
EMA20Values []float64 `json:"ema20_values"` // EMA20 series
|
||||
EMA50Values []float64 `json:"ema50_values"` // EMA50 series
|
||||
MACDValues []float64 `json:"macd_values"` // MACD series
|
||||
RSI7Values []float64 `json:"rsi7_values"` // RSI7 series
|
||||
RSI14Values []float64 `json:"rsi14_values"` // RSI14 series
|
||||
Volume []float64 `json:"volume"` // Volume series
|
||||
ATR14 float64 `json:"atr14"` // ATR14
|
||||
}
|
||||
|
||||
// OIData Open Interest数据
|
||||
// OIData Open Interest data
|
||||
type OIData struct {
|
||||
Latest float64
|
||||
Average float64
|
||||
}
|
||||
|
||||
// IntradayData 日内数据(3分钟间隔)
|
||||
// IntradayData intraday data (3-minute interval)
|
||||
type IntradayData struct {
|
||||
MidPrices []float64
|
||||
EMA20Values []float64
|
||||
@@ -49,7 +49,7 @@ type IntradayData struct {
|
||||
ATR14 float64
|
||||
}
|
||||
|
||||
// LongerTermData 长期数据(4小时时间框架)
|
||||
// LongerTermData longer-term data (4-hour timeframe)
|
||||
type LongerTermData struct {
|
||||
EMA20 float64
|
||||
EMA50 float64
|
||||
@@ -61,7 +61,7 @@ type LongerTermData struct {
|
||||
RSI14Values []float64
|
||||
}
|
||||
|
||||
// Binance API 响应结构
|
||||
// Binance API response structure
|
||||
type ExchangeInfo struct {
|
||||
Symbols []SymbolInfo `json:"symbols"`
|
||||
}
|
||||
@@ -105,7 +105,7 @@ type Ticker24hr struct {
|
||||
QuoteVolume string `json:"quoteVolume"`
|
||||
}
|
||||
|
||||
// 特征数据结构
|
||||
// SymbolFeatures feature data structure
|
||||
type SymbolFeatures struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
@@ -126,7 +126,7 @@ type SymbolFeatures struct {
|
||||
PositionInRange float64 `json:"position_in_range"`
|
||||
}
|
||||
|
||||
// 警报数据结构
|
||||
// Alert alert data structure
|
||||
type Alert struct {
|
||||
Type string `json:"type"`
|
||||
Symbol string `json:"symbol"`
|
||||
@@ -150,10 +150,10 @@ type AlertThresholds struct {
|
||||
RSIOversold float64 `json:"rsi_oversold"`
|
||||
}
|
||||
type CleanupConfig struct {
|
||||
InactiveTimeout time.Duration `json:"inactive_timeout"` // 不活跃超时时间
|
||||
MinScoreThreshold float64 `json:"min_score_threshold"` // 最低评分阈值
|
||||
NoAlertTimeout time.Duration `json:"no_alert_timeout"` // 无警报超时时间
|
||||
CheckInterval time.Duration `json:"check_interval"` // 检查间隔
|
||||
InactiveTimeout time.Duration `json:"inactive_timeout"` // Inactive timeout duration
|
||||
MinScoreThreshold float64 `json:"min_score_threshold"` // Minimum score threshold
|
||||
NoAlertTimeout time.Duration `json:"no_alert_timeout"` // No alert timeout duration
|
||||
CheckInterval time.Duration `json:"check_interval"` // Check interval
|
||||
}
|
||||
|
||||
var config = Config{
|
||||
|
||||
@@ -83,16 +83,16 @@ func (w *WSClient) Connect() error {
|
||||
|
||||
conn, _, err := dialer.Dial("wss://ws-fapi.binance.com/ws-fapi/v1", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebSocket连接失败: %v", err)
|
||||
return fmt.Errorf("WebSocket connection failed: %v", err)
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
w.conn = conn
|
||||
w.mu.Unlock()
|
||||
|
||||
log.Println("WebSocket连接成功")
|
||||
log.Println("WebSocket connected successfully")
|
||||
|
||||
// 启动消息读取循环
|
||||
// Start message reading loop
|
||||
go w.readMessages()
|
||||
|
||||
return nil
|
||||
@@ -124,7 +124,7 @@ func (w *WSClient) subscribe(stream string) error {
|
||||
defer w.mu.RUnlock()
|
||||
|
||||
if w.conn == nil {
|
||||
return fmt.Errorf("WebSocket未连接")
|
||||
return fmt.Errorf("WebSocket not connected")
|
||||
}
|
||||
|
||||
err := w.conn.WriteJSON(subscribeMsg)
|
||||
@@ -132,7 +132,7 @@ func (w *WSClient) subscribe(stream string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("订阅流: %s", stream)
|
||||
log.Printf("Subscribing to stream: %s", stream)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func (w *WSClient) readMessages() {
|
||||
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("读取WebSocket消息失败: %v", err)
|
||||
log.Printf("Failed to read WebSocket message: %v", err)
|
||||
w.handleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func (w *WSClient) readMessages() {
|
||||
func (w *WSClient) handleMessage(message []byte) {
|
||||
var wsMsg WSMessage
|
||||
if err := json.Unmarshal(message, &wsMsg); err != nil {
|
||||
// 可能是其他格式的消息
|
||||
// Might be a different message format
|
||||
return
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func (w *WSClient) handleMessage(message []byte) {
|
||||
select {
|
||||
case ch <- wsMsg.Data:
|
||||
default:
|
||||
log.Printf("订阅者通道已满: %s", wsMsg.Stream)
|
||||
log.Printf("Subscriber channel is full: %s", wsMsg.Stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,11 +188,11 @@ func (w *WSClient) handleReconnect() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("尝试重新连接...")
|
||||
log.Println("Attempting to reconnect...")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
if err := w.Connect(); err != nil {
|
||||
log.Printf("重新连接失败: %v", err)
|
||||
log.Printf("Reconnection failed: %v", err)
|
||||
go w.handleReconnect()
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (w *WSClient) Close() {
|
||||
w.conn = nil
|
||||
}
|
||||
|
||||
// 关闭所有订阅者通道
|
||||
// Close all subscriber channels
|
||||
for stream, ch := range w.subscribers {
|
||||
close(ch)
|
||||
delete(w.subscribers, stream)
|
||||
|
||||
Reference in New Issue
Block a user