132 lines
2.7 KiB
Go
132 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// TradeHandler handles publicTrade stream messages, feeding the trade aggregator and writer.
|
|
type TradeHandler struct {
|
|
cfg Config
|
|
storage *StreamStorage
|
|
tickCh chan Tick
|
|
aggregator *Aggregator
|
|
writer *Writer
|
|
}
|
|
|
|
// NewTradeHandler initializes the trade handler, tick channel, aggregator, and writer.
|
|
func NewTradeHandler(cfg Config, sm *StorageManager) (*TradeHandler, error) {
|
|
ss := sm.GetStreamStorage("trades")
|
|
if ss == nil {
|
|
return nil, fmt.Errorf("trades stream storage not found")
|
|
}
|
|
|
|
tickCh := make(chan Tick, cfg.TickChannelBuffer)
|
|
|
|
// Aggregator uses trade features.db
|
|
featDB, err := OpenDBWithAutoVacuum(ss.DBPath("features.db"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open trade features db: %w", err)
|
|
}
|
|
|
|
var lastPrice float64
|
|
err = featDB.QueryRow(`
|
|
SELECT close_price FROM five_second_features
|
|
ORDER BY timestamp DESC LIMIT 1
|
|
`).Scan(&lastPrice)
|
|
if err != nil {
|
|
lastPrice = 0
|
|
}
|
|
|
|
agg := &Aggregator{
|
|
featDB: featDB,
|
|
lastPrice: lastPrice,
|
|
}
|
|
|
|
hotDB, err := OpenDBWithAutoVacuum(ss.DBPath("hot_ticks.db"))
|
|
if err != nil {
|
|
featDB.Close()
|
|
return nil, fmt.Errorf("open hot_ticks db: %w", err)
|
|
}
|
|
hotDB.SetMaxOpenConns(1)
|
|
|
|
w := &Writer{
|
|
tickCh: tickCh,
|
|
hotDB: hotDB,
|
|
batchSize: cfg.WriterBatchSize,
|
|
flushMs: cfg.WriterFlushIntervalMs,
|
|
}
|
|
|
|
return &TradeHandler{
|
|
cfg: cfg,
|
|
storage: ss,
|
|
tickCh: tickCh,
|
|
aggregator: agg,
|
|
writer: w,
|
|
}, nil
|
|
}
|
|
|
|
func (th *TradeHandler) Topics() []string {
|
|
return []string{fmt.Sprintf("publicTrade.%s", th.cfg.Symbol)}
|
|
}
|
|
|
|
func (th *TradeHandler) HandleMessage(data []byte) {
|
|
var msg BybitWSMessage
|
|
if err := json.Unmarshal(data, &msg); err != nil {
|
|
return
|
|
}
|
|
|
|
if len(msg.Data) == 0 {
|
|
return
|
|
}
|
|
|
|
recvTS := time.Now().UnixMilli()
|
|
|
|
for _, raw := range msg.Data {
|
|
price, err := strconv.ParseFloat(raw.P, 64)
|
|
if err != nil {
|
|
log.Printf("[trade_handler] bad price %q: %v", raw.P, err)
|
|
continue
|
|
}
|
|
|
|
volume, err := strconv.ParseFloat(raw.V, 64)
|
|
if err != nil {
|
|
log.Printf("[trade_handler] bad volume %q: %v", raw.V, err)
|
|
continue
|
|
}
|
|
|
|
tick := Tick{
|
|
TradeID: raw.I,
|
|
Seq: raw.Seq,
|
|
TradeTS: raw.T,
|
|
MessageTS: msg.TS,
|
|
RecvTS: recvTS,
|
|
Symbol: raw.S,
|
|
Side: raw.SD,
|
|
Price: price,
|
|
Volume: volume,
|
|
TickDir: raw.L,
|
|
BlockTrade: raw.BT,
|
|
RPI: raw.RPI,
|
|
}
|
|
|
|
// Feed feature generator
|
|
th.aggregator.ProcessTick(tick)
|
|
|
|
// Feed writer
|
|
select {
|
|
case th.tickCh <- tick:
|
|
default:
|
|
log.Println("[trade_handler] WARNING: tick channel full, dropping tick")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (th *TradeHandler) Close() {
|
|
th.aggregator.Close()
|
|
close(th.tickCh)
|
|
}
|