Browse Source

exchange fetch klines

main
strange 11 months ago
parent
commit
ec58d0187d
  1. 16
      README.md
  2. 30
      api/exchange.proto
  3. 2
      api/indicator.proto
  4. 8
      api/market.proto
  5. 14
      api/pub.proto
  6. 21
      cmd/exchange/main.go
  7. 4
      cmd/indicator/main.go
  8. 6
      cmd/market/main.go
  9. 2
      config/exchange.toml
  10. 2
      go.mod
  11. 2
      go.sum
  12. 52
      internal/exchange/exchange.go
  13. 50
      internal/exchange/exchange_data_persist.go
  14. 34
      internal/exchange/exchange_data_service.go
  15. 451
      internal/exchange/exchange_grpc_server.go
  16. 478
      internal/exchange/exchange_service.go
  17. 3
      internal/exchange/okx/channel_kline.go
  18. 5
      internal/exchange/okx/okx_fetch.go
  19. 5
      internal/exchange/okx/okx_subscriber.go
  20. 17
      internal/indicator/indicator.go
  21. 2
      internal/market/market_grpc_server.go
  22. 2
      internal/market/trade_instance_service.go
  23. 8
      internal/market/validation.go
  24. 11
      internal/sig/sig_server.go
  25. 24
      pkg/aside/trade_instance_client.go
  26. 9
      pkg/data/common.go
  27. 44
      pkg/indicator/rsi.go
  28. 4
      pkg/mapping/market.go
  29. 12
      pkg/storage/tsdb/victoria_metrics/metric.go
  30. 2
      pkg/storage/tsdb/victoria_metrics/vm.go
  31. 3
      pkg/storage/tsdb/victoria_metrics/vm_test.go
  32. 36
      pkg/types/exchange.go
  33. 4
      pkg/types/indicator.go
  34. 14
      pkg/types/instance.go
  35. 118
      pkg/types/interval.go
  36. 8
      pkg/types/kline.go
  37. 33
      pkg/types/series/decimals.go
  38. 26
      pkg/types/series/floats.go
  39. 10
      pkg/types/series/series.go

16
README.md

@ -48,3 +48,19 @@ k线推送
价格/交易量精度->tsdb读写存储
kline时间窗口
tradingview advanced-charts:
- https://cn.tradingview.com/advanced-charts/
tradingview lightweight-charts:
- https://tradingview.github.io/lightweight-charts/
- https://github.com/tradingview/lightweight-charts
d3js:
- https://d3js.org/
DXcharts:
- https://devexperts.com/dxcharts/
- https://github.com/devexperts/dxcharts-lite
- https://financefeeds.com/zh-CN/dxcharts-%E5%9B%BE%E8%A1%A8%E7%9A%84%E5%B4%9B%E8%B5%B7%E6%88%90%E4%B8%BA%E4%BA%A4%E6%98%93%E5%B9%B3%E5%8F%B0%E9%87%91%E8%9E%8D%E7%A7%91%E6%8A%80%E7%BD%91%E7%AB%99%E7%9A%84%E7%AB%9E%E4%BA%89%E4%BC%98%E5%8A%BF/
golang-clash:
- https://hub.docker.com/r/dreamacro/clash

30
api/exchange.proto

@ -6,22 +6,42 @@ option go_package = "./pb";
// //,k线图//
service ExchangeService {
rpc SubscribeKline(stream ReqStreamSubscribeKline)
returns (stream RspStreamSubscribeKline);
// K线
rpc SubscribeKline(stream ReqStreamSubscribeKline) returns (stream RspStreamSubscribeKline);
//
rpc Exchanges(ReqExchanges) returns (RspExchanges);
//
rpc ExchangeInstanceState(ReqExchangeInstanceState) returns (RspExchangeInstanceState);
}
message ReqStreamSubscribeKline {
SubscribeType subType = 1;
repeated Exchange exchanges = 2; //
repeated ExchangeType exchanges = 2; //
repeated string instIds = 3;
repeated string intervals = 4;
bool onlyConfirm = 5; // confirm的k线
}
message RspStreamSubscribeKline { StreamKline kline = 1; }
message StreamKline {
repeated Kline klines = 2;
Exchange exchange = 3; //
ExchangeType exchange = 3; //
string instId = 4; // id
}
message ReqExchanges {
}
message RspExchanges {
repeated ExchangeType exchanges = 1;
}
message ReqExchangeInstanceState {
repeated string insts = 1;
bool allExchange = 2;
repeated ExchangeType exchanges = 3; //
}
message RspExchangeInstanceState {
repeated TradeInstanceState instsState = 1; //
}

2
api/indicator.proto

@ -16,7 +16,7 @@ message IndicatorSubReq {
}
message Indicator {
Exchange exhcange = 1;
ExchangeType exhcange = 1;
string instId = 2;
string indicator = 3;
string sub = 4; // , MA5, MA10, MA20

8
api/market.proto

@ -5,7 +5,7 @@ import "api/pub.proto";
option go_package = "./pb";
//
service Market {
service MarketService {
rpc GetTradeInstance(ReqGetTradeInstance) returns (RspGetTradeInstance);
rpc AddTradeInstance(ReqAddTradeInstance) returns (RspAddTradeInstance);
// rpc UpdateTradeInstance(ReqUpdateTradeInstance) returns (RspUpdateTradeInstance);
@ -46,18 +46,18 @@ message RspStatusTradeInstance {
message ReqListTradeInstance {
int32 page = 1;
int32 pageSize = 2;
Exchange exchange = 3; //
ExchangeType exchange = 3; //
string topic = 4;
string instId = 5;
}
message RspListTradeInstance {
repeated Kline klines = 1;
Exchange exchange = 2; //
ExchangeType exchange = 2; //
string instId = 3; // id
}
message ReqListExchangeTradeInstance {
repeated Exchange exchanges = 1;
repeated ExchangeType exchanges = 1;
}
message RspListExchangeTradeInstance {
repeated TradeInstanceExchange exchangeInsts = 1;

14
api/pub.proto

@ -8,7 +8,7 @@ enum SubscribeType {
UnsubscribeAll = 2;
}
enum Exchange {
enum ExchangeType {
SIG = 0;
OKX = 1;
BINANCE = 2;
@ -79,11 +79,19 @@ message TradeInstanceExchange {
string exchangeInstId = 1;
string instId = 2;
int32 status = 3;
int32 exchange = 4;
ExchangeType exchange = 4;
string updateBy = 5;
int64 updateTime = 6;
}
//
message TradeInstanceState {
ExchangeType exchange = 1;
string instId = 2;
int32 status = 3;
string last = 4; //
}
message Kline {
int64 Ts = 2;
string interval = 3; //
@ -98,7 +106,7 @@ message Kline {
// https://maicoin.github.io/max-websocket-docs/#/private_channels?id=snapshot
message Order {
string exchange = 1;
ExchangeType exchange = 1;
string symbol = 2;
string id = 3;
Side side = 4;

21
cmd/exchange/main.go

@ -42,7 +42,7 @@ func main() {
resolver := dis.Resolver()
// new market grpc client
marketUrl := discovery.ConsulDialUrl(pb.Market_ServiceDesc.ServiceName)
marketUrl := discovery.ConsulDialUrl(pb.MarketService_ServiceDesc.ServiceName)
marketConn, err := grpc.NewClient(marketUrl,
grpc.WithResolvers(resolver),
grpc.WithTransportCredentials(insecure.NewCredentials()),
@ -50,7 +50,7 @@ func main() {
if err != nil {
panic(err)
}
marketClient := pb.NewMarketClient(marketConn)
marketClient := pb.NewMarketServiceClient(marketConn)
tradeInstanceAside := aside.NewTradeInstanceAside(marketClient)
// kvrocks db
@ -69,17 +69,20 @@ func main() {
// tsdb VictoriaMetrics
vmtsdb := vmts.NewVictoriaMetricsTSDB(conf.Tsdb.Victoriametrics)
tsdbService := exchange.NewExchangeDataService(vmtsdb, nil)
tsdbService := exchange.NewExchangeDataService(vmtsdb, kvdb)
if err := tsdbService.Init(); err != nil {
panic(err)
}
// exhcange main service
exchangeService := exchange.NewExchangeGrpcServer(tradeInstanceAside, tsdbService, kvdb, okxExchange)
// exchange main service
exchangeService := exchange.NewExchangeService(tradeInstanceAside, tsdbService, okxExchange)
if err := exchangeService.Init(); err != nil {
panic(err)
}
exchangeGrpcServer := exchange.NewExchangeGrpcServer(exchangeService)
if err := exchangeGrpcServer.Init(); err != nil {
panic(err)
}
grpcServer := grpc.NewServer(config.GetGrpcOptions(
conf.Grpc,
grpc.UnaryInterceptor(interceptor.RecoverInterceptor))...,
@ -88,15 +91,13 @@ func main() {
// 注册反射服务
reflection.Register(grpcServer)
}
pb.RegisterExchangeServiceServer(grpcServer, exchangeService)
pb.RegisterExchangeServiceServer(grpcServer, exchangeGrpcServer)
exit.AddHook(grpcServer.GracefulStop, exit.WithOrderFront())
// consul 服务注册
register := exchangeConf.Register
if register.Name == "" {
register.Name = pb.ExchangeService_ServiceDesc.ServiceName
}
register.Name = pb.ExchangeService_ServiceDesc.ServiceName
if err := dis.Registry(grpcServer, register); err != nil {
panic(err)
}

4
cmd/indicator/main.go

@ -81,7 +81,7 @@ func subscribeEvents(client pb.ExchangeServiceClient) {
instIds := []string{"BTC-USDT", "DOGE-USDT-SWAP"}
msg := &pb.ReqStreamSubscribeKline{
SubType: pb.SubscribeType_Subscribe,
Exchanges: []pb.Exchange{pb.Exchange_OKX},
Exchanges: []pb.ExchangeType{pb.ExchangeType_OKX},
InstIds: instIds,
Intervals: []string{
string(types.Interval1s),
@ -99,7 +99,7 @@ func subscribeEvents(client pb.ExchangeServiceClient) {
<-time.After(10 * time.Second)
msg := &pb.ReqStreamSubscribeKline{
SubType: pb.SubscribeType_Unsubscribe,
Exchanges: []pb.Exchange{pb.Exchange_OKX},
Exchanges: []pb.ExchangeType{pb.ExchangeType_OKX},
InstIds: []string{"BTC-USDT"},
Intervals: []string{
string(types.Interval1s),

6
cmd/market/main.go

@ -59,15 +59,13 @@ func main() {
// 注册反射服务
reflection.Register(grpcServer)
}
pb.RegisterMarketServer(grpcServer, marketGrpcServer)
pb.RegisterMarketServiceServer(grpcServer, marketGrpcServer)
exit.AddHook(grpcServer.GracefulStop, exit.WithOrderFront())
// consul 服务注册
register := marketConf.Register
if register.Name == "" {
register.Name = pb.Market_ServiceDesc.ServiceName
}
register.Name = pb.MarketService_ServiceDesc.ServiceName
dis := discovery.NewConsulDiscovery(client)
if err := dis.Registry(grpcServer, register); err != nil {
panic(err)

2
config/exchange.toml

@ -1,5 +1,5 @@
grpcReflection = false # 注册grpc反射服务
grpcReflection = true # 注册grpc反射服务
[register]
nodeId = 1 # grpc服务节点id, 多实例唯一

2
go.mod

@ -87,7 +87,7 @@ require (
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

2
go.sum

@ -268,6 +268,8 @@ github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=

52
internal/exchange/exchange.go

@ -3,15 +3,18 @@ package exchange
import (
"context"
"fmt"
"sig-pub/api/pb"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/collect"
"sync"
"sync/atomic"
"github.com/govalues/decimal"
)
// 交易所行情数据订阅
type ExchangeSubscriber interface {
// 交易所类型
ExhcangeType() types.Exchange
ExhcangeType() pb.ExchangeType
// 消费k线行情数据
ConsumerKline() <-chan *types.ChannelKline
@ -26,42 +29,28 @@ type ExchangeSubscriber interface {
// 交易所行情数据请求
type ExchangeFetcher interface {
// 交易所类型
ExhcangeType() types.Exchange
ExhcangeType() pb.ExchangeType
// 获取区间内历史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
ExchangeType pb.ExchangeType
Fetcher ExchangeFetcher
Subscriber ExchangeSubscriber
TradeInstIds *collect.SyncMap[string, string] // map[string]*ExchangeTradeInstance // <TradeInstId, ExchangeInstId>
ExchangeInsts *collect.SyncMap[string, *ExchangeTradeInstance] // map[string]*ExchangeTradeInstance // <ExchangeInstId, Inst>
}
// 交易所交易产品
type ExchangeTradeInstance struct {
Inst *types.TradeInstance
Status int32 // 交易产品状态, 0.初始化中 1.正常
LiveMarkTs int64 // websocket订阅k线标记时间戳
LiveKStartTs map[types.Interval]int64 // ws开始订阅k线标记时间戳
LiveKMarkTs map[types.Interval]int64 // ws实时订阅k线(confirmed)标记时间戳
HistoryMarkTs map[types.Interval]int64 // 拉取历史k线标记时间戳
Lock sync.RWMutex
}
func (exInst *ExchangeTradeInstance) GetLiveMarkTs(interval types.Interval) (ts int64) {
exInst.Lock.RLock()
ts = exInst.LiveKMarkTs[interval]
exInst.Lock.RUnlock()
return
}
func (exInst *ExchangeTradeInstance) SetLiveMarkTs(interval types.Interval, ts int64) {
exInst.Lock.Lock()
exInst.LiveKMarkTs[interval] = ts
exInst.Lock.Unlock()
Status atomic.Int32 // 交易产品状态, 0.初始化中 1.正常
LiveKline *types.IntervalState[types.Kline] // 实时k线数据
LiveKStartTs *types.IntervalState[int64] // ws开始订阅k线标记时间戳
HistoryMarkTs *types.IntervalState[int64] // 拉取历史k线标记时间戳
Last decimal.Decimal // 交易产品实时价格tick更新
}
func NewExchange(fetcher ExchangeFetcher, subscriber ExchangeSubscriber) *Exchange {
@ -71,9 +60,10 @@ func NewExchange(fetcher ExchangeFetcher, subscriber ExchangeSubscriber) *Exchan
}
return &Exchange{
ExType: exType,
Fetcher: fetcher,
Subscriber: subscriber,
Insts: collect.NewSyncMap[string, *ExchangeTradeInstance](),
ExchangeType: exType,
Fetcher: fetcher,
Subscriber: subscriber,
TradeInstIds: collect.NewSyncMap[string, string](),
ExchangeInsts: collect.NewSyncMap[string, *ExchangeTradeInstance](),
}
}

50
internal/exchange/exchange_data_persist.go

@ -0,0 +1,50 @@
package exchange
import (
"context"
"fmt"
"sig-pub/api/pb"
"sig-pub/pkg/storage/kvrocks"
vmts "sig-pub/pkg/storage/tsdb/victoria_metrics"
"sig-pub/pkg/types"
)
type ExchangeDataPersist struct {
vmtsdb *vmts.VictoriaMetricsTSDB
kvdb *kvrocks.KVRocksDB
// exchangeFetch ExchangeFetcher
}
func NewExchangeDataService(
vmdb *vmts.VictoriaMetricsTSDB,
kvdb *kvrocks.KVRocksDB,
) *ExchangeDataPersist {
return &ExchangeDataPersist{
vmtsdb: vmdb,
kvdb: kvdb,
}
}
func (p *ExchangeDataPersist) Init() (err error) {
return
}
func (p *ExchangeDataPersist) SaveKlines(inst types.TradeInstance, klines []*types.Kline) (err error) {
err = p.vmtsdb.SaveKlines(inst, klines)
if err != nil {
return
}
return
}
func (p *ExchangeDataPersist) GetHistoryKlineMarkTs(exchange pb.ExchangeType, instId string, interval types.Interval) (ts int64, err error) {
tsKey := fmt.Sprintf(HistoryKlineTsKey, exchange, instId, interval)
ts, err = p.kvdb.GetI64(context.Background(), tsKey)
return
}
func (p *ExchangeDataPersist) SaveHistoryKlineMarkTs(exchange pb.ExchangeType, instId string, interval types.Interval, ts int64) (tsKey string, err error) {
tsKey = fmt.Sprintf(HistoryKlineTsKey, exchange, instId, interval)
err = p.kvdb.SetI64(context.Background(), tsKey, ts)
return
}

34
internal/exchange/exchange_data_service.go

@ -1,34 +0,0 @@
package exchange
import (
vmts "sig-pub/pkg/storage/tsdb/victoria_metrics"
"sig-pub/pkg/types"
)
type ExchangeDataService struct {
vmdb *vmts.VictoriaMetricsTSDB
exchangeFetch ExchangeFetcher
}
func NewExchangeDataService(
vmdb *vmts.VictoriaMetricsTSDB,
exchangeFetch ExchangeFetcher,
) *ExchangeDataService {
return &ExchangeDataService{
vmdb: vmdb,
exchangeFetch: exchangeFetch,
}
}
func (svc *ExchangeDataService) Init() (err error) {
return
}
func (svc *ExchangeDataService) SaveKlines(inst types.TradeInstance, klines []*types.Kline) (err error) {
err = svc.vmdb.SaveKlines(inst, klines)
if err != nil {
return
}
// todo klines 时间点回溯检查
return
}

451
internal/exchange/exchange_grpc_server.go

@ -4,203 +4,36 @@ import (
"context"
"fmt"
"io"
"runtime"
"sig-pub/api/pb"
"sig-pub/pkg/aside"
"sig-pub/pkg/data"
"sig-pub/pkg/storage/kvrocks"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/collect"
"sig-pub/pkg/utils/times"
"sig-pub/pkg/zlog"
"sync"
"sync/atomic"
"time"
"google.golang.org/grpc"
)
type ExchangeGrpcServer struct {
pb.UnimplementedExchangeServiceServer
exchangeMap map[types.Exchange]*Exchange
tradeInstanceAside *aside.TradeInstanceAside
exchangeDataService *ExchangeDataService
kvdb *kvrocks.KVRocksDB
exchangeService *ExchangeService
klineStreamId int64
klineStreamId int64
klinePublisher *Publisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]]
klineSubscriber *Publisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]]
}
// exchanges: 支持的数据源交易所
func NewExchangeGrpcServer(
tradeInstanceAside *aside.TradeInstanceAside,
exchangeDataService *ExchangeDataService,
kvdb *kvrocks.KVRocksDB,
exchanges ...*Exchange,
) *ExchangeGrpcServer {
exchangeMap := make(map[types.Exchange]*Exchange)
for _, exchange := range exchanges {
exchangeMap[exchange.ExType] = exchange
}
func NewExchangeGrpcServer(exchangeService *ExchangeService) *ExchangeGrpcServer {
return &ExchangeGrpcServer{
exchangeMap: exchangeMap,
tradeInstanceAside: tradeInstanceAside,
exchangeDataService: exchangeDataService,
kvdb: kvdb,
klinePublisher: NewPublisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]](16),
exchangeService: exchangeService,
}
}
func (svc *ExchangeGrpcServer) Init() (err error) {
svc.subscribeExchanges()
func (svr *ExchangeGrpcServer) Init() (err error) {
svr.klineSubscriber = svr.exchangeService.GetKlineSubscriber()
return
}
// 订阅交易所推送行情
func (svc *ExchangeGrpcServer) subscribeExchanges() {
// consumerKline
// 交易所订阅交易产品
for _, exchange := range svc.exchangeMap {
go func(exchange *Exchange) {
// get exchange trade instances
insts, err := svc.tradeInstanceAside.ListExchangeTradeInstance(context.Background(), exchange.ExType)
if err != nil {
zlog.Error(err)
return
}
var exchangeInstIds []string
var processingInsts []types.TradeInstance
for _, inst := range insts {
exchangeInstIds = append(exchangeInstIds, inst.ExchangeInstId)
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,
LiveKMarkTs: make(map[types.Interval]int64),
HistoryMarkTs: make(map[types.Interval]int64),
})
// 待初始化币种数据
if inst.Status == data.StatusProcessing {
processingInsts = append(processingInsts, *tradeInst)
}
}
// instIds := []string{"BTC-USDT", "DOGE-USDT-SWAP"}
err = exchange.Subscriber.SubscribeKline(exchangeInstIds...)
if err != nil {
zlog.Error(err)
return
}
go func() {
c := exchange.Subscriber.ConsumerKline()
svc.consumerKline(exchange, c)
// todo subscribe books 订单簿
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 {
return
}
// 交易所 instid 转 sig-instid
var exInst *types.TradeInstance
if inst, ok := exchange.Insts.Load(channelK.ExgInstId); ok && inst != nil {
exInst = inst.Inst
// 标记交易产品开始订阅k线时间
if len(channelK.Klines) > 0 {
kline := collect.MustMax(channelK.Klines, func(k *types.Kline) int64 { return k.Ts })
if inst.GetLiveMarkTs(kline.Interval) == 0 {
inst.SetLiveMarkTs(kline.Interval, kline.Ts)
}
}
// confirmKlines := collect.Filter(channelK.Klines, func(_ int, k *types.Kline) bool { return k.Confirm })
} else {
zlog.Errorf("unknown exchange instId: %v, %s", channelK.Exchange, channelK.ExgInstId)
continue
}
// publish to subscribers
pubMsgMap := make(map[string]*pb.StreamKline)
// instId := channelK.InstId
pbExType, err := channelK.Exchange.Exchange2PB()
if err != nil {
zlog.Error(err)
continue
}
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", exchangeType, exInst.InstId, kline.Interval, confirm)
// todo 优化没有订阅者就跳过
msg, ok := pubMsgMap[pubKey]
if !ok {
msg = new(pb.StreamKline)
msg.InstId = exInst.InstId
msg.Exchange = pbExType
pubMsgMap[pubKey] = msg
}
pbk := kline.ToPBKline()
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
}
subs := svc.klinePublisher.Publisher(pubKey)
for _, sub := range subs {
if err := sub.Send(&pb.RspStreamSubscribeKline{Kline: msg}); err != nil {
zlog.Error(err)
}
}
}
}
}
// SubscribeKline 订阅k线stream
func (svc *ExchangeGrpcServer) SubscribeKline(stream grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]) (err0 error) {
streamId := atomic.AddInt64(&svc.klineStreamId, 1)
func (svr *ExchangeGrpcServer) SubscribeKline(stream grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]) (err0 error) {
streamId := atomic.AddInt64(&svr.klineStreamId, 1)
// subKey = /kline/exchange/instId/interval/confirm
// 接收消息的goroutine
@ -228,17 +61,17 @@ func (svc *ExchangeGrpcServer) SubscribeKline(stream grpc.BidiStreamingServer[pb
select {
case <-stream.Context().Done():
// 客户端断开连接
svc.klinePublisher.UnsubscribeAll(streamId)
svr.klineSubscriber.UnsubscribeAll(streamId)
return stream.Context().Err()
case msg, ok := <-recvChan:
if !ok {
// 接收通道关闭,结束流
svc.klinePublisher.UnsubscribeAll(streamId)
svr.klineSubscriber.UnsubscribeAll(streamId)
return
}
if msg.SubType == pb.SubscribeType_UnsubscribeAll {
svc.klinePublisher.UnsubscribeAll(streamId)
svr.klineSubscriber.UnsubscribeAll(streamId)
continue
}
for _, exchange := range msg.Exchanges {
@ -253,9 +86,9 @@ func (svc *ExchangeGrpcServer) SubscribeKline(stream grpc.BidiStreamingServer[pb
zlog.Infof("stream: %d sub: %s", streamId, subKey)
switch msg.SubType {
case pb.SubscribeType_Subscribe:
svc.klinePublisher.Subscribe(subKey, streamId, stream)
svr.klineSubscriber.Subscribe(subKey, streamId, stream)
case pb.SubscribeType_Unsubscribe:
svc.klinePublisher.Unsubscribe(subKey, streamId)
svr.klineSubscriber.Unsubscribe(subKey, streamId)
}
}
}
@ -263,261 +96,21 @@ func (svc *ExchangeGrpcServer) SubscribeKline(stream grpc.BidiStreamingServer[pb
}
}
}
// for {
// select {
// case <-ctx.Done():
// // 客户端断开连接
// // log.Printf("Client disconnected from topic: %s", topic)
// return
// default:
// // 模拟事件生成
// event := &pb.SubscribeKlineStreamRsp{
// Kline: &pb.StreamKline{
// InstId: "DOGE/USDT",
// Exchange: pb.Exchange_OKX,
// Klines: []*pb.Kline{
// {
// Ts: time.Now().Unix(),
// },
// },
// },
// }
// // 推送事件
// if err := streamRsp.Send(event); err != nil {
// log.Printf("Failed to send event to client: %v", err)
// return err
// }
// // 模拟事件间隔
// time.Sleep(2 * time.Second)
// }
// }
}
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 fetchKlineTask struct {
inst types.TradeInstance
interval types.Interval
afterTs int64
beforeTs int64
times int32 // 重试次数
}
func (t fetchKlineTask) 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) {
// 记录成功和失败的交易产品
var success, failed []types.TradeInstance
for _, inst := range insts {
err := svc.initialTradeInstanceKlines(exchange, inst)
if err != nil {
zlog.Errorf("initial fetch trade instance error: %s(%s), err=%v", inst.InstId, inst.Exchange, err)
failed = append(failed, inst)
} else {
success = append(success, inst)
}
}
zlog.Infof("%d insts initial finished, success %d, failed %d", len(insts), len(success), len(failed))
}
var (
SingleTaskMaxFailTimes int32 = 10
)
// initTradeInstanceKlines 初始化交易产品历史k线数据
func (svc *ExchangeGrpcServer) initialTradeInstanceKlines(exchange *Exchange, inst types.TradeInstance) (err error) {
// 并发数
concurrent := max(8, runtime.NumCPU()*2)
// 任务 channel
taskCh := make(chan fetchKlineTask, concurrent)
retryTaskCh := make(chan fetchKlineTask, concurrent)
// 发布任务数, 成功任务数, 失败任务次数
var pubTasks, subTasks, failTasks atomic.Int32
var pubTaskDone atomic.Bool // 所有任务已发布
var historyMarkTs = make(map[types.Interval]int64) // 已初始化最后k线时间戳
var historyMarkTsMu sync.Mutex
ctx, cancel := context.WithCancel(context.Background())
defer func() {
if err != nil {
return
}
for interval, ts := range historyMarkTs {
tsKey := fmt.Sprintf(HistoryKlineTsKey, inst.Exchange, inst.InstId, interval)
if ex := svc.kvdb.SetI64(context.Background(), tsKey, ts); ex != nil {
zlog.Errorf("history mark inititaled ts error: key=%s, ts=%d", tsKey, ts)
}
}
}()
go func() {
defer func() {
pubTaskDone.Store(true)
// 无任务处理
if subTasks.Load() == 0 {
cancel()
}
zlog.Infof("trade instance initial kline %s(%s), pub %d fetch tasks", inst.InstId, inst.Exchange, pubTasks.Load())
}()
// 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, ex := svc.kvdb.GetI64(context.Background(), tsKey)
if ex != nil {
err = ex
zlog.Error(err)
cancel()
return
}
if beforeTs == 0 {
beforeTs = intervalAdder(KlineBefore0, -1)
}
for {
// 对比 ws 获取的实时k线
exchangeInst, ok := exchange.Insts.Load(inst.ExchangeInstId)
if !ok {
err = fmt.Errorf("not load exchange trade instance: %s", inst.ExchangeInstId)
cancel()
return
}
// 订阅完成
if exchangeInst.LiveMarkTs != 0 && beforeTs >= exchangeInst.LiveMarkTs {
// history status -> ok
break
}
if beforeTs > time.Now().UnixMilli() {
if exchangeInst.LiveMarkTs != 0 {
// history status -> ok
} else {
// live status -> not ok
}
break
}
afterTs := intervalAdder(beforeTs, 101)
task := fetchKlineTask{
inst: inst,
interval: interval,
afterTs: afterTs,
beforeTs: beforeTs,
times: 0,
}
// 发布任务
select {
case taskCh <- task:
pubTasks.Add(1)
case <-ctx.Done():
return
}
beforeTs = intervalAdder(afterTs, -1)
}
}()
// 任务消费器 8协程并行
wg := new(sync.WaitGroup)
for range concurrent {
wg.Add(1)
go func() {
defer wg.Done()
var task fetchKlineTask
for {
select {
case <-ctx.Done():
return
case task = <-taskCh:
case task = <-retryTaskCh:
}
if task.times > 0 {
zlog.Infof("retry fetch history kline task %d times: task -> %s", task.times, task.logKey())
}
if lastKlineTs, ex := svc.fetchTaskKlines(exchange, task); ex != nil {
failTasks.Add(1)
if task.times >= SingleTaskMaxFailTimes {
err = fmt.Errorf("task failed to many times %d, key: %s, err: %v", task.times, task.logKey(), ex)
cancel()
return
}
// retry task
task.times++
select {
case retryTaskCh <- task:
case <-ctx.Done():
return
}
} else {
// 周期任务最后kline时间
if lastKlineTs != 0 {
historyMarkTsMu.Lock()
if ts, ok := historyMarkTs[task.interval]; ok {
if lastKlineTs > ts {
historyMarkTs[task.interval] = lastKlineTs
}
} else {
historyMarkTs[task.interval] = lastKlineTs
}
historyMarkTsMu.Unlock()
}
zlog.Debugf("trade instance initial kline tasks processing: %s(%s), pub %d, sub %d, fail %d", inst.InstId, inst.Exchange, pubTasks.Load(), subTasks.Load(), failTasks.Load())
// 任务都已执行成功结束
subs := subTasks.Add(1)
if pubTaskDone.Load() && subs >= pubTasks.Load() {
zlog.Infof("trade instance initial kline tasks success finished, %s(%s), pub %d, sub %d, fail %d", inst.InstId, inst.Exchange, pubTasks.Load(), subTasks.Load(), failTasks.Load())
cancel()
return
}
}
}
}()
}
wg.Wait()
return
}
func (svc *ExchangeGrpcServer) fetchTaskKlines(exchange *Exchange, task fetchKlineTask) (lastKlineTs int64, err error) {
interval, afterTs, beforeTs := task.interval, task.afterTs, task.beforeTs
klines, err := exchange.Fetcher.FetchHistoryKlines(context.Background(), task.inst.ExchangeInstId, interval, afterTs, beforeTs)
// 获取支持的交易所列表
func (svr *ExchangeGrpcServer) Exchanges(ctx context.Context, req *pb.ReqExchanges) (rsp *pb.RspExchanges, err error) {
exchanges, err := svr.exchangeService.Exchanges()
if err != nil {
zlog.Errorf("fetch history kline task error: task -> %s, err -> %v", task.logKey(), err)
return
}
if len(klines) == 0 {
return
}
return &pb.RspExchanges{Exchanges: exchanges}, nil
}
loc, _ := time.LoadLocation("Asia/Shanghai")
sts, ets := klines[0].Ts, klines[len(klines)-1].Ts
lastKlineTs = max(sts, ets)
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, beforeTs, afterTs, ets, sts, len(klines), ee, ss)
// store to tsdb
err = svc.exchangeDataService.SaveKlines(task.inst, klines)
func (svr *ExchangeGrpcServer) ExchangeInstanceState(ctx context.Context, req *pb.ReqExchangeInstanceState) (rsp *pb.RspExchangeInstanceState, err error) {
states, err := svr.exchangeService.ExchangeInstanceState(req.AllExchange, req.Exchanges, req.Insts)
if err != nil {
zlog.Errorf("save history klines to tsdb error: task -> %s, err -> %v", task.logKey(), err)
return
}
return
return &pb.RspExchangeInstanceState{InstsState: states}, nil
}

478
internal/exchange/exchange_service.go

@ -0,0 +1,478 @@
package exchange
import (
"context"
"errors"
"fmt"
"runtime"
"sig-pub/api/pb"
"sig-pub/pkg/aside"
"sig-pub/pkg/data"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/times"
"sig-pub/pkg/zlog"
"sort"
"sync"
"sync/atomic"
"time"
"google.golang.org/grpc"
)
// ExchangeService 交易所服务
type ExchangeService struct {
exchangeMap map[pb.ExchangeType]*Exchange
tradeInstanceAside *aside.TradeInstanceAside
exchangeDataService *ExchangeDataPersist
klinePublisher *Publisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]]
}
// exchanges: 支持的数据源交易所
func NewExchangeService(
tradeInstanceAside *aside.TradeInstanceAside,
exchangeDataService *ExchangeDataPersist,
exchanges ...*Exchange,
) *ExchangeService {
exchangeMap := make(map[pb.ExchangeType]*Exchange)
for _, exchange := range exchanges {
exchangeMap[exchange.ExchangeType] = exchange
}
return &ExchangeService{
exchangeMap: exchangeMap,
tradeInstanceAside: tradeInstanceAside,
exchangeDataService: exchangeDataService,
klinePublisher: NewPublisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]](16),
}
}
func (svc *ExchangeService) Init() (err error) {
svc.subscribeExchanges()
return
}
// GetKlineSubscriber 订阅k线订阅器
func (svc *ExchangeService) GetKlineSubscriber() (subscriber *Publisher[int64, grpc.BidiStreamingServer[pb.ReqStreamSubscribeKline, pb.RspStreamSubscribeKline]]) {
subscriber = svc.klinePublisher
return
}
// 订阅交易所推送行情
func (svc *ExchangeService) subscribeExchanges() {
// consumerKline
// 交易所订阅交易产品
for _, exchange := range svc.exchangeMap {
go func(exchange *Exchange) {
// get exchange trade instances
insts, err := svc.tradeInstanceAside.ListExchangeTradeInstance(context.Background(), exchange.ExchangeType)
if err != nil {
zlog.Error(err)
return
}
var exchangeInstIds []string
var processingInsts []types.TradeInstance
for _, inst := range insts {
exchangeInstIds = append(exchangeInstIds, inst.ExchangeInstId)
tradeInst := &types.TradeInstance{
InstId: inst.InstId,
Status: inst.Status,
PriceSz: 0,
QuantitySz: 0,
ExchangeInstId: inst.ExchangeInstId,
Exchange: exchange.ExchangeType,
}
exchange.TradeInstIds.Store(inst.InstId, inst.ExchangeInstId)
exchange.ExchangeInsts.Store(inst.ExchangeInstId, &ExchangeTradeInstance{
Inst: tradeInst,
LiveKline: types.NewIntervalState[types.Kline](),
LiveKStartTs: types.NewIntervalState[int64](),
HistoryMarkTs: types.NewIntervalState[int64](),
})
// 待初始化币种数据
if inst.Status == int32(data.StatusProcessing) {
processingInsts = append(processingInsts, *tradeInst)
}
}
// instIds := []string{"BTC-USDT", "DOGE-USDT-SWAP"}
err = exchange.Subscriber.SubscribeKline(exchangeInstIds...)
if err != nil {
zlog.Error(err)
return
}
go func() {
c := exchange.Subscriber.ConsumerKline()
svc.consumerKline(exchange, c)
// todo subscribe books 订单簿
zlog.Infof("unsubscribe exchange: %s", exchange.ExchangeType)
}()
// 初始化k线数据
go svc.initialKlines(exchange, processingInsts)
}(exchange)
}
}
// consumerKline 消费交易所k线数据
func (svc *ExchangeService) consumerKline(exchange *Exchange, c <-chan *types.ChannelKline) {
exchangeType := exchange.ExchangeType
// publish to subscribers
pubStreamKlineMap := make(map[string]*pb.StreamKline)
for {
clear(pubStreamKlineMap)
channelK, ok := <-c
if !ok {
return
}
if len(channelK.Klines) == 0 {
continue
}
if len(channelK.Klines) > 1 {
sort.Slice(channelK.Klines, func(i, j int) bool {
return channelK.Klines[i].Ts < channelK.Klines[j].Ts
})
}
firstKline, lastKline := channelK.Klines[0], channelK.Klines[len(channelK.Klines)-1]
// 交易所 instid 转 sig-instid
var tradeInst *types.TradeInstance
exchangeInst, ok := exchange.ExchangeInsts.Load(channelK.ExgInstId)
if !ok || exchangeInst == nil || exchangeInst.Inst == nil {
zlog.Errorf("unknown exchange instId: %v, %s", channelK.Exchange, channelK.ExgInstId)
continue
}
tradeInst = exchangeInst.Inst
// 标记交易产品开始订阅k线时间
exchangeInst.LiveKStartTs.SetIf(firstKline.Interval, firstKline.Ts, func(old int64) bool { return old == 0 })
// 标记实时k线
exchangeInst.LiveKline.Set(lastKline.Interval, *lastKline)
// 记录实时价格
exchangeInst.Last = lastKline.Close
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", exchangeType, tradeInst.InstId, kline.Interval, confirm)
msg, ok := pubStreamKlineMap[pubKey]
if !ok {
msg = new(pb.StreamKline)
msg.InstId = tradeInst.InstId
msg.Exchange = channelK.Exchange
pubStreamKlineMap[pubKey] = msg
}
pbk := kline.ToPBKline()
msg.Klines = append(msg.Klines, pbk)
}
if len(confirmKlines) > 0 {
// tsdb storage todo 异步处理
err := svc.exchangeDataService.SaveKlines(*tradeInst, confirmKlines)
if err != nil {
zlog.Errorf("kline save to tsdb error: ", err)
}
// 初始化状态完成, 检查k线时间戳标记
if exchangeInst.Status.Load() == int32(data.StatusOk) {
lastConfirmKline := confirmKlines[len(confirmKlines)-1]
historyMark := exchangeInst.HistoryMarkTs.Get(lastConfirmKline.Interval)
_ = historyMark
}
}
// publish grpc stream klines
for pubKey, kline := range pubStreamKlineMap {
if len(kline.Klines) == 0 {
continue
}
subs := svc.klinePublisher.Publisher(pubKey)
for _, sub := range subs {
if err := sub.Send(&pb.RspStreamSubscribeKline{Kline: kline}); err != nil {
zlog.Error(err)
}
}
}
}
}
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
SingleKlineFetchTaskMaxFailTimes int32 = 100 // 单个k线拉取任务最大失败次数
)
type fetchKlineTask struct {
inst types.TradeInstance
interval types.Interval
afterTs int64
beforeTs int64
times int32 // 重试次数
}
func (t fetchKlineTask) 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 *ExchangeService) initialKlines(exchange *Exchange, insts []types.TradeInstance) {
// 记录成功和失败的交易产品
var success, failed []types.TradeInstance
for _, inst := range insts {
err := svc.initialTradeInstanceKlines(exchange, inst)
if err != nil {
zlog.Errorf("initial fetch trade instance error: %s(%s), err=%v", inst.InstId, inst.Exchange, err)
failed = append(failed, inst)
} else {
success = append(success, inst)
}
}
zlog.Infof("%d insts initial finished, success %d, failed %d", len(insts), len(success), len(failed))
}
// initTradeInstanceKlines 初始化交易产品历史k线数据
func (svc *ExchangeService) initialTradeInstanceKlines(exchange *Exchange, tradeInst types.TradeInstance) (err error) {
exchangeInst, ok := exchange.ExchangeInsts.Load(tradeInst.ExchangeInstId)
if !ok {
err = fmt.Errorf("not load exchange trade instance: %s", tradeInst.ExchangeInstId)
return
}
// 并发数
concurrent := max(8, runtime.NumCPU()*2)
// 任务 channel
taskCh := make(chan fetchKlineTask, concurrent)
retryTaskCh := make(chan fetchKlineTask, concurrent)
// 发布任务数, 成功任务数, 失败任务次数
var pubTasks, subTasks, failTasks atomic.Int32
var pubTaskDone atomic.Bool // 所有任务已发布
ctx, cancel := context.WithCancel(context.Background())
defer func() {
if err != nil {
// 交易所k线初始化失败
exchangeInst.Status.Store(int32(data.StatusFailed))
return
}
exchangeInst.HistoryMarkTs.Range(func(_ int, interval types.Interval, ts int64) {
tsKey, ex := svc.exchangeDataService.SaveHistoryKlineMarkTs(tradeInst.Exchange, tradeInst.InstId, interval, ts)
if ex != nil {
zlog.Errorf("history mark inititaled ts error: key=%s, ts=%d, %v", tsKey, ts, ex)
}
})
exchangeInst.Status.Store(int32(data.StatusOk))
}()
go func() {
defer func() {
pubTaskDone.Store(true)
// 无任务处理
if subTasks.Load() == 0 {
cancel()
}
zlog.Infof("trade instance initial kline %s(%s), pub %d fetch tasks", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load())
}()
for interval, intervalAdder := range types.SupportedIntervals {
// interval := types.Interval1d
// intervalAdder := types.SupportedIntervals[interval]
// history 未补全前, history写 kvdb ts mark, 补全后 ws live 写 ts mark
beforeTs, ex := svc.exchangeDataService.GetHistoryKlineMarkTs(tradeInst.Exchange, tradeInst.InstId, interval)
if ex != nil {
err = ex
zlog.Error(err)
cancel()
return
}
if beforeTs == 0 {
beforeTs = intervalAdder(KlineBefore0, -1)
}
exchangeInst.HistoryMarkTs.Set(interval, beforeTs)
for {
// 判定订阅任务发布完成
liveStartTs := exchangeInst.LiveKStartTs.Get(interval)
if liveStartTs != 0 && beforeTs >= liveStartTs {
break
}
if beforeTs > time.Now().UnixMilli() {
break
}
afterTs := intervalAdder(beforeTs, 101)
task := fetchKlineTask{
inst: tradeInst,
interval: interval,
afterTs: afterTs,
beforeTs: beforeTs,
times: 0,
}
// 发布任务
select {
case taskCh <- task:
pubTasks.Add(1)
case <-ctx.Done():
return
}
beforeTs = intervalAdder(afterTs, -1)
}
}
}()
// 任务消费器 多协程并行
wg := new(sync.WaitGroup)
for range concurrent {
wg.Add(1)
go func() {
defer wg.Done()
var task fetchKlineTask
for {
select {
case <-ctx.Done():
return
case task = <-taskCh:
case task = <-retryTaskCh:
}
if task.times > 0 {
zlog.Infof("retry fetch history kline task %d times: task -> %s", task.times, task.logKey())
}
if lastKlineTs, ex := svc.fetchTaskKlines(exchange, task); ex != nil {
failTasks.Add(1)
if task.times >= SingleKlineFetchTaskMaxFailTimes {
err = fmt.Errorf("task failed to many times %d, key: %s, err: %v", task.times, task.logKey(), ex)
cancel()
return
}
// retry task
task.times++
select {
case retryTaskCh <- task:
case <-ctx.Done():
return
}
} else {
// 周期任务最后kline时间
if lastKlineTs != 0 {
exchangeInst.HistoryMarkTs.SetIf(task.interval, lastKlineTs, func(old int64) bool {
return lastKlineTs > old
})
// historyMarkTsMu.Lock()
// historyMarkTs[task.interval] = max(historyMarkTs[task.interval], lastKlineTs)
// historyMarkTsMu.Unlock()
}
zlog.Debugf("trade instance initial kline tasks processing: %s(%s), pub %d, sub %d, fail %d", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load(), subTasks.Load(), failTasks.Load())
// 任务都已执行成功结束
subs := subTasks.Add(1)
if pubTaskDone.Load() && subs >= pubTasks.Load() {
zlog.Infof("trade instance initial kline tasks success finished, %s(%s), pub %d, sub %d, fail %d", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load(), subTasks.Load(), failTasks.Load())
cancel()
return
}
}
}
}()
}
wg.Wait()
return
}
func (svc *ExchangeService) fetchTaskKlines(exchange *Exchange, task fetchKlineTask) (lastKlineTs int64, err error) {
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)
return
}
if len(klines) == 0 {
return
}
loc, _ := time.LoadLocation("Asia/Shanghai")
sts, ets := klines[0].Ts, klines[len(klines)-1].Ts
lastKlineTs = max(sts, ets)
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, beforeTs, afterTs, ets, sts, len(klines), ee, ss)
// 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)
return
}
return
}
// Exchanges 支持的交易所列表
func (svc *ExchangeService) Exchanges() (exchanges []pb.ExchangeType, err error) {
for exchange := range svc.exchangeMap {
exchanges = append(exchanges, exchange)
}
return
}
// ExchangeInstanceState 交易所交易产品状态
func (svc *ExchangeService) ExchangeInstanceState(allExchange bool, exchangeTypes []pb.ExchangeType, instIds []string) (states []*pb.TradeInstanceState, err error) {
var exchanges []*Exchange
if allExchange {
for _, exg := range svc.exchangeMap {
exchanges = append(exchanges, exg)
}
} else {
for _, exchangeType := range exchangeTypes {
exg, ok := svc.exchangeMap[exchangeType]
if !ok {
err = fmt.Errorf("not support exchange: %v", exchangeType)
return
}
exchanges = append(exchanges, exg)
}
}
if len(exchanges) == 0 {
err = errors.New("no support exchanges")
return
}
for _, exchange := range exchanges {
for _, instId := range instIds {
// trade instId to exchangeInstId
exchangeInstId, ok := exchange.TradeInstIds.Load(instId)
if !ok {
continue
}
inst, ok := exchange.ExchangeInsts.Load(exchangeInstId)
if !ok {
continue
}
state := &pb.TradeInstanceState{
Exchange: exchange.ExchangeType,
InstId: instId,
Last: inst.Last.String(),
}
states = append(states, state)
}
}
return
}

3
internal/exchange/okx/channel_kline.go

@ -2,6 +2,7 @@ package okx
import (
"fmt"
"sig-pub/api/pb"
"sig-pub/pkg/types"
"strconv"
@ -87,7 +88,7 @@ func (c *ChannelCandle) GetSubscribes() (instIds []string) {
func candleData2Klines(channelData *ChannelData[*CandleData]) (r *types.ChannelKline, err error) {
r = &types.ChannelKline{
ExgInstId: channelData.InstId,
Exchange: types.ExchangeOKX,
Exchange: pb.ExchangeType_OKX,
}
for _, data := range *channelData.Data {
ts, e := strconv.ParseInt(data[0], 10, 64)

5
internal/exchange/okx/okx_fetch.go

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
"sig-pub/api/pb"
"sig-pub/pkg/types"
"strconv"
"strings"
@ -45,8 +46,8 @@ func NewOkxFetcher(httpProxy string) (f *OkxFetcher) {
return
}
func (okx *OkxFetcher) ExhcangeType() types.Exchange {
return types.ExchangeOKX
func (okx *OkxFetcher) ExhcangeType() pb.ExchangeType {
return pb.ExchangeType_OKX
}
// FetchHistoryKlines 获取交易产品历史K线数据

5
internal/exchange/okx/okx_subscriber.go

@ -1,6 +1,7 @@
package okx
import (
"sig-pub/api/pb"
"sig-pub/pkg/config"
"sig-pub/pkg/types"
)
@ -51,8 +52,8 @@ func (okx *OkxSubscriber) Init() (err error) {
return
}
func (okx *OkxSubscriber) ExhcangeType() types.Exchange {
return types.ExchangeOKX
func (okx *OkxSubscriber) ExhcangeType() pb.ExchangeType {
return pb.ExchangeType_OKX
}
func (okx *OkxSubscriber) ConsumerKline() <-chan *types.ChannelKline {

17
internal/indicator/indicator.go

@ -1,3 +1,20 @@
package indicator
// load indicator plugin
// 热指标 自动加载/实时更新/内存缓存
// 历史指标 实时计算
// 自定义插件化指标
type IndicatorService struct {
}
func NewIndicatorService() *IndicatorService {
return &IndicatorService{}
}
// 加载热指标
// 订阅k线数据 更新指标
func (svc *IndicatorService) Init() {
}

2
internal/market/market_grpc_server.go

@ -9,7 +9,7 @@ import (
)
type MarketGrpcServer struct {
pb.UnimplementedMarketServer
pb.UnimplementedMarketServiceServer
tradeInstanceService *TradeInstanceService
}

2
internal/market/trade_instance_service.go

@ -140,7 +140,7 @@ func (s *TradeInstanceService) UpdateInstanceStatus(inst *args.UpdateTradeInstan
func (s *TradeInstanceService) ListExchangeTradeInstance(exchanges []int32) (exchangesInsts []*entity.TradeInstanceExchange, err error) {
err = s.db.Select(&exchangesInsts, `
select * from t_trade_instance_exchange where exchange in ? and status in ? order by inst_id, exchange
`, exchanges, []int{data.StatusOk, data.StatusProcessing})
`, exchanges, []data.Status{data.StatusOk, data.StatusProcessing})
if err != nil {
return
}

8
internal/market/validation.go

@ -5,7 +5,6 @@ import (
"fmt"
"sig-pub/api/pb"
"sig-pub/pkg/data/entity"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/validator"
)
@ -17,12 +16,13 @@ func validateTradeInstance(inst *entity.TradeInstance) (err error) {
if validator.IsAnyBlank(inst.InstId, inst.InstPair, inst.InstCoin) {
return errors.New("InstId InstPair InstCoin can't be blank")
}
// todo validate others
// 交易所
for _, ex := range inst.Exchanges {
_, ok := types.ExchangePBParse(pb.Exchange(ex.Exchange))
if !ok {
_, ok := pb.ExchangeType_name[ex.Exchange]
if !ok || ex.Exchange == 0 {
return fmt.Errorf("exchange %d not valid", ex.Exchange)
}
}
// todo validate others
return
}

11
internal/sig/sig_server.go

@ -11,6 +11,7 @@ import (
"sig-pub/pkg/resp"
"sig-pub/pkg/utils/strs"
"sig-pub/pkg/zlog"
"strings"
"time"
"github.com/gin-gonic/gin"
@ -61,14 +62,16 @@ func (s *SigServer) Run(addr string) (err error) {
}
func (s *SigServer) handleGrpcGenericCall(c *gin.Context) {
svc := strs.UpperInitialLetter(c.Param("svr"))
svr := strs.UpperInitialLetter(c.Param("svr"))
method := strs.UpperInitialLetter(c.Param("method"))
if svc == "" || method == "" {
if svr == "" || method == "" {
c.JSON(http.StatusBadRequest, resp.Error("service not found"))
return
}
if !strings.HasSuffix(svr, "Service") {
svr += "Service"
}
// todo service white list
// get request body
@ -80,7 +83,7 @@ func (s *SigServer) handleGrpcGenericCall(c *gin.Context) {
ctx1, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
grpcGenericClient, err := s.grpcGenericClientFactory.GetClient(ctx1, svc)
grpcGenericClient, err := s.grpcGenericClientFactory.GetClient(ctx1, svr)
if err != nil {
zlog.Error(err)
c.JSON(http.StatusForbidden, resp.Error(err.Error()))

24
pkg/aside/trade_instance_client.go

@ -5,7 +5,6 @@ import (
"sig-pub/api/pb"
"sig-pub/pkg/data/entity"
"sig-pub/pkg/mapping"
"sig-pub/pkg/types"
"sig-pub/pkg/utils/kvcache"
"time"
@ -17,14 +16,14 @@ import (
// todo nats 更新监听 更新缓存
// grpc trade instance client
type TradeInstanceAside struct {
client pb.MarketClient
cache *kvcache.KVCache[*entity.TradeInstance]
cacheSf singleflight.Group
marketClient pb.MarketServiceClient
cache *kvcache.KVCache[*entity.TradeInstance]
cacheSf singleflight.Group
}
func NewTradeInstanceAside(client pb.MarketClient) *TradeInstanceAside {
func NewTradeInstanceAside(marketClient pb.MarketServiceClient) *TradeInstanceAside {
return &TradeInstanceAside{
client: client,
marketClient: marketClient,
cache: kvcache.NewExpireStore[*entity.TradeInstance](
time.Minute,
cache.WithShards(16),
@ -52,7 +51,7 @@ func (c *TradeInstanceAside) getTradeInstance0(ctx context.Context, instId strin
return
}
// grpc 获取
rsp, err := c.client.GetTradeInstance(ctx, &pb.ReqGetTradeInstance{InstId: instId})
rsp, err := c.marketClient.GetTradeInstance(ctx, &pb.ReqGetTradeInstance{InstId: instId})
if err != nil {
return
}
@ -62,14 +61,9 @@ func (c *TradeInstanceAside) getTradeInstance0(ctx context.Context, instId strin
}
// ListExchangeTradeInstance 获取交易所支持的交易实例
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{pbExType},
func (c *TradeInstanceAside) ListExchangeTradeInstance(ctx context.Context, exchange pb.ExchangeType) (exInsts []*entity.TradeInstanceExchange, err error) {
rsp, err := c.marketClient.ListExchangeTradeInstance(ctx, &pb.ReqListExchangeTradeInstance{
Exchanges: []pb.ExchangeType{exchange},
})
if err != nil {
return

9
pkg/data/common.go

@ -6,10 +6,11 @@ import "errors"
type Status int32
const (
StatusDisabled = 0
StatusOk = 1
StatusProcessing = 2
StatusDeleted = 4
StatusDisabled Status = 0
StatusOk Status = 1
StatusProcessing Status = 2
StatusDeleted Status = 4
StatusFailed Status = 5
)
var (

44
pkg/indicator/rsi.go

@ -0,0 +1,44 @@
package indicator
import (
"fmt"
"sig-pub/pkg/types"
"sig-pub/pkg/types/series"
"github.com/spf13/cast"
)
// RSI: 相对强弱指数 (RSI)
// rsi define: https://www.investopedia.com/terms/r/rsi.asp
type RSI struct {
series.Series
values series.Floats
prices series.Floats
baseline int32
}
func NewRSI(baseline int32) *RSI {
return &RSI{
baseline: baseline,
}
}
func (ind *RSI) Init(prams map[string]any) {
cast.ToIntE("1")
}
func (ind *RSI) Update(klines []types.Kline) (err error) {
for _, kline := range klines {
c, ok := kline.Close.Float64()
if !ok {
err = fmt.Errorf("kline close to float64 error: %s", kline.Close.String())
return
}
ind.prices.Push(c)
}
diff := ind.prices.Diff()
_ = diff
return
}

4
pkg/mapping/market.go

@ -32,7 +32,7 @@ func ExchangeInstance2Proto(exInst *entity.TradeInstanceExchange) (pbExInst *pb.
ExchangeInstId: exInst.ExchangeInstId,
InstId: exInst.InstId,
Status: exInst.Status,
Exchange: exInst.Exchange,
Exchange: pb.ExchangeType(exInst.Exchange),
UpdateBy: exInst.UpdateBy,
UpdateTime: exInst.UpdateTime,
}
@ -66,7 +66,7 @@ func Proto2ExchangeTradeInstance(pbExInst *pb.TradeInstanceExchange) (exInst *en
ExchangeInstId: pbExInst.ExchangeInstId,
InstId: pbExInst.InstId,
Status: pbExInst.Status,
Exchange: pbExInst.Exchange,
Exchange: int32(pbExInst.Exchange),
UpdateBy: pbExInst.UpdateBy,
UpdateTime: pbExInst.UpdateTime,
}

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), "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"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", inst.Exchange.String(), "kind", "open"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", inst.Exchange.String(), "kind", "high"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", inst.Exchange.String(), "kind", "low"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", inst.Exchange.String(), "kind", "close"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", inst.Exchange.String(), "kind", "vol"),
NewMetric(inst.InstId, "interval", string(kline.Interval), "exchange", inst.Exchange.String(), "kind", "volQuote"),
}
instMetrics[inst.InstId] = ms
for i := range len(ms) {

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

@ -100,7 +100,7 @@ func (vm *VictoriaMetricsTSDB) GetRangeKline(inst types.TradeInstance, interval
return
}
match := fmt.Sprintf("%s{interval=\"%s\"}", inst.InstId, interval)
match := fmt.Sprintf("%s{exchange=\"%s\", interval=\"%s\"}", inst.InstId, inst.Exchange.String(), interval)
params := fmt.Sprintf("start=%d&end=%d&match[]=%s", start, end, url.QueryEscape(match))
resp, err := http.Get(fmt.Sprintf("%s/api/v1/export?%s", vm.addr, params))
if err != nil {

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

@ -1,6 +1,7 @@
package vmts
import (
"sig-pub/api/pb"
"sig-pub/pkg/config"
"sig-pub/pkg/types"
"testing"
@ -13,7 +14,7 @@ func TestGetRangeKline(t *testing.T) {
vmdb := NewVictoriaMetricsTSDB(config.VictoriaMetricsConfig{Addr: test_addr})
inst := types.TradeInstance{
InstId: "BTC_USDT",
Exchange: types.ExchangeOKX,
Exchange: pb.ExchangeType_OKX,
}
vmdb.GetRangeKline(inst, types.Interval1m, 1753368047691, time.Now().UnixMilli())

36
pkg/types/exchange.go

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

4
pkg/types/indicator.go

@ -15,10 +15,10 @@ type Indicator interface {
Meta() IndicatorMeta
// init id, 计算完成 publish 时使用id,
// exchange: 封装indicator访问, 封装历史k线访问
// args: 动态指标参数, 执行时创建
// args: 动态指标参数定义, 执行时创建
Init(indId int64, exchange any, args map[string]any)
Dependes() []IndicatorDef // 需要订阅的指标列表(包括k线, 实时k线/关闭k线)
OnKline(Kline) // 驱动k线数据, 待驱动k线到达后, 再待subscribe计算完成后执行
OnKline([]Kline) // 驱动k线数据, 待驱动k线到达后, 再待subscribe计算完成后执行
Emit(func(klineTs int64, indicators map[string]decimal.Decimal)) // 指标计算完成后发送, 由指标执行器进行存储或分发
}

14
pkg/types/instance.go

@ -1,11 +1,13 @@
package types
import "sig-pub/api/pb"
// TradeInstance 交易产品
type TradeInstance struct {
InstId string // 交易产品系统id
PriceSz int32 // 价格精度
QuantitySz int32 // 交易量精度
Status int32 // 交易所交易产品状态
ExchangeInstId string // 交易所交易产品id
Exchange Exchange // 当前处理交易产品交易所
InstId string // 交易产品系统id
PriceSz int32 // 价格精度
QuantitySz int32 // 交易量精度
Status int32 // 交易所交易产品状态
ExchangeInstId string // 交易所交易产品id
Exchange pb.ExchangeType // 当前处理交易产品交易所
}

118
pkg/types/interval.go

@ -1,6 +1,8 @@
package types
import (
"sig-pub/pkg/zlog"
"sort"
"time"
)
@ -18,40 +20,7 @@ func (i Interval) AddMul(ts, mul int64) (int64, bool) {
return c(ts, mul), true
}
// Iota 返回interval唯一索引
func (i Interval) Iota(ts, mul int64) (int, bool) {
index, ok := IntervalIotas[i]
if !ok {
return 0, false
}
return index, 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) 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 (
const (
Interval1s = Interval("1s")
Interval1m = Interval("1m")
Interval3m = Interval("3m")
@ -109,24 +78,65 @@ var SupportedIntervals = IntervalMap{
Interval3mo: func(ts, mul int64) (ret int64) { return time.UnixMilli(ts).AddDate(0, int(3*mul), 0).UnixMilli() },
}
type IntervalIotaMap map[Interval]int
var IntervalIotas = IntervalIotaMap{
Interval1m: 2,
Interval3m: 3,
Interval5m: 4,
Interval15m: 5,
Interval30m: 6,
Interval1h: 7,
Interval2h: 8,
Interval4h: 9,
Interval6h: 10,
Interval12h: 11,
Interval1d: 12,
Interval2d: 13,
Interval3d: 14,
Interval5d: 15,
Interval1w: 16,
Interval1mo: 17,
Interval3mo: 18,
var (
iotasIntervals []Interval
intervalIotaMax int
intervalIotas = map[Interval]int{}
)
func init() {
iotasIntervals = make([]Interval, len(SupportedIntervals))
var tss = make([]int64, len(SupportedIntervals))
var i = 0
for interval, adder := range SupportedIntervals {
iotasIntervals[i] = interval
tss[i] = adder(0, 1)
i++
}
sort.Slice(iotasIntervals, func(i, j int) bool {
return tss[i] < tss[j]
})
for i, interval := range iotasIntervals {
index := i + 1 // 0保留
intervalIotas[interval] = index
intervalIotaMax = max(intervalIotaMax, index)
}
zlog.Debugf("init intervals: %#v", iotasIntervals)
zlog.Debugf("init intervalsIotas: %#v", intervalIotas)
}
type IntervalState[T any] struct {
state []T
}
func NewIntervalState[T any]() *IntervalState[T] {
return &IntervalState[T]{
state: make([]T, intervalIotaMax+1),
}
}
func (s *IntervalState[T]) Get(interval Interval) T {
i := intervalIotas[interval]
return s.state[i]
}
func (s *IntervalState[T]) Set(interval Interval, v T) {
i := intervalIotas[interval]
s.state[i] = v
}
func (s *IntervalState[T]) Range(f func(i int, interval Interval, v T)) {
for i, interval := range iotasIntervals {
index := i + 1 // 0保留
v := s.state[index]
f(i, interval, v)
}
}
func (s *IntervalState[T]) SetIf(interval Interval, v T, cond func(old T) bool) {
i := intervalIotas[interval]
old := s.state[i]
if cond(old) {
s.state[i] = v
}
}

8
pkg/types/kline.go

@ -25,7 +25,7 @@ type Kline struct {
// okx.CandleData{[]string{"1746842757000", "0.20545", "0.20545", "0.2054", "0.2054", "7.57", "7570", "1555.2542", "1"}}
func (k *Kline) ParsePBKline(exchange pb.Exchange, kline *pb.Kline) {
func (k *Kline) ParsePBKline(exchange pb.ExchangeType, kline *pb.Kline) {
// k.Exchange = exchange.String()
k.Interval = Interval(kline.Interval)
k.Ts = kline.Ts
@ -55,7 +55,7 @@ func (k *Kline) ToPBKline() (kline *pb.Kline) {
// ChannelKline k线订阅消息
type ChannelKline struct {
ExgInstId string `json:"instId"` // 交易所交易产品id,如 BTC_USDT_SWAP
Exchange Exchange `json:"exchange"` // 交易所
Klines []*Kline `json:"klines"`
ExgInstId string `json:"instId"` // 交易所交易产品id,如 BTC_USDT_SWAP
Exchange pb.ExchangeType `json:"exchange"` // 交易所
Klines []*Kline `json:"klines"`
}

33
pkg/types/series/decimals.go

@ -0,0 +1,33 @@
package series
import "github.com/govalues/decimal"
// 值列表
type Decimals []decimal.Decimal
func NewDecimals(a ...decimal.Decimal) Decimals {
return Decimals(a)
}
func (s *Decimals) Push(v decimal.Decimal) {
*s = append(*s, v)
}
func (s *Decimals) Append(vs ...decimal.Decimal) {
*s = append(*s, vs...)
}
func (s Decimals) Diff() (values Decimals, err error) {
var r decimal.Decimal
for i, v := range s {
if i == 0 {
values.Push(decimal.Zero)
continue
}
if r, err = v.Sub(s[i-1]); err != nil {
return
}
values.Push(r)
}
return
}

26
pkg/types/series/floats.go

@ -0,0 +1,26 @@
package series
type Floats []float64
func NewFloats(a ...float64) Floats {
return Floats(a)
}
func (s *Floats) Push(v float64) {
*s = append(*s, v)
}
func (s *Floats) Append(vs ...float64) {
*s = append(*s, vs...)
}
func (s Floats) Diff() (values Floats) {
for i, v := range s {
if i == 0 {
values.Push(0)
continue
}
values.Push(v - s[i-1])
}
return values
}

10
pkg/types/series/series.go

@ -0,0 +1,10 @@
package series
type Series interface {
Last(i int) float64
Index(i int) float64
Length() int
}
type Series0 struct {
}
Loading…
Cancel
Save