refactor: standardize code comments

This commit is contained in:
tinkle-community
2025-12-08 01:40:48 +08:00
parent 0636ced476
commit a12c0ae8c9
103 changed files with 5466 additions and 5468 deletions

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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 (14period): %.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")

View File

@@ -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},

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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{

View File

@@ -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)