diff --git a/pkg/core/converter.go b/pkg/core/converter.go new file mode 100644 index 0000000000..a154e2758a --- /dev/null +++ b/pkg/core/converter.go @@ -0,0 +1,85 @@ +package core + +import ( + "errors" + + "github.com/c9s/bbgo/pkg/types" +) + +type Converter interface { + OrderConverter + TradeConverter + Initialize() error +} + +// OrderConverter converts the order to another order +type OrderConverter interface { + ConvertOrder(order types.Order) (types.Order, error) +} + +// TradeConverter converts the trade to another trade +type TradeConverter interface { + ConvertTrade(trade types.Trade) (types.Trade, error) +} + +type OrderConvertFunc func(order types.Order) (types.Order, error) +type TradeConvertFunc func(trade types.Trade) (types.Trade, error) + +type DynamicConverter struct { + orderConverter OrderConvertFunc + tradeConverter TradeConvertFunc +} + +func NewDynamicConverter(orderConverter OrderConvertFunc, tradeConverter TradeConvertFunc) *DynamicConverter { + return &DynamicConverter{orderConverter: orderConverter, tradeConverter: tradeConverter} +} + +func (c *DynamicConverter) Initialize() error { + return nil +} + +func (c *DynamicConverter) ConvertOrder(order types.Order) (types.Order, error) { + return c.orderConverter(order) +} + +func (c *DynamicConverter) ConvertTrade(trade types.Trade) (types.Trade, error) { + return c.tradeConverter(trade) +} + +// SymbolConverter converts the symbol to another symbol +type SymbolConverter struct { + FromSymbol string `json:"from"` + ToSymbol string `json:"to"` +} + +func NewSymbolConverter(fromSymbol, toSymbol string) *SymbolConverter { + return &SymbolConverter{FromSymbol: fromSymbol, ToSymbol: toSymbol} +} + +func (c *SymbolConverter) Initialize() error { + if c.ToSymbol == "" { + return errors.New("toSymbol can not be empty") + } + + if c.FromSymbol == "" { + return errors.New("fromSymbol can not be empty") + } + + return nil +} + +func (c *SymbolConverter) ConvertOrder(order types.Order) (types.Order, error) { + if order.Symbol == c.FromSymbol { + order.Symbol = c.ToSymbol + } + + return order, nil +} + +func (c *SymbolConverter) ConvertTrade(trade types.Trade) (types.Trade, error) { + if trade.Symbol == c.FromSymbol { + trade.Symbol = c.ToSymbol + } + + return trade, nil +} diff --git a/pkg/core/converter_test.go b/pkg/core/converter_test.go new file mode 100644 index 0000000000..4718eca2ba --- /dev/null +++ b/pkg/core/converter_test.go @@ -0,0 +1,31 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func TestSymbolConverter(t *testing.T) { + converter := NewSymbolConverter("MAXEXCHANGEUSDT", "MAXUSDT") + trade, err := converter.ConvertTrade(types.Trade{ + Symbol: "MAXEXCHANGEUSDT", + }) + + if assert.NoError(t, err) { + assert.Equal(t, "MAXUSDT", trade.Symbol) + } + + order, err := converter.ConvertOrder(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "MAXEXCHANGEUSDT", + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, "MAXUSDT", order.Symbol) + } + +} diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 6069d2c266..9622e912c1 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -12,8 +12,87 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type TradeConverter interface { - Convert(trade types.Trade) (types.Trade, error) +type ConverterSetting struct { + SymbolConverter *SymbolConverter `json:"symbolConverter" yaml:"symbolConverter"` +} + +func (s *ConverterSetting) getConverter() Converter { + if s.SymbolConverter != nil { + return s.SymbolConverter + } + + return nil +} + +func (s *ConverterSetting) InitializeConverter() (Converter, error) { + converter := s.getConverter() + if converter == nil { + return nil, nil + } + + logrus.Infof("initializing converter %T ...", converter) + err := converter.Initialize() + return nil, err +} + +type ConverterManager struct { + ConverterSettings []ConverterSetting `json:"converters,omitempty" yaml:"converters,omitempty"` + + converters []Converter +} + +func (c *ConverterManager) Initialize() error { + for _, setting := range c.ConverterSettings { + + converter, err := setting.InitializeConverter() + if err != nil { + return err + } + + c.AddConverter(converter) + } + + return nil +} + +func (c *ConverterManager) AddConverter(converter Converter) { + c.converters = append(c.converters, converter) +} + +func (c *ConverterManager) ConvertOrder(order types.Order) types.Order { + if len(c.converters) == 0 { + return order + } + + for _, converter := range c.converters { + convOrder, err := converter.ConvertOrder(order) + if err != nil { + logrus.WithError(err).Errorf("converter %+v error, order: %s", converter, order.String()) + continue + } + + order = convOrder + } + + return order +} + +func (c *ConverterManager) ConvertTrade(trade types.Trade) types.Trade { + if len(c.converters) == 0 { + return trade + } + + for _, converter := range c.converters { + convTrade, err := converter.ConvertTrade(trade) + if err != nil { + logrus.WithError(err).Errorf("converter %+v error, trade: %s", converter, trade.String()) + continue + } + + trade = convTrade + } + + return trade } //go:generate callbackgen -type TradeCollector @@ -29,14 +108,14 @@ type TradeCollector struct { mu sync.Mutex - tradeConverters []TradeConverter - recoverCallbacks []func(trade types.Trade) tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) positionUpdateCallbacks []func(position *types.Position) profitCallbacks []func(trade types.Trade, profit *types.Profit) + + ConverterManager } func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector { @@ -55,28 +134,6 @@ func NewTradeCollector(symbol string, position *types.Position, orderStore *Orde } } -func (c *TradeCollector) AddTradeConverter(converter TradeConverter) { - c.tradeConverters = append(c.tradeConverters, converter) -} - -func (c *TradeCollector) convertTrade(trade types.Trade) types.Trade { - if len(c.tradeConverters) == 0 { - return trade - } - - for _, converter := range c.tradeConverters { - convTrade, err := converter.Convert(trade) - if err != nil { - logrus.WithError(err).Errorf("trade %+v converter error, trade: %s", converter, trade.String()) - continue - } - - trade = convTrade - } - - return trade -} - // OrderStore returns the order store used by the trade collector func (c *TradeCollector) OrderStore() *OrderStore { return c.orderStore @@ -108,7 +165,7 @@ func (c *TradeCollector) BindStreamForBackground(stream types.Stream) { func (c *TradeCollector) BindStream(stream types.Stream) { stream.OnTradeUpdate(func(trade types.Trade) { - c.processTrade(trade) + c.ProcessTrade(trade) }) } @@ -144,7 +201,7 @@ func (c *TradeCollector) Recover( } func (c *TradeCollector) RecoverTrade(td types.Trade) bool { - td = c.convertTrade(td) + td = c.ConvertTrade(td) logrus.Debugf("checking trade: %s", td.String()) if c.processTrade(td) { @@ -260,7 +317,7 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { // return true when the given trade is added // return false when the given trade is not added func (c *TradeCollector) ProcessTrade(trade types.Trade) bool { - return c.processTrade(c.convertTrade(trade)) + return c.processTrade(c.ConvertTrade(trade)) } // Run is a goroutine executed in the background @@ -279,7 +336,7 @@ func (c *TradeCollector) Run(ctx context.Context) { c.Process() case trade := <-c.tradeC: - c.processTrade(c.convertTrade(trade)) + c.processTrade(c.ConvertTrade(trade)) } } diff --git a/pkg/strategy/common/profit_fixer.go b/pkg/strategy/common/profit_fixer.go index 44817acdf5..9aebcf6178 100644 --- a/pkg/strategy/common/profit_fixer.go +++ b/pkg/strategy/common/profit_fixer.go @@ -10,6 +10,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/types" ) @@ -22,6 +23,8 @@ type ProfitFixerConfig struct { // ProfitFixer implements a trade-history-based profit fixer type ProfitFixer struct { sessions map[string]types.ExchangeTradeHistoryService + + core.ConverterManager } func NewProfitFixer() *ProfitFixer { @@ -106,6 +109,8 @@ func (f *ProfitFixer) Fix( func (f *ProfitFixer) FixFromTrades(allTrades []types.Trade, stats *types.ProfitStats, position *types.Position) error { for _, trade := range allTrades { + trade = f.ConverterManager.ConvertTrade(trade) + profit, netProfit, madeProfit := position.AddTrade(trade) if madeProfit { p := position.NewProfit(trade, profit, netProfit) diff --git a/pkg/strategy/xdepthmaker/strategy.go b/pkg/strategy/xdepthmaker/strategy.go index 4a2623c4b9..103c50bf49 100644 --- a/pkg/strategy/xdepthmaker/strategy.go +++ b/pkg/strategy/xdepthmaker/strategy.go @@ -12,6 +12,7 @@ import ( "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/strategy/common" @@ -48,7 +49,10 @@ type CrossExchangeMarketMakingStrategy struct { Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"` - mu sync.Mutex + + core.ConverterManager + + mu sync.Mutex MakerOrderExecutor, HedgeOrderExecutor *bbgo.GeneralOrderExecutor } @@ -78,6 +82,10 @@ func (s *CrossExchangeMarketMakingStrategy) Initialize( return fmt.Errorf("maker session market %s is not defined", symbol) } + if err := s.ConverterManager.Initialize(); err != nil { + return err + } + if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.makerMarket) } @@ -106,6 +114,10 @@ func (s *CrossExchangeMarketMakingStrategy) Initialize( s.makerMarket.Symbol, strategyID, instanceID, s.Position) + + // update converter manager + s.MakerOrderExecutor.TradeCollector().ConverterManager = s.ConverterManager + s.MakerOrderExecutor.BindEnvironment(environ) s.MakerOrderExecutor.BindProfitStats(s.ProfitStats) s.MakerOrderExecutor.Bind() @@ -121,6 +133,9 @@ func (s *CrossExchangeMarketMakingStrategy) Initialize( s.HedgeOrderExecutor.BindEnvironment(environ) s.HedgeOrderExecutor.BindProfitStats(s.ProfitStats) s.HedgeOrderExecutor.Bind() + + s.HedgeOrderExecutor.TradeCollector().ConverterManager = s.ConverterManager + s.HedgeOrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { // bbgo.Sync(ctx, s) }) @@ -149,6 +164,7 @@ type Strategy struct { Environment *bbgo.Environment + // Symbol is the maker exchange symbol Symbol string `json:"symbol"` // HedgeSymbol is the symbol for the hedge exchange @@ -251,6 +267,7 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { }) hedgeSession.Subscribe(types.KLineChannel, s.HedgeSymbol, types.SubscribeOptions{Interval: "1m"}) + makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) } @@ -344,6 +361,8 @@ func (s *Strategy) CrossRun( s.CrossExchangeMarketMakingStrategy.ProfitStats = types.NewProfitStats(makerMarket) fixer := common.NewProfitFixer() + fixer.ConverterManager = s.ConverterManager + if ss, ok := makerSession.Exchange.(types.ExchangeTradeHistoryService); ok { log.Infof("adding makerSession %s to profitFixer", makerSession.Name) fixer.AddExchange(makerSession.Name, ss) diff --git a/pkg/types/position.go b/pkg/types/position.go index 589fa2a2be..d0b37f031a 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -19,16 +19,19 @@ const ( PositionClosed = PositionType("Closed") ) +// ExchangeFee stores the exchange fee rate type ExchangeFee struct { MakerFeeRate fixedpoint.Value TakerFeeRate fixedpoint.Value } +// PositionRisk stores the position risk data type PositionRisk struct { - Leverage fixedpoint.Value `json:"leverage"` - LiquidationPrice fixedpoint.Value `json:"liquidationPrice"` + Leverage fixedpoint.Value `json:"leverage,omitempty"` + LiquidationPrice fixedpoint.Value `json:"liquidationPrice,omitempty"` } +// Position stores the position data type Position struct { Symbol string `json:"symbol" db:"symbol"` BaseCurrency string `json:"baseCurrency" db:"base"` @@ -281,8 +284,14 @@ type FuturesPosition struct { ExchangeFeeRates map[ExchangeName]ExchangeFee `json:"exchangeFeeRates"` // Futures data fields - Isolated bool `json:"isolated"` - UpdateTime int64 `json:"updateTime"` + // ------------------- + // Isolated margin mode + Isolated bool `json:"isolated"` + + // UpdateTime is the time when the position is updated + UpdateTime int64 `json:"updateTime"` + + // PositionRisk stores the position risk data PositionRisk *PositionRisk }