diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index 6cb84247d6..8f8cabf7e9 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -565,7 +565,9 @@ func getStrategySignatures(exchangeStrategies map[string]SingleExchangeStrategy) // filterStrategies filters the exchange strategies by a filter tester function // if filter() returns true, the strategy will be added to the returned map. -func filterStrategies(exchangeStrategies map[string]SingleExchangeStrategy, filter func(s SingleExchangeStrategy) bool) (map[string]SingleExchangeStrategy, error) { +func filterStrategies( + exchangeStrategies map[string]SingleExchangeStrategy, filter func(s SingleExchangeStrategy) bool, +) (map[string]SingleExchangeStrategy, error) { retStrategies := make(map[string]SingleExchangeStrategy) for signature, strategy := range exchangeStrategies { if ok := filter(strategy); ok { @@ -594,14 +596,18 @@ func testInterface(obj interface{}, checkType interface{}) bool { return reflect.TypeOf(obj).Implements(rt) } -func filterStrategiesByInterface(exchangeStrategies map[string]SingleExchangeStrategy, checkInterface interface{}) (map[string]SingleExchangeStrategy, error) { +func filterStrategiesByInterface( + exchangeStrategies map[string]SingleExchangeStrategy, checkInterface interface{}, +) (map[string]SingleExchangeStrategy, error) { rt := reflect.TypeOf(checkInterface).Elem() return filterStrategies(exchangeStrategies, func(s SingleExchangeStrategy) bool { return reflect.TypeOf(s).Implements(rt) }) } -func filterStrategiesByField(exchangeStrategies map[string]SingleExchangeStrategy, fieldName string, fieldType reflect.Type) (map[string]SingleExchangeStrategy, error) { +func filterStrategiesByField( + exchangeStrategies map[string]SingleExchangeStrategy, fieldName string, fieldType reflect.Type, +) (map[string]SingleExchangeStrategy, error) { return filterStrategies(exchangeStrategies, func(s SingleExchangeStrategy) bool { r := reflect.ValueOf(s).Elem() f := r.FieldByName(fieldName) diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 13c83212b1..c001cc82f9 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -13,34 +13,20 @@ import ( "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" ) -// Strategy method calls: -// -> Defaults() (optional method) -// -// setup default static values from constants -// -// -> Initialize() (optional method) -// -// initialize dynamic runtime objects -// -// -> Subscribe() -// -// register the subscriptions -// -// -> Validate() (optional method) -// -> Run() (optional method) -// -> Shutdown(shutdownCtx context.Context, wg *sync.WaitGroup) -type StrategyID interface { - ID() string -} - // SingleExchangeStrategy represents the single Exchange strategy type SingleExchangeStrategy interface { - StrategyID + types.StrategyID Run(ctx context.Context, orderExecutor OrderExecutor, session *ExchangeSession) error } +type CrossExchangeStrategy interface { + types.StrategyID + CrossRun(ctx context.Context, orderExecutionRouter OrderExecutionRouter, sessions map[string]*ExchangeSession) error +} + // StrategyInitializer's Initialize method is called before the Subscribe method call. type StrategyInitializer interface { Initialize() error @@ -68,11 +54,6 @@ type CrossExchangeSessionSubscriber interface { CrossSubscribe(sessions map[string]*ExchangeSession) } -type CrossExchangeStrategy interface { - StrategyID - CrossRun(ctx context.Context, orderExecutionRouter OrderExecutionRouter, sessions map[string]*ExchangeSession) error -} - type Logging interface { EnableLogging() DisableLogging() @@ -358,7 +339,7 @@ func (trader *Trader) Run(ctx context.Context) error { // It sets the default values and validates the strategy configurations. // And calls the Initialize method if the strategy implements the Initialize method. func (trader *Trader) Initialize(ctx context.Context) error { - return trader.IterateStrategies(func(strategy StrategyID) error { + return trader.IterateStrategies(func(strategy types.StrategyID) error { if defaulter, ok := strategy.(StrategyDefaulter); ok { if err := defaulter.Defaults(); err != nil { return err @@ -388,13 +369,13 @@ func (trader *Trader) LoadState(ctx context.Context) error { ps := isolation.persistenceServiceFacade.Get() log.Infof("loading strategies states...") - return trader.IterateStrategies(func(strategy StrategyID) error { + return trader.IterateStrategies(func(strategy types.StrategyID) error { id := dynamic.CallID(strategy) return loadPersistenceFields(strategy, id, ps) }) } -func (trader *Trader) IterateStrategies(f func(st StrategyID) error) error { +func (trader *Trader) IterateStrategies(f func(st types.StrategyID) error) error { for _, strategies := range trader.exchangeStrategies { for _, strategy := range strategies { if err := f(strategy); err != nil { @@ -423,7 +404,7 @@ func (trader *Trader) SaveState(ctx context.Context) error { ps := isolation.persistenceServiceFacade.Get() log.Debugf("saving strategy persistence states...") - return trader.IterateStrategies(func(strategy StrategyID) error { + return trader.IterateStrategies(func(strategy types.StrategyID) error { id := dynamic.CallID(strategy) if len(id) == 0 { return nil diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index d94e930236..64191e2bed 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -378,7 +378,7 @@ var BacktestCmd = &cobra.Command{ } stateRecorder := backtest.NewStateRecorder(reportDir) - err = trader.IterateStrategies(func(st bbgo.StrategyID) error { + err = trader.IterateStrategies(func(st types.StrategyID) error { return stateRecorder.Scan(st.(backtest.Instance)) }) if err != nil { diff --git a/pkg/dynamic/metric.go b/pkg/dynamic/metric.go new file mode 100644 index 0000000000..034dfd0e42 --- /dev/null +++ b/pkg/dynamic/metric.go @@ -0,0 +1,114 @@ +package dynamic + +import ( + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +var dynamicStrategyConfigMetrics = map[string]any{} + +func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { + matchFirstCapRE := regexp.MustCompile("(.)([A-Z][a-z]+)") + + tv := reflect.TypeOf(s).Elem() + sv := reflect.Indirect(reflect.ValueOf(s)) + + symbolField := sv.FieldByName("Symbol") + hasSymbolField := symbolField.IsValid() + +nextStructField: + for i := 0; i < tv.NumField(); i++ { + field := tv.Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + continue nextStructField + } + + tagAttrs := strings.Split(jsonTag, ",") + if len(tagAttrs) == 0 { + continue nextStructField + } + + fieldName := tagAttrs[0] + fieldName = strings.ToLower(matchFirstCapRE.ReplaceAllString(fieldName, "${1}_${2}")) + + isStr := false + + val := 0.0 + valInf := sv.Field(i).Interface() + switch tt := valInf.(type) { + case string: + isStr = true + + case fixedpoint.Value: + val = tt.Float64() + case *fixedpoint.Value: + if tt != nil { + val = tt.Float64() + } + case float64: + val = tt + case int: + val = float64(tt) + case int32: + val = float64(tt) + case int64: + val = float64(tt) + case bool: + if tt { + val = 1.0 + } else { + val = 0.0 + } + default: + continue nextStructField + } + + if isStr { + continue nextStructField + } + + symbol := "" + if hasSymbolField { + symbol = symbolField.String() + } + + metricName := id + "_config_" + fieldName + anyMetric, ok := dynamicStrategyConfigMetrics[metricName] + if !ok { + gaugeMetric := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: metricName, + Help: id + " config value of " + field.Name, + }, + []string{"strategy_type", "strategy_id", "symbol"}, + ) + if err := prometheus.Register(gaugeMetric); err != nil { + return fmt.Errorf("unable to register metrics on field %+v, error: %+v", field.Name, err) + } + + anyMetric = gaugeMetric + dynamicStrategyConfigMetrics[metricName] = anyMetric + } + + if anyMetric != nil { + switch metric := anyMetric.(type) { + case *prometheus.GaugeVec: + metric.With(prometheus.Labels{ + "strategy_type": id, + "strategy_id": instanceId, + "symbol": symbol, + }).Set(val) + } + } + } + + return nil +} diff --git a/pkg/strategy/liquiditymaker/strategy.go b/pkg/strategy/liquiditymaker/strategy.go index b18f683039..913586395e 100644 --- a/pkg/strategy/liquiditymaker/strategy.go +++ b/pkg/strategy/liquiditymaker/strategy.go @@ -11,6 +11,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/dbg" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/strategy/common" @@ -200,6 +201,11 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.stopEMA = session.Indicators(s.Symbol).EMA(s.StopEMA.IntervalWindow) } + instanceID := s.InstanceID() + if err := dynamic.InitializeConfigMetrics(ID, instanceID, s); err != nil { + return err + } + s.metricsLabels["exchange"] = session.ExchangeName.String() s.orderGenerator = &LiquidityOrderGenerator{ diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 053bf20b14..8dbf0ff488 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" . "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/strategy/common" @@ -122,6 +123,11 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.liquidityScale = scale + instanceID := s.InstanceID() + if err := dynamic.InitializeConfigMetrics(ID, instanceID, s); err != nil { + return err + } + s.initializeMidPriceEMA(session) s.initializePriceRangeBollinger(session) s.initializeIntensityIndicator(session) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index ebd10eff87..49a5b9f629 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -14,6 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/core" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/pricesolver" "github.com/c9s/bbgo/pkg/slack/slackalert" @@ -439,12 +440,15 @@ func (s *Strategy) selectSessionForCurrency( func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { instanceID := s.InstanceID() - _ = instanceID s.sessions = make(map[string]*bbgo.ExchangeSession) s.orderBooks = make(map[string]*bbgo.ActiveOrderBook) s.orderStore = core.NewOrderStore("") + if err := dynamic.InitializeConfigMetrics(ID, instanceID, s); err != nil { + return err + } + for currency, expectedValue := range s.ExpectedBalances { s.deviationDetectors[currency] = detector.NewDeviationDetector( types.Balance{Currency: currency, NetAsset: expectedValue}, // Expected value diff --git a/pkg/strategy/xmaker/metrics.go b/pkg/strategy/xmaker/metrics.go index c4acef245b..942f1933c0 100644 --- a/pkg/strategy/xmaker/metrics.go +++ b/pkg/strategy/xmaker/metrics.go @@ -71,30 +71,6 @@ var aggregatedSignalMetrics = prometheus.NewGaugeVec( Help: "", }, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) -var configNumOfLayersMetrics = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "xmaker_config_num_of_layers", - Help: "", - }, []string{"strategy_type", "strategy_id", "symbol"}) - -var configMaxExposureMetrics = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "xmaker_config_max_exposure", - Help: "", - }, []string{"strategy_type", "strategy_id", "symbol"}) - -var configBidMarginMetrics = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "xmaker_config_bid_margin", - Help: "", - }, []string{"strategy_type", "strategy_id", "symbol"}) - -var configAskMarginMetrics = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "xmaker_config_ask_margin", - Help: "", - }, []string{"strategy_type", "strategy_id", "symbol"}) - var netProfitMarginHistogram = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "xmaker_net_profit_margin", @@ -113,10 +89,6 @@ func init() { aggregatedSignalMetrics, cancelOrderDurationMetrics, makerOrderPlacementDurationMetrics, - configNumOfLayersMetrics, - configMaxExposureMetrics, - configBidMarginMetrics, - configAskMarginMetrics, delayedHedgeCounterMetrics, delayedHedgeMaxDurationMetrics, netProfitMarginHistogram, diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 79d914a7cd..83d07c6a94 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -1884,6 +1884,10 @@ func (s *Strategy) CrossRun( s.PrintConfig(configWriter, true, false) s.logger.Infof("config: %s", configWriter.String()) + if err := dynamic.InitializeConfigMetrics(ID, instanceID, s); err != nil { + return err + } + // configure sessions sourceSession, ok := sessions[s.SourceExchange] if !ok { @@ -1926,21 +1930,13 @@ func (s *Strategy) CrossRun( s.groupID = util.FNV32(instanceID) s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) - configLabels := prometheus.Labels{"strategy_id": s.InstanceID(), "strategy_type": ID, "symbol": s.Symbol} - configNumOfLayersMetrics.With(configLabels).Set(float64(s.NumLayers)) - configMaxExposureMetrics.With(configLabels).Set(s.MaxExposurePosition.Float64()) - configBidMarginMetrics.With(configLabels).Set(s.BidMargin.Float64()) - configAskMarginMetrics.With(configLabels).Set(s.AskMargin.Float64()) - if s.Position == nil { s.Position = types.NewPositionFromMarket(s.makerMarket) - s.Position.Strategy = ID - s.Position.StrategyInstanceID = instanceID - } else { - s.Position.Strategy = ID - s.Position.StrategyInstanceID = instanceID } + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID + if s.makerSession.MakerFeeRate.Sign() > 0 || s.makerSession.TakerFeeRate.Sign() > 0 { s.Position.SetExchangeFeeRate(types.ExchangeName(s.MakerExchange), types.ExchangeFee{ MakerFeeRate: s.makerSession.MakerFeeRate, diff --git a/pkg/types/strategy.go b/pkg/types/strategy.go new file mode 100644 index 0000000000..ef396f2cf4 --- /dev/null +++ b/pkg/types/strategy.go @@ -0,0 +1,21 @@ +package types + +// Strategy method calls: +// -> Defaults() (optional method) +// +// setup default static values from constants +// +// -> Initialize() (optional method) +// +// initialize dynamic runtime objects +// +// -> Subscribe() +// +// register the subscriptions +// +// -> Validate() (optional method) +// -> Run() (optional method) +// -> Shutdown(shutdownCtx context.Context, wg *sync.WaitGroup) +type StrategyID interface { + ID() string +}