Files
AITrading/go-service/main.go
2026-03-26 14:13:44 +08:00

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
}