Compare commits

...

5 Commits

  1. 4
      README.md
  2. 2
      cmd/exchange/exchange_test.go
  3. 24
      cmd/exchange/main.go
  4. 10
      cmd/test/test.go
  5. 2
      config/config.toml
  6. 1097
      config/kvrocks/kvrocks.conf
  7. 6
      docker-compose.yml
  8. 4
      go.mod
  9. 4
      go.sum
  10. 42
      internal/exchange/exchange.go
  11. 314
      internal/exchange/exchange_grpc_server.go
  12. 43
      internal/exchange/okx/channel_kline.go
  13. 45
      internal/exchange/okx/okx.go
  14. 126
      internal/exchange/okx/okx_fetch.go
  15. 46
      internal/exchange/okx/okx_fetch_test.go
  16. 12
      internal/exchange/okx/okx_limiter.go
  17. 70
      internal/exchange/okx/okx_subscriber.go
  18. 7
      internal/exchange/okx/types.go
  19. 10
      pkg/aside/trade_instance_client.go
  20. 53
      pkg/storage/kvrocks/kvrocks.go
  21. 12
      pkg/storage/tsdb/victoria_metrics/metric.go
  22. 68
      pkg/storage/tsdb/victoria_metrics/vm.go
  23. 2
      pkg/storage/tsdb/victoria_metrics/vm_test.go
  24. 13
      pkg/types/exchange.go
  25. 10
      pkg/types/instance.go
  26. 89
      pkg/types/interval.go
  27. 3
      pkg/types/kline.go
  28. 34
      pkg/utils/collect/sync_map.go
  29. 117
      pkg/utils/promise/promise.go
  30. 12
      pkg/utils/promise/promise_test.go
  31. 9
      pkg/utils/times/times.go

4
README.md

@ -46,5 +46,5 @@ k线推送
- closed -> cache -> async tsdb -> kline signal
- query(监控成功率/缓存命中率) -> cache -> tsdb
价格/交易量精度->tsdb读写存储
kline时间窗口

2
cmd/exchange/exchange_test.go

@ -47,7 +47,7 @@ func TestExchange(t *testing.T) {
klines = klines[:0]
}
zlog.Infof("channel: %s, instId: %s, datas: %#v", candle.InstId, candle.Exchange, candle.Klines[0])
zlog.Infof("channel: %s, instId: %s, datas: %#v", candle.ExgInstId, candle.Exchange, candle.Klines[0])
}
}

24
cmd/exchange/main.go

@ -11,6 +11,7 @@ import (
"sig-pub/pkg/config"
"sig-pub/pkg/grpc/discovery"
"sig-pub/pkg/grpc/interceptor"
"sig-pub/pkg/storage/kvrocks"
vmts "sig-pub/pkg/storage/tsdb/victoria_metrics"
"sig-pub/pkg/utils/exit"
"sig-pub/pkg/zlog"
@ -38,11 +39,6 @@ func main() {
panic(err)
}
okxExchange := okx.NewOkxExchange(conf.Exchange.Okx)
if err := okxExchange.Init(); err != nil {
panic(err)
}
etcdClient, err := clientv3.New(conf.Etcd)
if err != nil {
panic(err)
@ -65,7 +61,23 @@ func main() {
}
marketClient := pb.NewMarketClient(conn)
tradeInstanceAside := aside.NewTradeInstanceAside(marketClient)
exchangeService := exchange.NewExchangeGrpcServer(tradeInstanceAside, tsdbService, okxExchange)
// kvrocks db
kvdb := kvrocks.NewKVRocksDB(conf.Database.Kvrocks)
if err := kvdb.Ping(); err != nil {
panic(err)
}
// okx exchange
okxSubscriber := okx.NewOkxSubscriber(conf.Exchange.Okx)
if err := okxSubscriber.Init(); err != nil {
panic(err)
}
okxFetcher := okx.NewOkxFetcher(conf.Exchange.Okx.HttpProxy)
okxExchange := exchange.NewExchange(okxFetcher, okxSubscriber)
// exhcange main service
exchangeService := exchange.NewExchangeGrpcServer(tradeInstanceAside, tsdbService, kvdb, okxExchange)
if err := exchangeService.Init(); err != nil {
panic(err)
}

10
cmd/test/test.go

@ -1,8 +1,6 @@
package main
import (
"sig-pub/api/pb"
"sig-pub/pkg/types"
"sig-pub/pkg/zlog"
"github.com/VictoriaMetrics/metrics"
@ -24,10 +22,10 @@ type BTC struct {
func testDecimal() {
// zlog.Init()
var interval = "1m"
interval0 := types.Interval(interval)
sec, _ := interval0.Seconds()
zlog.Infof("hello...: %s, %s, %d", pb.Exchange_OKX.String(), pb.Exchange_BINANCE.String(), sec)
// var interval = "1m"
// interval0 := types.Interval(interval)
// sec, _ := interval0.Seconds()
// zlog.Infof("hello...: %s, %s, %d", pb.Exchange_OKX.String(), pb.Exchange_BINANCE.String())
// data := `{"price":"1.23456"}`
// btc := new(BTC)

2
config/config.toml

@ -59,7 +59,7 @@ receiveBuffer = 4096
marketSubscribeLimit = 16
consumeBatch = 1024
consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume
httpProxy = "http://192.168.1.5:7890"
httpProxy = "http://192.168.1.6:7890"
# 模拟盘API交易地址如下:
# REST:https://www.okx.com

1097
config/kvrocks/kvrocks.conf

File diff suppressed because it is too large Load Diff

6
docker-compose.yml

@ -27,14 +27,12 @@ services:
container_name: sig-kvrocks
hostname: sig-kvrocks
user: 'root'
network_mode: host
# restart: always
sysctls:
net.core.somaxconn: 1024
volumes:
- "/etc/localtime:/etc/localtime:ro"
- "./config/kvrocks/kvrocks.conf:/var/lib/kvrocks/kvrocks.conf:ro"
- "./fs/kvrocks_data:/var/lib/kvrocks"
ports:
- '7666:6666'
command: --bind 0.0.0.0 --dir /var/lib/kvrocks
sig-mysql:
container_name: sig-mysql

4
go.mod

@ -9,6 +9,7 @@ require (
github.com/dsnet/golib/unitconv v1.0.2
github.com/fanjindong/go-cache v0.0.6
github.com/gin-gonic/gin v1.10.0
github.com/go-resty/resty/v2 v2.16.5
github.com/gorilla/websocket v1.5.3
github.com/govalues/decimal v0.1.36
github.com/influxdata/influxdb-client-go/v2 v2.14.0
@ -21,6 +22,8 @@ require (
go.etcd.io/etcd/client/v3 v3.6.1
go.uber.org/zap v1.27.0
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0
golang.org/x/time v0.8.0
google.golang.org/grpc v1.71.1
google.golang.org/protobuf v1.36.6
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@ -79,7 +82,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect

4
go.sum

@ -57,6 +57,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
@ -225,6 +227,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

42
internal/exchange/exchange.go

@ -1,6 +1,11 @@
package exchange
import "sig-pub/pkg/types"
import (
"context"
"fmt"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/collect"
)
// 交易所行情数据订阅
type ExchangeSubscriber interface {
@ -19,4 +24,39 @@ type ExchangeSubscriber interface {
// 交易所行情数据请求
type ExchangeFetcher interface {
// 交易所类型
ExhcangeType() types.Exchange
// 获取区间内历史k线数据
FetchHistoryKlines(ctx context.Context, instId string, interval types.Interval, after, before int64) (klines []*types.Kline, err error)
}
// 交易所交互接口
type Exchange struct {
ExType types.Exchange
Fetcher ExchangeFetcher
Subscriber ExchangeSubscriber
Insts *collect.SyncMap[string, *ExchangeTradeInstance] // map[string]*ExchangeTradeInstance // <ExchangeInstId, Inst>
// sync.RWMutex
}
// 交易所交易产品
type ExchangeTradeInstance struct {
Inst *types.TradeInstance
Status int32 // 交易产品状态, 0.初始化中 1.正常
LiveMarkTs int64 // websocket订阅k线标记时间戳
HistoryMarkTs int64 // 拉取历史k线标记时间戳
}
func NewExchange(fetcher ExchangeFetcher, subscriber ExchangeSubscriber) *Exchange {
exType := fetcher.ExhcangeType()
if exType != subscriber.ExhcangeType() {
panic(fmt.Errorf("exchange type not match: %#v, %#v", exType, subscriber.ExhcangeType()))
}
return &Exchange{
ExType: exType,
Fetcher: fetcher,
Subscriber: subscriber,
Insts: collect.NewSyncMap[string, *ExchangeTradeInstance](),
}
}

314
internal/exchange/exchange_grpc_server.go

@ -4,54 +4,49 @@ import (
"context"
"fmt"
"io"
"runtime"
"sig-pub/api/pb"
"sig-pub/pkg/aside"
"sig-pub/pkg/data/entity"
"sig-pub/pkg/data"
"sig-pub/pkg/storage/kvrocks"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/times"
"sig-pub/pkg/zlog"
"sync"
"sync/atomic"
"time"
"google.golang.org/grpc"
)
type Exchange struct {
Type pb.Exchange
Subscriber ExchangeSubscriber
Insts map[string]*entity.TradeInstanceExchange
sync.RWMutex
}
type ExchangeGrpcServer struct {
pb.UnimplementedExchangeServiceServer
exchangeMap map[pb.Exchange]*Exchange
exchangeMap map[types.Exchange]*Exchange
tradeInstanceAside *aside.TradeInstanceAside
exchangeDataService *ExchangeDataService
kvdb *kvrocks.KVRocksDB
klineStreamId int64
klinePublisher *Publisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]]
}
// exchanges: 支持的数据源交易所
func NewExchangeGrpcServer(tradeInstanceAside *aside.TradeInstanceAside, exchangeDataService *ExchangeDataService, exchanges ...ExchangeSubscriber) *ExchangeGrpcServer {
exchangeMap := make(map[pb.Exchange]*Exchange)
func NewExchangeGrpcServer(
tradeInstanceAside *aside.TradeInstanceAside,
exchangeDataService *ExchangeDataService,
kvdb *kvrocks.KVRocksDB,
exchanges ...*Exchange,
) *ExchangeGrpcServer {
exchangeMap := make(map[types.Exchange]*Exchange)
for _, exchange := range exchanges {
exchangeType := exchange.ExhcangeType()
pbExchangeType, ok := exchangeType.Exchange2PB()
if !ok {
panic(fmt.Errorf("unknown exchange: %#v", exchangeType))
}
exchangeMap[pbExchangeType] = &Exchange{
Type: pbExchangeType,
Subscriber: exchange,
Insts: make(map[string]*entity.TradeInstanceExchange),
}
exchangeMap[exchange.ExType] = exchange
}
return &ExchangeGrpcServer{
exchangeMap: exchangeMap,
tradeInstanceAside: tradeInstanceAside,
exchangeDataService: exchangeDataService,
kvdb: kvdb,
klinePublisher: NewPublisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]](16),
}
}
@ -68,18 +63,35 @@ func (svc *ExchangeGrpcServer) subscribeExchanges() {
for _, exchange := range svc.exchangeMap {
go func(exchange *Exchange) {
// get exchange trade instances
insts, err := svc.tradeInstanceAside.ListExchangeTradeInstance(context.Background(), exchange.Type)
insts, err := svc.tradeInstanceAside.ListExchangeTradeInstance(context.Background(), exchange.ExType)
if err != nil {
zlog.Error(err)
return
}
var exchangeInstIds []string
exchange.Lock()
var processingInsts []types.TradeInstance
for _, inst := range insts {
exchangeInstIds = append(exchangeInstIds, inst.ExchangeInstId)
exchange.Insts[inst.ExchangeInstId] = inst
tradeInst := &types.TradeInstance{
InstId: inst.InstId,
Status: inst.Status,
PriceSz: 0,
QuantitySz: 0,
ExchangeInstId: inst.ExchangeInstId,
Exchange: exchange.ExType,
}
exchange.Insts.Store(inst.ExchangeInstId, &ExchangeTradeInstance{
Inst: tradeInst,
Status: 0,
LiveMarkTs: 0,
HistoryMarkTs: 0,
})
// 待初始化币种数据
if inst.Status == data.StatusProcessing {
processingInsts = append(processingInsts, *tradeInst)
}
}
exchange.Unlock()
// instIds := []string{"BTC-USDT", "DOGE-USDT-SWAP"}
err = exchange.Subscriber.SubscribeKline(exchangeInstIds...)
@ -87,16 +99,24 @@ func (svc *ExchangeGrpcServer) subscribeExchanges() {
zlog.Error(err)
return
}
go func() {
c := exchange.Subscriber.ConsumerKline()
svc.consumerKline(exchange, c)
// todo subscribe books 订单簿
zlog.Infof("unsubscribe exchange: %s", exchange.Type.String())
zlog.Infof("unsubscribe exchange: %s", exchange.ExType)
}()
// 初始化k线数据
go svc.initialKlines(exchange, processingInsts)
}(exchange)
}
}
// consumerKline 消费交易所k线数据
func (svc *ExchangeGrpcServer) consumerKline(exchange *Exchange, c <-chan *types.ChannelKline) {
exchangeType := exchange.ExType
for {
channelK, ok := <-c
if !ok {
@ -104,53 +124,43 @@ func (svc *ExchangeGrpcServer) consumerKline(exchange *Exchange, c <-chan *types
}
// 交易所 instid 转 sig-instid
var exInst *entity.TradeInstanceExchange
exchange.RLock()
if inst, ok := exchange.Insts[channelK.InstId]; ok && inst != nil {
exchange.RUnlock()
exInst = inst
var exInst *types.TradeInstance
if inst, ok := exchange.Insts.Load(channelK.ExgInstId); ok && inst != nil {
exInst = inst.Inst
// 标记交易产品开始订阅k线时间
if inst.LiveMarkTs == 0 && len(channelK.Klines) > 0 {
inst.LiveMarkTs = channelK.Klines[0].Ts
}
} else {
exchange.RUnlock()
zlog.Errorf("unknown exchange instId: %v, %s", channelK.Exchange, channelK.InstId)
zlog.Errorf("unknown exchange instId: %v, %s", channelK.Exchange, channelK.ExgInstId)
continue
}
// tsdb storage
typeInst := types.TradeInstance{
InstId: exInst.InstId, // channelK.InstId
TickSz: 0,
MinSz: 0,
}
// todo 异步处理
err := svc.exchangeDataService.SaveKlines(typeInst, channelK.Klines)
if err != nil {
zlog.Errorf("kline save to tsdb error: ", err)
}
// publish to subscribers
pubMsgMap := make(map[string]*pb.StreamKline)
// instId := channelK.InstId
exchange, ok := channelK.Exchange.Exchange2PB()
if !ok {
zlog.Errorf("unknown exchange kline: %v", channelK.Exchange)
pbExType, err := channelK.Exchange.Exchange2PB()
if err != nil {
zlog.Error(err)
continue
}
exchangeName := exchange.String()
var confirmKlines []*types.Kline
for _, kline := range channelK.Klines {
// zlog.Infof("recv kline: %#v", kline)
confirm := 0
if kline.Confirm {
confirm = 1
confirmKlines = append(confirmKlines, kline)
}
pubKey := fmt.Sprintf("/kline/%s/%s/%s/%d", exchangeName, exInst.InstId, kline.Interval, confirm)
pubKey := fmt.Sprintf("/kline/%s/%s/%s/%d", exchangeType, exInst.InstId, kline.Interval, confirm)
// todo 优化没有订阅者就跳过
msg, ok := pubMsgMap[pubKey]
if !ok {
msg = new(pb.StreamKline)
msg.InstId = exInst.InstId
msg.Exchange = exchange
msg.Exchange = pbExType
pubMsgMap[pubKey] = msg
}
@ -158,6 +168,15 @@ func (svc *ExchangeGrpcServer) consumerKline(exchange *Exchange, c <-chan *types
msg.Klines = append(msg.Klines, pbk)
}
// tsdb storage
if len(confirmKlines) > 0 {
// todo 异步处理
err := svc.exchangeDataService.SaveKlines(*exInst, confirmKlines)
if err != nil {
zlog.Errorf("kline save to tsdb error: ", err)
}
}
for pubKey, msg := range pubMsgMap {
if len(msg.Klines) == 0 {
continue
@ -269,3 +288,196 @@ func (svc *ExchangeGrpcServer) SubscribeKline(stream grpc.BidiStreamingServer[pb
// }
// }
}
const (
KlineBefore0 int64 = 1672502400000 // k线开始数据 2023-01-01 00:00:00 GMT+8
HistoryKlineTsKey string = "history-kline-ts:%s:%s:%s" // exchange:sig-instid:interval
)
type initialKlineTask struct {
inst types.TradeInstance
interval types.Interval
afterTs int64
beforeTs int64
times int32 // 重试次数
}
func (t initialKlineTask) logKey() string {
return fmt.Sprintf("%s:%s:%s:%d:%d", t.inst.Exchange, t.inst.InstId, t.interval, t.beforeTs, t.afterTs)
}
// initialKline 初始化交易产品历史k线数据
func (svc *ExchangeGrpcServer) initialKlines(exchange *Exchange, insts []types.TradeInstance) {
concurrent := max(8, runtime.NumCPU()*2)
// 任务 channel
taskCh := make(chan initialKlineTask, concurrent)
// 任务生成
go func() {
for _, inst := range insts {
// for interval, intervalAdder := range types.SupportedIntervals {
interval := types.Interval1d
intervalAdder := types.SupportedIntervals[interval]
// history 未补全前, history写 kvdb ts mark, 补全后 ws live 写 ts mark
tsKey := fmt.Sprintf(HistoryKlineTsKey, inst.Exchange, inst.InstId, interval)
beforeTs, err := svc.kvdb.GetI64(context.Background(), tsKey)
if err != nil {
zlog.Error(err)
panic(err)
}
if beforeTs == 0 {
beforeTs = intervalAdder(KlineBefore0, -1)
}
for {
afterTs := intervalAdder(beforeTs, 101)
taskCh <- initialKlineTask{
inst: inst,
interval: interval,
afterTs: afterTs,
beforeTs: beforeTs,
times: 0,
}
beforeTs = intervalAdder(afterTs, -1)
// 对比 ws 获取的实时k线
inst, ok := exchange.Insts.Load(inst.ExchangeInstId)
if !ok {
break
}
// 订阅完成
if inst.LiveMarkTs != 0 && beforeTs > inst.LiveMarkTs {
// history status -> ok
break
}
if beforeTs > time.Now().UnixMilli() {
if inst.LiveMarkTs != 0 {
// history status -> ok
} else {
// live status -> not ok
}
break
}
}
// }
}
close(taskCh)
}()
loc, _ := time.LoadLocation("Asia/Shanghai")
// 任务消费器 8协程并行
wg := new(sync.WaitGroup)
for range concurrent {
wg.Add(1)
go func() {
defer wg.Done()
for {
task, ok := <-taskCh
if !ok {
break
}
if task.times > 0 {
zlog.Infof("retry fetch history kline task %d times: task -> %s", task.times, task.logKey())
}
interval, afterTs, beforeTs := task.interval, task.afterTs, task.beforeTs
klines, err := exchange.Fetcher.FetchHistoryKlines(context.Background(), task.inst.ExchangeInstId, interval, afterTs, beforeTs)
if err != nil {
zlog.Errorf("fetch history kline task error: task -> %s, err -> %v", task.logKey(), err)
// retry task
task.times++
taskCh <- task
return
}
if len(klines) == 0 {
continue
}
sts, ets := klines[0].Ts, klines[len(klines)-1].Ts
ss := time.UnixMilli(sts).In(loc).Format(times.FORMAT_DATE)
ee := time.UnixMilli(ets).In(loc).Format(times.FORMAT_DATE)
zlog.Infof("fetch interval %s %d~%d klines: ret=%d~%d, %d klines, %s~%s", interval, afterTs, beforeTs, sts, ets, len(klines), ss, ee)
// store to tsdb
err = svc.exchangeDataService.SaveKlines(task.inst, klines)
if err != nil {
zlog.Errorf("save history klines to tsdb error: task -> %s, err -> %v", task.logKey(), err)
// retry task
task.times++
taskCh <- task
return
}
}
}()
}
wg.Wait()
zlog.Infof("%d insts initial finished", len(insts))
// var intervals []types.Interval
// for interval := range types.SupportedIntervals {
// intervals = append(intervals, interval)
// }
// var intervalsTs = make([]int, len(intervals))
// var index int
// var lock sync.Mutex
// var getTask = func() (interval types.Interval, afterTs, beforeTs int64) {
// lock.Lock()
// ts := intervalsTs[index]
// if ts == -1 {
// index++
// }
// lock.Unlock()
// return
// }
// var finishTask = func(interval types.Interval) {
// }
// ctx := context.Background()
// inst := insts[0]
// // for interval, intervalAdder := range types.SupportedIntervals {
// interval := types.Interval1h
// intervalAdder := types.SupportedIntervals[interval]
// // kvrocks get exchange+inst+interval last/ts
// tsKey := fmt.Sprintf(HistoryKlineTsKey, inst.Exchange, inst.InstId, interval)
// beforeTs, e := svc.kvdb.GetI64(ctx, tsKey)
// if e != nil {
// err = e
// return
// }
// if beforeTs == 0 {
// beforeTs = intervalAdder(KlineBefore0, -1)
// }
// for {
// afterTs := intervalAdder(beforeTs, 101)
// klines, e := exchange.Fetcher.FetchHistoryKlines(ctx, inst.ExchangeInstId, interval, afterTs, beforeTs)
// if e != nil {
// err = e
// return
// }
// if len(klines) == 0 {
// break
// }
// zlog.Infof("fetch interval %s %d~%d klines: ret=%d~%d, %d klines", interval, afterTs, beforeTs, klines[0].Ts, klines[len(klines)-1].Ts, len(klines))
// // store to tsdb
// err = svc.exchangeDataService.SaveKlines(inst, klines)
// if err != nil {
// return
// }
// // set kvdb inst ts mark
// beforeTs = klines[0].Ts
// }
// // }
// zlog.Infof("%s %s initial finished", inst.Exchange, inst.InstId)
}

43
internal/exchange/okx/channel_kline.go

@ -14,19 +14,20 @@ import (
type CandleData [][]string
var (
// k线类型
// 如 [1s/1m/3m/5m/15m/30m/1H/2H/4H]
// 香港时间开盘价k线:[6H/12H/1D/2D/3D/1W/1M/3M]
// UTC时间开盘价k线:[6Hutc/12Hutc/1Dutc/2Dutc/3Dutc/1Wutc/1Mutc/3Mutc]
// "candle1s" candle3Mutc candle1Mutc candle1Wutc candle1Dutc candle2Dutc candle3Dutc candle5Dutc candle12Hutc candle6Hutc
// candles = []string{"candle3M", "candle1M", "candle1W", "candle1D", "candle2D", "candle3D", "candle5D", "candle12H", "candle6H", "candle4H", "candle2H", "candle1H", "candle30m", "candle15m", "candle5m", "candle3m", "candle1m"}
subCandles = []string{
// "candle3M", "candle1M",
"candle1W", "candle1D", "candle2D", "candle3D", "candle5D",
"candle12H", "candle6H", "candle4H", "candle2H", "candle1H",
"candle30m", "candle15m", "candle5m", "candle3m", "candle1m",
"candle1s",
}
// k线类型
// 如 [1s/1m/3m/5m/15m/30m/1H/2H/4H]
// 香港时间开盘价k线:[6H/12H/1D/2D/3D/1W/1M/3M]
// UTC时间开盘价k线:[6Hutc/12Hutc/1Dutc/2Dutc/3Dutc/1Wutc/1Mutc/3Mutc]
// "candle1s" candle3Mutc candle1Mutc candle1Wutc candle1Dutc candle2Dutc candle3Dutc candle5Dutc candle12Hutc candle6Hutc
// candles = []string{"candle3M", "candle1M", "candle1W", "candle1D", "candle2D", "candle3D", "candle5D", "candle12H", "candle6H", "candle4H", "candle2H", "candle1H", "candle30m", "candle15m", "candle5m", "candle3m", "candle1m"}
//
// subCandles = []string{
// "candle3M", "candle1M",
// "candle1W", "candle1D", "candle2D", "candle3D", "candle5D",
// "candle12H", "candle6H", "candle4H", "candle2H", "candle1H",
// "candle30m", "candle15m", "candle5m", "candle3m", "candle1m",
// "candle1s",
// }
)
// ChannelCandle k线订阅频道
@ -39,11 +40,15 @@ func NewChannelCandle(
traceId string,
httpProxy string,
) *ChannelCandle {
var candles []string
for _, interval := range subscribeCandles {
candles = append(candles, "candle"+interval)
}
cfg := wsChannelConfig[*CandleData, *types.ChannelKline]{
channelId: traceId,
httpProxy: httpProxy,
wsUrl: "/ws/v5/business",
subscribeChannels: subCandles,
subscribeChannels: candles,
dataInstanceFunc: func() *CandleData {
var d CandleData
return &d
@ -81,7 +86,7 @@ func (c *ChannelCandle) GetSubscribes() (instIds []string) {
// func candleData2Klines() (klines []*types.Kline) {
func candleData2Klines(channelData *ChannelData[*CandleData]) (r *types.ChannelKline, err error) {
r = &types.ChannelKline{
InstId: channelData.InstId,
ExgInstId: channelData.InstId,
Exchange: types.ExchangeOKX,
}
for _, data := range *channelData.Data {
@ -134,10 +139,10 @@ func candleData2Klines(channelData *ChannelData[*CandleData]) (r *types.ChannelK
kline.Interval = types.Interval5d
case "candle1W":
kline.Interval = types.Interval1w
// case "candle1M":
// kline.Interval = types.Interval1mo
// case "candle3M":
// kline.Interval = types.Interval3mo
case "candle1M":
kline.Interval = types.Interval1mo
case "candle3M":
kline.Interval = types.Interval3mo
}
var kinds = []*decimal.Decimal{&kline.Open, &kline.High, &kline.Low, &kline.Close, &kline.Vol, nil, &kline.VolQuote}
for i := 1; i <= 7; i++ {

45
internal/exchange/okx/okx.go

@ -1,45 +0,0 @@
package okx
import (
"sig-pub/pkg/config"
"sig-pub/pkg/types"
)
// kline
type OkxExchange struct {
conf config.OkxExchange
channelCandle *ChannelCandle // K线频道
}
func NewOkxExchange(conf config.OkxExchange) *OkxExchange {
return &OkxExchange{
conf: conf,
}
}
func (okx *OkxExchange) Init() (err error) {
// TODO 多个 ChannelCandle 实例 OkxAggregate
okx.channelCandle = NewChannelCandle("candle-0", okx.conf.HttpProxy)
if err = okx.channelCandle.Init(); err != nil {
return
}
return
}
func (okx *OkxExchange) ExhcangeType() types.Exchange {
return types.ExchangeOKX
}
func (okx *OkxExchange) ConsumerKline() <-chan *types.ChannelKline {
return okx.channelCandle.Consumer()
}
// 订阅产品k线行情
func (okx *OkxExchange) SubscribeKline(instIds ...string) (err error) {
return okx.channelCandle.Subscribe(instIds...)
}
// 取消订阅产品k线行情
func (okx *OkxExchange) UnsubscribeKline(instIds ...string) (err error) {
return okx.channelCandle.Unsubscribe(instIds...)
}

126
internal/exchange/okx/okx_fetch.go

@ -0,0 +1,126 @@
package okx
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sig-pub/pkg/types"
"strconv"
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/go-resty/resty/v2"
"github.com/govalues/decimal"
)
const (
HttpBaseUrl = "https://www.okx.com"
)
type OkxFetcher struct {
client *resty.Client
httpProxy string
}
func NewOkxFetcher(httpProxy string) (f *OkxFetcher) {
client := resty.New()
client.SetTimeout(30 * time.Second)
client.SetTransport(&http.Transport{
MaxIdleConns: 100,
MaxConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
})
if httpProxy != "" {
client.SetProxy(httpProxy)
}
f = &OkxFetcher{
client: client,
httpProxy: httpProxy,
}
return
}
func (okx *OkxFetcher) ExhcangeType() types.Exchange {
return types.ExchangeOKX
}
// FetchHistoryKlines 获取交易产品历史K线数据
// https://my.okx.com/docs-v5/zh/#order-book-trading-market-data-get-candlesticks-history
// 周期区间 after > before, (after, before)
func (f *OkxFetcher) FetchHistoryKlines(ctx context.Context, okxInstId string, interval types.Interval, after, before int64) (klines []*types.Kline, err error) {
if okxInstId == "" {
err = errors.New("instid is empty")
return
}
if after <= 0 && before <= 0 {
err = errors.New("time range zero")
return
}
if err = fetchHistoryKlineLimiter.Wait(ctx); err != nil {
return
}
var params []string
params = append(params, fmt.Sprintf("instId=%s", url.QueryEscape(okxInstId)))
params = append(params, "limit=100") // 最大为100
if v, ok := subscribeCandles[interval]; ok {
params = append(params, "bar="+v)
}
if after > 0 {
params = append(params, fmt.Sprintf("after=%d", after))
}
if before > 0 {
params = append(params, fmt.Sprintf("before=%d", before))
}
url := fmt.Sprintf("%s/api/v5/market/history-candles?%s", HttpBaseUrl, strings.Join(params, "&"))
resp, err := f.client.R().Get(url)
if err != nil {
return
}
// http status: 429 Too Many Requests
status := resp.StatusCode()
if status != 200 {
err = fmt.Errorf("request history klines status error: %s, %s", url, resp.Status())
return
}
r := new(RespHistoryKline)
if err = sonic.Unmarshal(resp.Body(), r); err != nil {
return
}
if r.Code != "0" {
err = fmt.Errorf("code: %s, msg: %s", r.Code, r.Msg)
return
}
for _, data := range r.Data {
ts, e := strconv.ParseInt(data[0], 10, 64)
if e != nil {
err = e
return
}
kline := &types.Kline{
Interval: interval,
Ts: ts,
Confirm: data[8] == "1",
}
var kinds = []*decimal.Decimal{&kline.Open, &kline.High, &kline.Low, &kline.Close, &kline.Vol, nil, &kline.VolQuote}
for i := 1; i <= 7; i++ {
if kinds[i-1] == nil {
continue
}
*kinds[i-1], err = decimal.Parse(data[i])
if err != nil {
return
}
}
klines = append(klines, kline)
}
return
}

46
internal/exchange/okx/okx_fetch_test.go

@ -0,0 +1,46 @@
package okx
import (
"context"
"fmt"
"sig-pub/pkg/types"
"testing"
"time"
)
var klineBefore0 int64 = 1672502400000 // k线开始数据 2023-01-01 00:00:00 GMT+8
func TestFetchHistoryKlines(t *testing.T) {
okxFetcher := NewOkxFetcher("http://192.168.1.5:7890")
// get inst+interval before, if=0 -> global before
// for interval, adder := range types.SupportedIntervals {
// _, _ = interval, adder
// }
interval := types.Interval5m
intervalAdder := types.SupportedIntervals[interval]
before := intervalAdder(klineBefore0, -1)
for range 10 {
after := intervalAdder(before, 10)
klines, err := okxFetcher.FetchHistoryKlines(context.Background(), "BTC-USDT", interval, after, before)
if err != nil {
t.Error(err)
return
}
lastTs := intervalAdder(after, -1) // todo ts(last kline)-1
before = lastTs
for _, kline := range klines {
fmt.Println(kline)
}
fmt.Println("-----------------------------------------------------")
}
}
func TestA(t *testing.T) {
begin := time.UnixMilli(klineBefore0)
before := begin.AddDate(0, -1, 0)
after := begin.AddDate(0, 3, 0)
fmt.Println("before:", before.UnixMilli())
fmt.Println("after:", after.UnixMilli())
}

12
internal/exchange/okx/okx_limiter.go

@ -0,0 +1,12 @@
package okx
import (
"time"
"golang.org/x/time/rate"
)
var (
// https://my.okx.com/docs-v5/zh/#order-book-trading-market-data-get-candlesticks-history
fetchHistoryKlineLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 2)
)

70
internal/exchange/okx/okx_subscriber.go

@ -0,0 +1,70 @@
package okx
import (
"sig-pub/pkg/config"
"sig-pub/pkg/types"
)
var subscribeCandles = map[types.Interval]string{
// "3M", "1M", "1W", "1D", "2D", "3D", "5D",
// "12H", "6H", "4H", "2H", "1H",
// "30m", "15m", "5m", "3m", "1m",
// "1s",
types.Interval1s: "1s",
types.Interval1m: "1m",
types.Interval3m: "3m",
types.Interval5m: "5m",
types.Interval15m: "15m",
types.Interval30m: "30m",
types.Interval1h: "1H",
types.Interval2h: "2H",
types.Interval4h: "4H",
types.Interval6h: "6H",
types.Interval12h: "12H",
types.Interval1d: "1D",
types.Interval2d: "2D",
types.Interval3d: "3D",
types.Interval5d: "5D",
types.Interval1w: "1W",
types.Interval1mo: "1M",
types.Interval3mo: "3M",
}
// kline
type OkxSubscriber struct {
conf config.OkxExchange
channelCandle *ChannelCandle // K线频道
}
func NewOkxSubscriber(conf config.OkxExchange) *OkxSubscriber {
return &OkxSubscriber{
conf: conf,
}
}
func (okx *OkxSubscriber) Init() (err error) {
// TODO 多个 ChannelCandle 实例 OkxAggregate
okx.channelCandle = NewChannelCandle("candle-0", okx.conf.HttpProxy)
if err = okx.channelCandle.Init(); err != nil {
return
}
return
}
func (okx *OkxSubscriber) ExhcangeType() types.Exchange {
return types.ExchangeOKX
}
func (okx *OkxSubscriber) ConsumerKline() <-chan *types.ChannelKline {
return okx.channelCandle.Consumer()
}
// 订阅产品k线行情
func (okx *OkxSubscriber) SubscribeKline(instIds ...string) (err error) {
return okx.channelCandle.Subscribe(instIds...)
}
// 取消订阅产品k线行情
func (okx *OkxSubscriber) UnsubscribeKline(instIds ...string) (err error) {
return okx.channelCandle.Unsubscribe(instIds...)
}

7
internal/exchange/okx/types.go

@ -65,3 +65,10 @@ type MarketData struct {
Volume float64
Timestamp time.Time
}
// RespHistoryKline 历史k线响应
type RespHistoryKline struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data [][]string `json:"data"`
}

10
pkg/aside/trade_instance_client.go

@ -5,6 +5,7 @@ import (
"sig-pub/api/pb"
"sig-pub/pkg/data/entity"
"sig-pub/pkg/mapping"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/kvcache"
"time"
@ -61,9 +62,14 @@ func (c *TradeInstanceAside) getTradeInstance0(ctx context.Context, instId strin
}
// ListExchangeTradeInstance 获取交易所支持的交易实例
func (c *TradeInstanceAside) ListExchangeTradeInstance(ctx context.Context, exchange pb.Exchange) (exInsts []*entity.TradeInstanceExchange, err error) {
func (c *TradeInstanceAside) ListExchangeTradeInstance(ctx context.Context, exchange types.Exchange) (exInsts []*entity.TradeInstanceExchange, err error) {
pbExType, err := exchange.Exchange2PB()
if err != nil {
return
}
rsp, err := c.client.ListExchangeTradeInstance(ctx, &pb.ReqListExchangeTradeInstance{
Exchanges: []pb.Exchange{exchange},
Exchanges: []pb.Exchange{pbExType},
})
if err != nil {
return

53
pkg/storage/kvrocks/kvrocks.go

@ -0,0 +1,53 @@
package kvrocks
import (
"context"
"fmt"
"strconv"
"time"
"github.com/redis/go-redis/v9"
)
type KVRocksDB struct {
client *redis.Client
}
func NewKVRocksDB(conf redis.Options) *KVRocksDB {
client := redis.NewClient(&conf)
return &KVRocksDB{
client: client,
}
}
func (db *KVRocksDB) Ping() (err error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err = db.client.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("can't connect kvrocks, %v", err)
}
return
}
func (db *KVRocksDB) DB() *redis.Client {
return db.client
}
func (db *KVRocksDB) GetI64(ctx context.Context, key string) (v int64, err error) {
v, err = db.DB().Get(ctx, key).Int64()
if err != nil {
if err == redis.Nil {
return 0, nil
}
return 0, err
}
return v, nil
}
// todo set async
func (db *KVRocksDB) SetI64(ctx context.Context, key string, v int64) (err error) {
vs := strconv.FormatInt(v, 10)
err = db.DB().Set(ctx, key, vs, 0).Err()
return
}

12
pkg/storage/tsdb/victoria_metrics/metric.go

@ -55,12 +55,12 @@ func Kline2Metrics(inst types.TradeInstance, klines []*types.Kline) (metrics []*
ms, ok := instMetrics[inst.InstId]
if !ok || (rawValueLimit > 0 && len(ms[0].Values) >= rawValueLimit) {
ms = [6]*Metric{
NewMetric(inst.InstId, "interval", string(kline.Interval), "kind", "open"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "kind", "high"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "kind", "low"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "kind", "close"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "kind", "vol"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "kind", "volQuote"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", string(inst.Exchange), "kind", "open"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", string(inst.Exchange), "kind", "high"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", string(inst.Exchange), "kind", "low"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", string(inst.Exchange), "kind", "close"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", string(inst.Exchange), "kind", "vol"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", string(inst.Exchange), "kind", "volQuote"),
}
instMetrics[inst.InstId] = ms
for i := range len(ms) {

68
pkg/storage/tsdb/victoria_metrics/vm.go

@ -8,10 +8,13 @@ import (
"io"
"net/http"
"net/url"
"runtime/debug"
"sig-pub/pkg/config"
"sig-pub/pkg/types"
"sig-pub/pkg/zlog"
"github.com/bytedance/sonic"
"github.com/govalues/decimal"
"github.com/klauspost/compress/zstd"
)
@ -81,6 +84,13 @@ func compressData(data []byte) ([]byte, error) {
// 获取原始k线列表
func (vm *VictoriaMetricsTSDB) GetRangeKline(inst types.TradeInstance, interval types.Interval, start, end int64) (err error) {
defer func() {
if r := recover(); r != nil {
zlog.Error("vmdb get range kline recover error:", r)
debug.PrintStack()
err = fmt.Errorf("%v", r)
}
}()
if inst.InstId == "" {
err = errors.New("instid is empty")
return
@ -104,17 +114,65 @@ func (vm *VictoriaMetricsTSDB) GetRangeKline(inst types.TradeInstance, interval
// read response json line
reader := bufio.NewReader(resp.Body)
var klines []*types.Kline
var vmLineBytes []byte
for {
line, err2 := reader.ReadBytes('\n')
if err2 == io.EOF {
vmLineBytes, err = reader.ReadBytes('\n')
if err == io.EOF || len(vmLineBytes) == 0 {
break
}
if err2 != nil {
if err != nil {
zlog.Error(err)
err = err2
return
}
zlog.Infof("response body line: %s", string(line))
vmMetric := new(VMMetricKline)
if err = sonic.Unmarshal(vmLineBytes, vmMetric); err != nil {
return
}
for i, ts := range vmMetric.Timestamps {
if len(klines) <= i {
klines = append(klines, &types.Kline{
Interval: types.Interval(vmMetric.Metric.Interval),
Ts: ts,
Confirm: true,
})
}
kline := klines[i]
if kline.Ts != ts {
err = errors.New("vm metric kline integrate error")
return
}
value := vmMetric.Values[i]
switch vmMetric.Metric.Kind {
case "open":
kline.Open = value
case "close":
kline.Close = value
case "high":
kline.High = value
case "low":
kline.Low = value
case "vol":
kline.Vol = value
case "volQuote":
kline.VolQuote = value
}
}
// zlog.Infof("response body line: %#v", vmMetric)
}
for _, kline := range klines {
zlog.Infof("kline: %#v", kline)
}
return
}
type VMMetricKline struct {
Metric struct {
Name string `json:"__name__"`
Interval string `json:"interval"`
Kind string `json:"kind"`
} `json:"metric"`
Values []decimal.Decimal `json:"values"`
Timestamps []int64 `json:"timestamps"`
}

2
pkg/storage/tsdb/victoria_metrics/vm_test.go

@ -13,8 +13,8 @@ func TestGetRangeKline(t *testing.T) {
vmdb := NewVictoriaMetricsTSDB(config.VictoriaMetricsConfig{Addr: test_addr})
inst := types.TradeInstance{
InstId: "BTC_USDT",
Exchange: types.ExchangeOKX,
}
vmdb.GetRangeKline(inst, types.Interval1m, 1753368047691, time.Now().UnixMilli())
}

13
pkg/types/exchange.go

@ -1,6 +1,9 @@
package types
import "sig-pub/api/pb"
import (
"fmt"
"sig-pub/api/pb"
)
type Exchange string
@ -10,14 +13,14 @@ var (
ExchangeBINANCE = Exchange(pb.Exchange_BINANCE.String()) // 币安
)
func (ex Exchange) Exchange2PB() (pb.Exchange, bool) {
func (ex Exchange) Exchange2PB() (pb.Exchange, error) {
switch ex {
case ExchangeOKX:
return pb.Exchange_OKX, true
return pb.Exchange_OKX, nil
case ExchangeBINANCE:
return pb.Exchange_BINANCE, true
return pb.Exchange_BINANCE, nil
default:
return pb.Exchange_SIG, false
return pb.Exchange_SIG, fmt.Errorf("unknown exchange: %v", ex)
}
}

10
pkg/types/instance.go

@ -2,8 +2,10 @@ package types
// TradeInstance 交易产品
type TradeInstance struct {
InstId string
TickSz int32
MinSz int32
// InstType string
InstId string // 交易产品系统id
PriceSz int32 // 价格精度
QuantitySz int32 // 交易量精度
Status int32 // 交易所交易产品状态
ExchangeInstId string // 交易所交易产品id
Exchange Exchange // 当前处理交易产品交易所
}

89
pkg/types/interval.go

@ -1,33 +1,44 @@
package types
import "time"
var LossEmoji = "🔥"
var ProfitEmoji = "💰"
type Interval string
func (i Interval) Minutes() (int64, bool) {
m, ok := SupportedIntervals[i]
if !ok || m <= 0 {
return m, false
// AddMul 对指定毫秒时间戳增加周期数
func (i Interval) AddMul(ts, mul int64) (int64, bool) {
c, ok := SupportedIntervals[i]
if !ok {
return ts, false
}
return m / 60, true
return c(ts, mul), true
}
func (i Interval) Seconds() (int64, bool) {
m, ok := SupportedIntervals[i]
if !ok || m <= 0 {
return m, false
}
return m, true
}
// func (i Interval) Minutes() (int64, bool) {
// c, ok := SupportedIntervals[i]
// if !ok || c <= 0 {
// return c, false
// }
// return c / 60, true
// }
func (i Interval) Milliseconds() (int64, bool) {
m, ok := SupportedIntervals[i]
if !ok || m <= 0 {
return m, false
}
return m * 1000, true
}
// func (i Interval) Seconds() (int64, bool) {
// m, ok := SupportedIntervals[i]
// if !ok || m <= 0 {
// return m, false
// }
// return m, true
// }
// func (i Interval) Milliseconds() (int64, bool) {
// m, ok := SupportedIntervals[i]
// if !ok || m <= 0 {
// return m, false
// }
// return m * 1000, true
// }
var (
Interval1s = Interval("1s")
@ -62,25 +73,27 @@ type IntervalWindow struct {
RightWindow *int `json:"rightWindow"`
}
type IntervalMap map[Interval]int64
type IntervalMap map[Interval]IntervalAdder
type IntervalAdder func(ts, mul int64) (ret int64)
var SupportedIntervals = IntervalMap{
Interval1s: 1,
Interval1m: 1 * 60,
Interval3m: 3 * 60,
Interval5m: 5 * 60,
Interval15m: 15 * 60,
Interval30m: 30 * 60,
Interval1h: 60 * 60,
Interval2h: 60 * 60 * 2,
Interval4h: 60 * 60 * 4,
Interval6h: 60 * 60 * 6,
Interval12h: 60 * 60 * 12,
Interval1d: 60 * 60 * 24,
Interval2d: 60 * 60 * 24 * 2,
Interval3d: 60 * 60 * 24 * 3,
Interval5d: 60 * 60 * 24 * 5,
Interval1w: 60 * 60 * 24 * 7,
// Interval1mo: 60 * 60 * 24 * 30,
// Interval3mo: 60 * 60 * 24 * 30 * 3,
// Interval1s: func(ts, mul int64) (ret int64) { return ts + (1000 * mul) },
Interval1m: func(ts, mul int64) (ret int64) { return ts + (1 * 60 * 1000 * mul) },
Interval3m: func(ts, mul int64) (ret int64) { return ts + (3 * 60 * 1000 * mul) },
Interval5m: func(ts, mul int64) (ret int64) { return ts + (5 * 60 * 1000 * mul) },
Interval15m: func(ts, mul int64) (ret int64) { return ts + (15 * 60 * 1000 * mul) },
Interval30m: func(ts, mul int64) (ret int64) { return ts + (30 * 60 * 1000 * mul) },
Interval1h: func(ts, mul int64) (ret int64) { return ts + (60 * 60 * 1000 * mul) },
Interval2h: func(ts, mul int64) (ret int64) { return ts + (2 * 60 * 60 * 1000 * mul) },
Interval4h: func(ts, mul int64) (ret int64) { return ts + (4 * 60 * 60 * 1000 * mul) },
Interval6h: func(ts, mul int64) (ret int64) { return ts + (4 * 60 * 60 * 1000 * mul) },
Interval12h: func(ts, mul int64) (ret int64) { return ts + (12 * 60 * 60 * 1000 * mul) },
Interval1d: func(ts, mul int64) (ret int64) { return ts + (24 * 60 * 60 * 1000 * mul) },
Interval2d: func(ts, mul int64) (ret int64) { return ts + (2 * 24 * 60 * 60 * 1000 * mul) },
Interval3d: func(ts, mul int64) (ret int64) { return ts + (3 * 24 * 60 * 60 * 1000 * mul) },
Interval5d: func(ts, mul int64) (ret int64) { return ts + (5 * 24 * 60 * 60 * 1000 * mul) },
Interval1w: func(ts, mul int64) (ret int64) { return ts + (7 * 24 * 60 * 60 * 1000 * mul) },
Interval1mo: func(ts, mul int64) (ret int64) { return time.UnixMilli(ts).AddDate(0, int(mul), 0).UnixMilli() },
Interval3mo: func(ts, mul int64) (ret int64) { return time.UnixMilli(ts).AddDate(0, int(3*mul), 0).UnixMilli() },
}

3
pkg/types/kline.go

@ -55,8 +55,7 @@ func (k *Kline) ToPBKline() (kline *pb.Kline) {
// ChannelKline k线订阅消息
type ChannelKline struct {
InstId string `json:"instId"` // 交易产品id,如 BTC-USDT-SWAP
ExchangeInstId string `json:"exchangeInstId"` // 交易所交易产品id
ExgInstId string `json:"instId"` // 交易所交易产品id,如 BTC_USDT_SWAP
Exchange Exchange `json:"exchange"` // 交易所
Klines []*Kline `json:"klines"`
}

34
pkg/utils/collect/sync_map.go

@ -0,0 +1,34 @@
package collect
import "sync"
// SyncMap sync.Map 泛型包装
type SyncMap[K comparable, V any] struct {
m *sync.Map
}
func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
return &SyncMap[K, V]{
m: new(sync.Map),
}
}
// Store 放置新值
func (m *SyncMap[K, V]) Store(k K, v V) {
m.m.Store(k, v)
}
func (m *SyncMap[K, V]) Load(k K) (v V, ok bool) {
value, ok := m.m.Load(k)
if !ok {
return
}
v = value.(V)
return
}
func (m *SyncMap[K, V]) Range(f func(k K, v V) bool) {
m.m.Range(func(key, value any) bool {
return f(key.(K), value.(V))
})
}

117
pkg/utils/promise/promise.go

@ -0,0 +1,117 @@
package promise
import (
"errors"
"sync"
"time"
)
var (
promiseTicker *time.Ticker
promiseTicks []*PromiseAll // todo 内存释放 capacity / len > ?
promiseTickerLock sync.Mutex
ErrorTimeout error = errors.New("promise timeout")
NoExpire time.Duration = 0
)
func init() {
promiseTicker = time.NewTicker(100 * time.Millisecond)
go func() {
for {
now := <-promiseTicker.C
promiseTickerLock.Lock()
index := 0
for _, tick := range promiseTicks {
if finish := tick.tick(now); !finish {
promiseTicks[index] = tick
index++
}
}
promiseTicks = promiseTicks[:index]
promiseTickerLock.Unlock()
}
}()
}
type PromiseAll struct {
stime time.Time
timeout time.Duration
subs int
keys map[string]bool
data map[string]any
finallyCall func(data map[string]any, err error)
sync.Mutex
}
func NewPromiseAll(timeout time.Duration) *PromiseAll {
return &PromiseAll{
timeout: timeout,
keys: make(map[string]bool),
data: make(map[string]any),
}
}
func (p *PromiseAll) tick(now time.Time) (finish bool) {
if p.stime.Add(p.timeout).After(now) {
p.finish(ErrorTimeout)
return true
}
return p.finallyCall != nil
}
func (p *PromiseAll) finish(err error) {
if p.finallyCall == nil {
return
}
// 执行结束回调函数
go p.finallyCall(p.data, err)
// promise 状态结束
p.finallyCall = nil
}
func (p *PromiseAll) Subscribe(keys ...string) *PromiseAll {
for _, key := range keys {
p.keys[key] = false
}
p.subs = len(p.keys)
return p
}
func (p *PromiseAll) Finally(func(data map[string]any, err error)) *PromiseAll {
if p.subs <= 0 {
p.finish(nil)
return p
}
if p.timeout != NoExpire {
promiseTickerLock.Lock()
promiseTicks = append(promiseTicks, p)
promiseTickerLock.Unlock()
p.stime = time.Now()
}
return p
}
func (p *PromiseAll) Update(k string, v any, err error) *PromiseAll {
// options: error stop, concurrent limit
p.Lock()
defer p.Unlock()
finish, ok := p.keys[k]
if !ok || finish {
return p
}
p.keys[k] = true
p.subs--
if err != nil {
p.finish(err)
return p
}
p.data[k] = v
if p.subs <= 0 {
p.finish(nil)
}
return p
}

12
pkg/utils/promise/promise_test.go

@ -0,0 +1,12 @@
package promise
import (
"testing"
)
func TestPromiseAll(t *testing.T) {
v1, v2, v3 := 1, 2, 3
_, _, _ = v1, v2, v3
// pub kline 1 data, i1 1 data, pub i2 1 data, pub i3 1 data
// resolve 1 [data, data, data, data]
}

9
pkg/utils/times/times.go

@ -0,0 +1,9 @@
package times
const FORMAT string = "2006-01-02 15:04:05"
const FORMAT2 string = "2006/01/02 15:04:05"
const FORMAT_DATE string = "2006-01-02"
const FORMAT_DATE2 string = "2006/01/02"
const FORMAT_MONTH string = "2006-01"
const FORMAT_TIME string = "15:04:05"
const FORMAT_TIME_Minute string = "15:04"
Loading…
Cancel
Save