Skip to content

Commit

Permalink
Merge pull request #1871 from c9s/c9s/feature/dynamic-metrics
Browse files Browse the repository at this point in the history
FEATURE:  support dynamic metrics for config fields
  • Loading branch information
c9s authored Dec 18, 2024
2 parents 6fb4729 + 5cf9a3a commit fbe01f2
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 74 deletions.
12 changes: 9 additions & 3 deletions pkg/bbgo/interact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 11 additions & 30 deletions pkg/bbgo/trader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/backtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
114 changes: 114 additions & 0 deletions pkg/dynamic/metric.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions pkg/strategy/liquiditymaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
6 changes: 6 additions & 0 deletions pkg/strategy/scmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion pkg/strategy/xalign/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
28 changes: 0 additions & 28 deletions pkg/strategy/xmaker/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -113,10 +89,6 @@ func init() {
aggregatedSignalMetrics,
cancelOrderDurationMetrics,
makerOrderPlacementDurationMetrics,
configNumOfLayersMetrics,
configMaxExposureMetrics,
configBidMarginMetrics,
configAskMarginMetrics,
delayedHedgeCounterMetrics,
delayedHedgeMaxDurationMetrics,
netProfitMarginHistogram,
Expand Down
18 changes: 7 additions & 11 deletions pkg/strategy/xmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit fbe01f2

Please sign in to comment.