Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: support dynamic metrics for config fields #1871

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
21 changes: 21 additions & 0 deletions pkg/types/strategy.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading