172 lines
4.0 KiB
Go
172 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
binance "github.com/adshao/go-binance/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
type KlineMessage struct {
|
|
Type string `json:"type"`
|
|
Source string `json:"source"`
|
|
Symbol string `json:"symbol"`
|
|
Interval string `json:"interval"`
|
|
EventTime int64 `json:"event_time"`
|
|
OpenTime int64 `json:"open_time"`
|
|
CloseTime int64 `json:"close_time"`
|
|
Open string `json:"open"`
|
|
High string `json:"high"`
|
|
Low string `json:"low"`
|
|
Close string `json:"close"`
|
|
Volume string `json:"volume"`
|
|
TradeNum int64 `json:"trade_num"`
|
|
Final bool `json:"final"`
|
|
}
|
|
|
|
type RedisPublisher interface {
|
|
Publish(ctx context.Context, channel string, payload []byte) error
|
|
Close() error
|
|
}
|
|
|
|
type NoopPublisher struct{}
|
|
|
|
func (p *NoopPublisher) Publish(_ context.Context, _ string, _ []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *NoopPublisher) Close() error {
|
|
return nil
|
|
}
|
|
|
|
type RedisChannelPublisher struct {
|
|
client *redis.Client
|
|
}
|
|
|
|
func (p *RedisChannelPublisher) Publish(ctx context.Context, channel string, payload []byte) error {
|
|
return p.client.Publish(ctx, channel, payload).Err()
|
|
}
|
|
|
|
func (p *RedisChannelPublisher) Close() error {
|
|
return p.client.Close()
|
|
}
|
|
|
|
func main() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
symbol := getEnv("BINANCE_SYMBOL", "btcusdt")
|
|
interval := getEnv("BINANCE_INTERVAL", "1m")
|
|
channel := getEnv("REDIS_CHANNEL", "kline.stream")
|
|
|
|
publisher, err := initPublisherFromEnv(ctx)
|
|
if err != nil {
|
|
log.Fatalf("init publisher failed: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := publisher.Close(); err != nil {
|
|
log.Printf("publisher close failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
doneC, stopC, err := binance.WsKlineServe(symbol, interval,
|
|
func(event *binance.WsKlineEvent) {
|
|
msg := KlineMessage{
|
|
Type: "kline",
|
|
Source: "binance_ws",
|
|
Symbol: strings.ToLower(event.Symbol),
|
|
Interval: event.Kline.Interval,
|
|
EventTime: event.Time,
|
|
OpenTime: event.Kline.StartTime,
|
|
CloseTime: event.Kline.EndTime,
|
|
Open: event.Kline.Open,
|
|
High: event.Kline.High,
|
|
Low: event.Kline.Low,
|
|
Close: event.Kline.Close,
|
|
Volume: event.Kline.Volume,
|
|
TradeNum: int64(event.Kline.TradeNum),
|
|
Final: event.Kline.IsFinal,
|
|
}
|
|
|
|
payload, err := json.Marshal(msg)
|
|
if err != nil {
|
|
log.Printf("marshal failed: %v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Println(string(payload))
|
|
|
|
if err := publisher.Publish(ctx, channel, payload); err != nil {
|
|
log.Printf("redis publish failed: %v", err)
|
|
}
|
|
},
|
|
func(err error) {
|
|
log.Printf("ws error: %v", err)
|
|
},
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("start ws stream failed: %v", err)
|
|
}
|
|
|
|
log.Printf("subscribed binance kline stream: symbol=%s interval=%s channel=%s", symbol, interval, channel)
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
select {
|
|
case <-sig:
|
|
log.Println("received shutdown signal")
|
|
close(stopC)
|
|
cancel()
|
|
case <-doneC:
|
|
log.Println("ws stream closed")
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
func initPublisherFromEnv(ctx context.Context) (RedisPublisher, error) {
|
|
addr := strings.TrimSpace(os.Getenv("REDIS_ADDR"))
|
|
if addr == "" {
|
|
log.Println("REDIS_ADDR not configured, using NoopPublisher")
|
|
return &NoopPublisher{}, nil
|
|
}
|
|
|
|
db, err := strconv.Atoi(getEnv("REDIS_DB", "0"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid REDIS_DB: %w", err)
|
|
}
|
|
|
|
client := redis.NewClient(&redis.Options{
|
|
Addr: addr,
|
|
Password: os.Getenv("REDIS_PASSWORD"),
|
|
DB: db,
|
|
DialTimeout: 5 * time.Second,
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 3 * time.Second,
|
|
})
|
|
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
return nil, fmt.Errorf("redis ping failed: %w", err)
|
|
}
|
|
|
|
log.Printf("connected redis at %s db=%d", addr, db)
|
|
return &RedisChannelPublisher{client: client}, nil
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
v := strings.TrimSpace(os.Getenv(key))
|
|
if v == "" {
|
|
return fallback
|
|
}
|
|
return v
|
|
}
|