diff --git a/api/indicator.proto b/api/indicator.proto deleted file mode 100644 index 7e4d512..0000000 --- a/api/indicator.proto +++ /dev/null @@ -1,25 +0,0 @@ -syntax = "proto3"; - -import "api/pub.proto"; - -option go_package = "./pb"; - -service IndicatorService { - rpc Subscribe(IndicatorSubReq) returns (stream Indicator); // 订阅指标 - rpc Plot(Indicator) returns (Indicator); // 绘图 -} - -message IndicatorSubReq { - string topic = 1; - string instId = 2; - int32 window = 3; -} - -message Indicator { - ExchangeType exhcange = 1; - string instId = 2; - string indicator = 3; - string sub = 4; // 指标子标题, MA5, MA10, MA20 - int64 Ts = 5; - bytes payload = 8; -} diff --git a/api/order.proto b/api/order.proto new file mode 100644 index 0000000..43248b1 --- /dev/null +++ b/api/order.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +import "api/pub.proto"; + +option go_package = "./pb"; + +service OrderService { + // request-response + rpc SubmitOrder(OrderSubmitReq) returns (OrderSubmitRsp) {} + // rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse) {} + // rpc QueryOrder(QueryOrderRequest) returns (QueryOrderResponse) {} + // rpc QueryOrders(QueryOrdersRequest) returns (QueryOrdersResponse) {} + // rpc QueryTrades(QueryTradesRequest) returns (QueryTradesResponse) {} +} + +message OrderSubmit { + string session = 1; + string exchange = 2; + string symbol = 3; + Side side = 4; + string price = 6; + string quantity = 5; + string stop_price = 7; + OrderType order_type = 8; + string client_order_id = 9; + int64 group_id = 10; +} + +message OrderSubmitReq { + string session = 1; + repeated OrderSubmit submit_orders = 2; +} + +message OrderSubmitRsp { + string session = 1; + repeated Order orders = 2; + Error error = 3; +} diff --git a/api/trading.proto b/api/trading.proto index ee3e7d2..deb5b0a 100644 --- a/api/trading.proto +++ b/api/trading.proto @@ -5,34 +5,20 @@ import "api/pub.proto"; option go_package = "./pb"; service TradingService { - // request-response - rpc SubmitOrder(SubmitOrderReq) returns (SubmitOrderRsp) {} - // rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse) {} - // rpc QueryOrder(QueryOrderRequest) returns (QueryOrderResponse) {} - // rpc QueryOrders(QueryOrdersRequest) returns (QueryOrdersResponse) {} - // rpc QueryTrades(QueryTradesRequest) returns (QueryTradesResponse) {} + rpc SubIndicator(IndicatorSubReq) returns (stream Indicator); // 订阅指标 } -message SubmitOrder { - string session = 1; - string exchange = 2; - string symbol = 3; - Side side = 4; - string price = 6; - string quantity = 5; - string stop_price = 7; - OrderType order_type = 8; - string client_order_id = 9; - int64 group_id = 10; +message IndicatorSubReq { + string topic = 1; + string instId = 2; + int32 window = 3; } -message SubmitOrderReq { - string session = 1; - repeated SubmitOrder submit_orders = 2; -} - -message SubmitOrderRsp { - string session = 1; - repeated Order orders = 2; - Error error = 3; +message Indicator { + ExchangeType exhcange = 1; + string instId = 2; + string indicator = 3; + string sub = 4; // 指标子标题, MA5, MA10, MA20 + int64 Ts = 5; + bytes payload = 8; } diff --git a/config/exchange.toml b/config/exchange.toml index d901904..c0a42b7 100644 --- a/config/exchange.toml +++ b/config/exchange.toml @@ -18,8 +18,8 @@ receiveBuffer = 4096 marketSubscribeLimit = 16 consumeBatch = 1024 consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume -# httpProxy = "http://192.168.1.6:7890" -httpProxy = "http://10.255.183.209:7890" +httpProxy = "http://192.168.1.5:7890" +# httpProxy = "http://10.255.183.209:7890" # 模拟盘API交易地址如下: # REST:https://www.okx.com diff --git a/generate.go b/generate.go index 47c263a..e1c65fa 100644 --- a/generate.go +++ b/generate.go @@ -6,7 +6,7 @@ import "fmt" //go:generate protoc --go_out=./api/ --go-grpc_out=./api/ ./api/pub.proto //go:generate protoc --go_out=./api/ --go-grpc_out=./api/ ./api/market.proto //go:generate protoc --go_out=./api/ --go-grpc_out=./api/ ./api/exchange.proto -//go:generate protoc --go_out=./api/ --go-grpc_out=./api/ ./api/indicator.proto +//go:generate protoc --go_out=./api/ --go-grpc_out=./api/ ./api/order.proto //go:generate protoc --go_out=./api/ --go-grpc_out=./api/ ./api/trading.proto // run cmd: go generate generate.go diff --git a/go.mod b/go.mod index 9118201..c1666f2 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( golang.org/x/net v0.38.0 golang.org/x/sync v0.12.0 golang.org/x/time v0.8.0 + gonum.org/v1/gonum v0.16.0 google.golang.org/grpc v1.71.1 google.golang.org/protobuf v1.36.6 gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum index bc5a0f9..c57d01c 100644 --- a/go.sum +++ b/go.sum @@ -424,6 +424,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= diff --git a/internal/exchange/exchange.go b/internal/exchange/exchange.go index 1d2d407..7cc6a9c 100644 --- a/internal/exchange/exchange.go +++ b/internal/exchange/exchange.go @@ -46,7 +46,7 @@ type Exchange struct { // 交易所交易产品 type ExchangeTradeInstance struct { Inst *types.TradeInstance - Status atomic.Int32 // 交易产品状态, 0.初始化中 1.正常 + Status atomic.Int32 // 交易产品状态, 0.初始化中 1.正常, todo status change event publish LiveKline *types.IntervalState[types.Kline] // 实时k线数据 LiveKStartTs *types.IntervalState[int64] // ws开始订阅k线标记时间戳 HistoryMarkTs *types.IntervalState[int64] // 拉取历史k线标记时间戳 diff --git a/internal/exchange/exchange_service.go b/internal/exchange/exchange_service.go index 1806bca..472d203 100644 --- a/internal/exchange/exchange_service.go +++ b/internal/exchange/exchange_service.go @@ -10,6 +10,7 @@ import ( "sig-pub/pkg/data" "sig-pub/pkg/types" "sig-pub/pkg/utils/collect" + "sig-pub/pkg/utils/conver" "sig-pub/pkg/utils/times" "sig-pub/pkg/zlog" "sync" @@ -176,9 +177,11 @@ func (svc *ExchangeService) consumerKline(exchange *Exchange, c <-chan *types.Ch msg.Klines = append(msg.Klines, pbk) } - if len(confirmKlines) > 0 { + // 存储到 tsdb + if _, ok := types.SupportedIntervals[firstKline.Interval]; ok && len(confirmKlines) > 0 { // tsdb storage todo 异步处理 err := svc.exchangeDataPersist.SaveKline(*tradeInst, confirmKlines) + // zlog.Infof("save confirm klines: instId=%s(%s), interval=%s, ts=%d", tradeInst.InstId, tradeInst.Exchange, firstKline.Interval, firstKline.Ts) if err != nil { zlog.Errorf("kline save to tsdb error: %v, %#v", err, confirmKlines) } else { @@ -261,8 +264,9 @@ func (svc *ExchangeService) initialTradeInstanceKlines(exchange *Exchange, trade retryTaskCh := make(chan fetchKlineTask, concurrent) // 发布任务数, 成功任务数, 失败任务次数 - var pubTasks, subTasks, failTasks atomic.Int32 + var pubTasks, subTasks, failTimes atomic.Int32 var pubTaskDone atomic.Bool // 所有任务已发布 + startTs := time.Now().UnixMilli() ctx, cancel := context.WithCancel(context.Background()) defer func() { @@ -278,16 +282,32 @@ func (svc *ExchangeService) initialTradeInstanceKlines(exchange *Exchange, trade } }) exchangeInst.Status.Store(int32(data.StatusOk)) + zlog.Infof("initial history kline finish: instId=%s(%s), pub=%d, sub=%d, fail=%d, use %s", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load(), subTasks.Load(), failTimes.Load(), conver.TimeMilliFormat(time.Now().UnixMilli()-startTs, "/")) }() + // progress monitor + go func() { + ticker := time.NewTicker(time.Second) + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + zlog.Debugf("processing initial history kline tasks: %s(%s), pub %d, sub %d, fail %d", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load(), subTasks.Load(), failTimes.Load()) + } + } + }() + + // fetch kline history task publish go func() { defer func() { pubTaskDone.Store(true) // 无任务处理 - if subTasks.Load() == 0 { + if pubTasks.Load() == 0 { cancel() } - zlog.Infof("trade instance initial kline %s(%s), pub %d fetch tasks", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load()) + // zlog.Infof("trade instance initial kline %s(%s), pub %d fetch tasks", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load()) }() for interval, intervalAdder := range types.SupportedIntervals { @@ -358,8 +378,9 @@ func (svc *ExchangeService) initialTradeInstanceKlines(exchange *Exchange, trade zlog.Infof("retry fetch history kline task %d times: task -> %s", task.times, task.logKey()) } + // fetch history kline if lastKlineTs, ex := svc.fetchTaskKlinesToTSDB(exchange, task); ex != nil { - failTasks.Add(1) + failTimes.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() @@ -382,9 +403,8 @@ func (svc *ExchangeService) initialTradeInstanceKlines(exchange *Exchange, trade // 任务都已执行成功结束 subs := subTasks.Add(1) - 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()) 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()) + // zlog.Infof("initial history kline tasks finished success, %s(%s), pub %d, sub %d, fail %d", tradeInst.InstId, tradeInst.Exchange, pubTasks.Load(), subTasks.Load(), failTimes.Load()) cancel() return } @@ -462,7 +482,7 @@ func (svc *ExchangeService) ExchangeInstanceState(allExchange bool, exchangeType if !ok { continue } - inst, ok := exchange.ExchangeInsts.Load(exchangeInstId) + exchangeInst, ok := exchange.ExchangeInsts.Load(exchangeInstId) if !ok { continue } @@ -470,7 +490,8 @@ func (svc *ExchangeService) ExchangeInstanceState(allExchange bool, exchangeType state := &pb.TradeInstanceState{ Exchange: exchange.ExchangeType, InstId: instId, - Last: inst.Last.String(), + Status: exchangeInst.Status.Load(), + Last: exchangeInst.Last.String(), } states = append(states, state) } diff --git a/internal/indicator/rsi.go b/internal/indicator/rsi.go deleted file mode 100644 index 73b09f2..0000000 --- a/internal/indicator/rsi.go +++ /dev/null @@ -1,35 +0,0 @@ -package indicator - -import ( - "fmt" - "sig-pub/api/pb" - "sig-pub/pkg/types" -) - -type RSI struct { - // types.IntervalWindow - window int // 窗口大小 -} - -func (ind *RSI) IntervalWindow() { - // 指标参数注入 -} - -func (ind *RSI) QueryRange(exchange pb.ExchangeType, instId string, interval types.Interval, rsi int) (query string, err error) { - var r types.MeticMatrix - _ = r - intervalAdder, ok := types.SupportedIntervals[interval] - if !ok { - err = fmt.Errorf("unsupport interval %s", interval) - return - } - minutes := intervalAdder(0, int64(rsi)) / 1000 / 60 - - query = fmt.Sprintf(` - 100 - 100 / (1 + ( - avg_over_time(clamp_min(delta(%s{kind="close", interval="%s", exchange="%s"}), 0)[%dm]) / - avg_over_time(abs(clamp_max(delta(%s{kind="close", interval="%s", exchange="%s"}), 0))[%dm]) - )) - `, instId, interval, exchange, minutes, instId, interval, exchange, minutes) - return -} diff --git a/internal/market/market_grpc_server.go b/internal/market/market_grpc_server.go index bd6dc41..a6b6d65 100644 --- a/internal/market/market_grpc_server.go +++ b/internal/market/market_grpc_server.go @@ -76,7 +76,7 @@ func (svr *MarketGrpcServer) ListMarketTradeInstance(ctx context.Context, req *p exchangeInstMap[exchangeInst.InstId] = pbInst } - tradeInsts, err := svr.tradeInstanceService.ListInstance(instIds...) + tradeInsts, err := svr.tradeInstanceService.ListInstanceById(instIds...) for _, inst := range tradeInsts { if pbInst, ok := exchangeInstMap[inst.InstId]; ok { pbInst.InstType = pb.TradeInstanceType(inst.InstType) diff --git a/internal/market/trade_instance_service.go b/internal/market/trade_instance_service.go index 6d13ec7..f0ea43c 100644 --- a/internal/market/trade_instance_service.go +++ b/internal/market/trade_instance_service.go @@ -40,7 +40,7 @@ func (s *TradeInstanceService) GetInstance(instId string) (inst *entity.TradeIns return } -func (s *TradeInstanceService) ListInstance(instIds ...string) (insts []*entity.TradeInstance, err error) { +func (s *TradeInstanceService) ListInstanceById(instIds ...string) (insts []*entity.TradeInstance, err error) { err = s.db.Select(&insts, `select * from t_trade_instance where inst_id in ? and status != ?`, instIds, data.StatusDeleted) if err != nil { return @@ -148,7 +148,7 @@ func (s *TradeInstanceService) UpdateInstanceStatus(inst *args.UpdateTradeInstan // ListExchangeTradeInstance 获取指定交易所的正常状态的交易产品 func (s *TradeInstanceService) ListExchangeTradeInstance(exchange pb.ExchangeType) (exchangesInsts []*entity.TradeInstanceExchange, err error) { err = s.db.Select(&exchangesInsts, ` - select * from t_trade_instance_exchange where exchange = ? and status in ? order by inst_id, exchange + select * from t_trade_instance_exchange where exchange = ? and status in ? order by inst_id, exchange order by update_time asc `, exchange, []data.Status{data.StatusOk, data.StatusProcessing}) if err != nil { return diff --git a/internal/trading/okx/trading.go b/internal/order/okx/trading.go similarity index 100% rename from internal/trading/okx/trading.go rename to internal/order/okx/trading.go diff --git a/internal/trading/service.go b/internal/order/service.go similarity index 86% rename from internal/trading/service.go rename to internal/order/service.go index ade22b8..d8539ee 100644 --- a/internal/trading/service.go +++ b/internal/order/service.go @@ -1,4 +1,4 @@ -package trading +package order // 交易服务 // load strategy plugins diff --git a/internal/indicator/indicator.go b/internal/trading/indicator.go similarity index 76% rename from internal/indicator/indicator.go rename to internal/trading/indicator.go index a03d0cb..ff962c6 100644 --- a/internal/indicator/indicator.go +++ b/internal/trading/indicator.go @@ -1,7 +1,7 @@ -package indicator +package trading import ( - vmts "sig-pub/pkg/storage/tsdb/victoria_metrics" + "sig-pub/pkg/indicator" "sync" ) @@ -11,15 +11,20 @@ import ( // 历史指标 实时计算 // 自定义插件化指标 type IndicatorService struct { - vmdb vmts.VictoriaMetricsTSDB + indicators map[string]indicator.RSI } -func NewIndicatorService(vmdb vmts.VictoriaMetricsTSDB) *IndicatorService { +func NewIndicatorService() *IndicatorService { return &IndicatorService{ - vmdb: vmdb, + indicators: make(map[string]indicator.RSI), } } +func (svc *IndicatorService) GetKlines(start, end int64) { + // Regist(RSI) + // if Indicators[RSI] +} + // 加载热指标 // 订阅k线数据 更新指标 func (svc *IndicatorService) Init() { @@ -43,6 +48,7 @@ func (svc *IndicatorService) Indicator(indicatorName string, args ...any) { } // kline -> indicator root -> /internal/force_flush -> all indicators (wg concurrent) -> all strategy (concurrent) +// kline -> indicator root -> all indicators -> type Ind struct { LiveMu sync.RWMutex // 实时k线锁, k线更新后指标更新时写锁 } diff --git a/internal/trading/kline_series.go b/internal/trading/kline_series.go new file mode 100644 index 0000000..334687e --- /dev/null +++ b/internal/trading/kline_series.go @@ -0,0 +1,56 @@ +package trading + +import ( + "sig-pub/api/pb" + "sig-pub/pkg/types" + "sig-pub/pkg/types/series" +) + +type KlineSeries struct { + Exchange pb.ExchangeType + InstId string + Interval types.Interval + Ts int64 + klines []*types.Kline + klineStore KlineStore +} + +func NewKlineSeries(ts int64, interval types.Interval, klineStore KlineStore) KlineSeries { + return KlineSeries{ + Ts: ts, + Interval: interval, + klineStore: klineStore, + } +} + +// Get +// [0]当前k线 +func (a KlineSeries) Get(start int16) (kline types.Kline) { + ts := a.Interval.MustAddMul(a.Ts, int64(-start)) + + for _, k := range a.klines { + if k.Ts == ts { + return *k + } + } + // todo query tsdb + return +} + +// Series [start...end] +func (a KlineSeries) Series(start, end int16) (klines series.Klines) { + endTs := a.Interval.MustAddMul(a.Ts, int64(-start)) + startTs := a.Interval.MustAddMul(a.Ts, int64(-end)) + _ = endTs + _ = startTs + // return a.klineStore.GetRange(startTs, endTs) + // todo + return +} + +func (s *KlineSeries) Update(kline *types.Kline) []types.Kline { + s.klines = append(s.klines, kline) + // todo copy(s.klines, s.klines[0:1]) set index=20, ts=kline.ts + s.Ts = kline.Ts + return nil +} diff --git a/internal/trading/kline_store.go b/internal/trading/kline_store.go new file mode 100644 index 0000000..1e8d172 --- /dev/null +++ b/internal/trading/kline_store.go @@ -0,0 +1,27 @@ +package trading + +import ( + "sig-pub/api/pb" + vmts "sig-pub/pkg/storage/tsdb/victoria_metrics" + "sig-pub/pkg/types" + "sig-pub/pkg/utils/collect" +) + +type KlineStore struct { + vmdb vmts.VictoriaMetricsTSDB + store [3]*collect.ConcurrentMap[string, collect.ConcurrentMap[types.Interval, *KlineSeries]] // K线列表: []exchange +} + +func NewKlineSeriesStore(vmdb vmts.VictoriaMetricsTSDB) (kss *KlineStore) { + kss = &KlineStore{ + vmdb: vmdb, + } + kss.store[pb.ExchangeType_OKX] = collect.NewConcurrentMap[string, collect.ConcurrentMap[types.Interval, *KlineSeries]](64, func(s string) string { return s }) + // kss.klines[pb.ExchangeType_BINANCE] = + return +} + +func (s *KlineStore) Update(exchange pb.ExchangeType, kline *types.Kline) (k types.Kline) { + + return +} diff --git a/pkg/indicator/base.go b/pkg/indicator/base.go new file mode 100644 index 0000000..8e716b2 --- /dev/null +++ b/pkg/indicator/base.go @@ -0,0 +1,23 @@ +package indicator + +import ( + "sig-pub/pkg/types" + "sig-pub/pkg/types/series" +) + +// IIndicator 指标基础计算接口 +type IIndicator interface { + Calculate() (vector float64) +} + +// IKlineSeries k线序列, strategy服务提供 +type IKlineSeries interface { + Get(start int16) (kline types.Kline) + Series(start, end int16) (klines series.Klines) +} + +// IIndicatorSeries 指标序列, 供策略读取, strategy服务提供 +type IIndicatorSeries interface { + Get(start int16) (vector float64) + Series(start, end int16) (matrix series.Floats) +} diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go index 4faf585..57e1b20 100644 --- a/pkg/indicator/rsi.go +++ b/pkg/indicator/rsi.go @@ -2,60 +2,65 @@ package indicator import ( "fmt" - "sig-pub/pkg/trader" + "sig-pub/api/pb" "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 +// RSI stateless indicator type RSI struct { - trader.Indicator - series.Series - values series.Floats - prices series.Floats - - argBaseDay int32 + // types.IntervalWindow + window int // 窗口大小 + kliner IKlineSeries } -func NewRSI() *RSI { - return &RSI{} +// New indicator interface +// 依赖注入 +// 指标参数注入 +func (c RSI) New(klineAccesser IKlineSeries) *RSI { + return &RSI{ + kliner: klineAccesser, + } } -func (ind RSI) Meta() trader.IndicatorMeta { - return trader.IndicatorMeta{ - Name: "RSI", - Desc: "", - Args: []trader.Arg{ - {Name: "基准天数", Desc: "", ArgType: trader.ArgTypeUInt}, - }, - } +// indicator interface +func (c *RSI) IntervalWindow(window int) { + c.window = window } -func (ind *RSI) Init(indId int64, exchange any, args []string) (code trader.ErrorCode, err error) { - arg0, err := cast.ToInt32E(args[0]) - if err != nil { - return - } - ind.argBaseDay = arg0 +// Calculate 计算单根k线rsi指标 +func (c *RSI) Calculate() (vector float64) { + // kline := c.kliner.Get(0) + // if kline.Close == kline.High { + // // emit buy/sell + // } + // 读k线, 计算 + klineSeries := c.kliner.Series(0, int16(c.window)) // 7根 + closeSeries := klineSeries.Close() + closeDiff := closeSeries.Diff() - cast.ToIntE("1") - return + avgGain := closeDiff.PositiveValuesOrZero().Abs().Sum() / float64(c.window) + avgLoss := closeDiff.NegativeValuesOrZero().Abs().Sum() / float64(c.window) + + rs := avgGain / avgLoss + rsi := 100 - (100 / (1 + rs)) + return rsi } -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) +func (c *RSI) QueryRange(exchange pb.ExchangeType, instId string, interval types.Interval, rsi int) (query string, err error) { + var r types.MeticMatrix + _ = r + intervalAdder, ok := types.SupportedIntervals[interval] + if !ok { + err = fmt.Errorf("unsupport interval %s", interval) + return } + minutes := intervalAdder(0, int64(rsi)) / 1000 / 60 - diff := ind.prices.Diff() - _ = diff + query = fmt.Sprintf(` + 100 - 100 / (1 + ( + avg_over_time(clamp_min(delta(%s{kind="close", interval="%s", exchange="%s"}), 0)[%dm]) / + avg_over_time(abs(clamp_max(delta(%s{kind="close", interval="%s", exchange="%s"}), 0))[%dm]) + )) + `, instId, interval, exchange, minutes, instId, interval, exchange, minutes) return } diff --git a/pkg/indicator/rsi0.go b/pkg/indicator/rsi0.go new file mode 100644 index 0000000..e6e2faa --- /dev/null +++ b/pkg/indicator/rsi0.go @@ -0,0 +1,61 @@ +package indicator + +import ( + "fmt" + "sig-pub/pkg/trader" + "sig-pub/pkg/types" + "sig-pub/pkg/types/series" + + "github.com/spf13/cast" +) + +// RSI0: 相对强弱指数 (RSI0) +// rsi define: https://www.investopedia.com/terms/r/rsi.asp +type RSI0 struct { + trader.Indicator + series.Series + values series.Floats + prices series.Floats + + argBaseDay int32 +} + +func NewRSI() *RSI0 { + return &RSI0{} +} + +func (ind RSI0) Meta() trader.IndicatorMeta { + return trader.IndicatorMeta{ + Name: "RSI", + Desc: "", + Args: []trader.Arg{ + {Name: "基准天数", Desc: "", ArgType: trader.ArgTypeUInt}, + }, + } +} + +func (ind *RSI0) Init(indId int64, exchange any, args []string) (code trader.ErrorCode, err error) { + arg0, err := cast.ToInt32E(args[0]) + if err != nil { + return + } + ind.argBaseDay = arg0 + + cast.ToIntE("1") + return +} + +func (ind *RSI0) 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 +} diff --git a/pkg/types/decimals/decimal.go b/pkg/types/decimals/decimal.go new file mode 100644 index 0000000..ae6ae9a --- /dev/null +++ b/pkg/types/decimals/decimal.go @@ -0,0 +1,15 @@ +package decimals + +import ( + "fmt" + + "github.com/govalues/decimal" +) + +func MustToFloat64(v decimal.Decimal) float64 { + f, ok := v.Float64() + if !ok { + panic(fmt.Errorf("decimal to float64 error: %v", v)) + } + return f +} diff --git a/pkg/types/interval.go b/pkg/types/interval.go index fa6c2f5..da06195 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "sig-pub/pkg/zlog" "sort" "time" @@ -20,6 +21,14 @@ func (i Interval) AddMul(ts, mul int64) (int64, bool) { return c(ts, mul), true } +func (i Interval) MustAddMul(ts, mul int64) int64 { + ts, ok := i.AddMul(ts, mul) + if !ok { + panic(fmt.Errorf("unsupport interval AddMul: interval=%s, ts=%d, mul=%d", i, ts, mul)) + } + return ts +} + const ( Interval1s = Interval("1s") Interval1m = Interval("1m") diff --git a/pkg/types/series/floats.go b/pkg/types/series/floats.go index d31af4f..8c4f325 100644 --- a/pkg/types/series/floats.go +++ b/pkg/types/series/floats.go @@ -1,9 +1,15 @@ package series +import ( + "math" + + "gonum.org/v1/gonum/floats" +) + type Floats []float64 -func NewFloats(length int) Floats { - return make(Floats, 0, length+1) +func NewFloats(vs ...float64) Floats { + return Floats(vs) } func (s *Floats) Push(v float64) { @@ -24,3 +30,52 @@ func (s Floats) Diff() (values Floats) { } return values } + +func (s Floats) Sub(b Floats) (c Floats) { + if len(s) != len(b) { + return c + } + + c = make(Floats, len(s)) + for i := range s { + c[i] = s[i] - b[i] + } + return c +} + +func (s Floats) Max() float64 { + return floats.Max(s) +} + +func (s Floats) Min() float64 { + return floats.Min(s) +} + +func (s Floats) PositiveValuesOrZero() (values Floats) { + for _, v := range s { + values.Push(math.Max(v, 0)) + } + return values +} + +func (s Floats) NegativeValuesOrZero() (values Floats) { + for _, v := range s { + values.Push(math.Min(v, 0)) + } + return values +} + +func (s Floats) Abs() (values Floats) { + values = make(Floats, 0, len(s)) + for _, v := range s { + values.Push(math.Abs(v)) + } + return values +} + +func (s Floats) Sum() (sum float64) { + for _, v := range s { + sum += v + } + return sum +} diff --git a/pkg/types/series/funcs.go b/pkg/types/series/funcs.go deleted file mode 100644 index c008a7a..0000000 --- a/pkg/types/series/funcs.go +++ /dev/null @@ -1,23 +0,0 @@ -package series - -import ( - "sig-pub/pkg/types" - - "github.com/govalues/decimal" -) - -func Klines2Decimals(klines []types.Kline, mapping func(kline types.Kline) decimal.Decimal) (r Decimals) { - r = NewDecimals(len(klines)) - for _, k := range klines { - r.Push(mapping(k)) - } - return -} - -func Klines2Floats(klines []types.Kline, mapping func(kline types.Kline) float64) (r Floats) { - r = NewFloats(len(klines)) - for _, k := range klines { - r.Push(mapping(k)) - } - return -} diff --git a/pkg/types/series/klines.go b/pkg/types/series/klines.go new file mode 100644 index 0000000..8f1b146 --- /dev/null +++ b/pkg/types/series/klines.go @@ -0,0 +1,37 @@ +package series + +import ( + "sig-pub/pkg/types" + "sig-pub/pkg/types/decimals" + "sig-pub/pkg/utils/collect" +) + +type Klines []types.Kline + +func (s Klines) Times() []int64 { + return collect.Mapping(s, func(_ int, k types.Kline) int64 { return k.Ts }) +} + +func (s Klines) Open() Floats { + return collect.Mapping(s, func(_ int, k types.Kline) float64 { return decimals.MustToFloat64(k.Open) }) +} + +func (s Klines) Close() Floats { + return collect.Mapping(s, func(_ int, k types.Kline) float64 { return decimals.MustToFloat64(k.Close) }) +} + +func (s Klines) High() Floats { + return collect.Mapping(s, func(_ int, k types.Kline) float64 { return decimals.MustToFloat64(k.High) }) +} + +func (s Klines) Low() Floats { + return collect.Mapping(s, func(_ int, k types.Kline) float64 { return decimals.MustToFloat64(k.Low) }) +} + +func (s Klines) Vol() Floats { + return collect.Mapping(s, func(_ int, k types.Kline) float64 { return decimals.MustToFloat64(k.Vol) }) +} + +func (s Klines) VolQuote() Floats { + return collect.Mapping(s, func(_ int, k types.Kline) float64 { return decimals.MustToFloat64(k.VolQuote) }) +} diff --git a/pkg/utils/conver/unit_conver.go b/pkg/utils/conver/unit_conver.go index 3c5752a..af9f3f3 100644 --- a/pkg/utils/conver/unit_conver.go +++ b/pkg/utils/conver/unit_conver.go @@ -2,6 +2,7 @@ package conver import ( "fmt" + "strings" "time" "github.com/dsnet/golib/unitconv" @@ -42,3 +43,35 @@ func MustParseDuration(s string) time.Duration { } return d } + +// TimeMilliFormat 时间戳差转换成可视化时间单位 2100 -> 1h-2s-100ms +func TimeMilliFormat(milli int64, sep string) (r string) { + day := milli / (1000 * 60 * 60 * 24) + milli -= day * (1000 * 60 * 60 * 24) + hour := milli / (1000 * 60 * 60) + milli -= hour * (1000 * 60 * 60) + minite := milli / (1000 * 60) + milli -= minite * (1000 * 60) + second := milli / (1000) + milli -= second * (1000) + + vs := []int64{day, hour, minite, second, milli} + units := []string{"d", "h", "m", "s", "ms"} + index := -1 + for i, v := range vs { + if v > 0 { + index = i + break + } + } + if index == -1 { + r = "0ms" + return + } + var rs []string + for ; index < len(vs); index++ { + rs = append(rs, fmt.Sprintf("%d%s", vs[index], units[index])) + } + r = strings.Join(rs, sep) + return +}