This commit is contained in:
bailantaotao 2024-09-03 18:21:01 +08:00 committed by GitHub
commit 030ba5290a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 678 additions and 121 deletions

153
pkg/core/convert_manager.go Normal file
View File

@ -0,0 +1,153 @@
package core
import (
"github.com/c9s/bbgo/pkg/types"
"github.com/sirupsen/logrus"
)
type ConverterSetting struct {
SymbolConverter *SymbolConverter `json:"symbolConverter" yaml:"symbolConverter"`
CurrencyConverter *CurrencyConverter `json:"currencyConverter" yaml:"currencyConverter"`
}
func (s *ConverterSetting) getConverter() Converter {
if s.SymbolConverter != nil {
return s.SymbolConverter
}
if s.CurrencyConverter != nil {
return s.CurrencyConverter
}
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 converter, err
}
// ConverterManager manages the converters for trade conversion
// It can be used to convert the trade symbol into the target symbol, or convert the price, volume into different units.
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
}
if converter != nil {
c.AddConverter(converter)
}
}
numConverters := len(c.converters)
logrus.Infof("%d converters loaded", numConverters)
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
}
func (c *ConverterManager) ConvertKLine(kline types.KLine) types.KLine {
if len(c.converters) == 0 {
return kline
}
for _, converter := range c.converters {
convKline, err := converter.ConvertKLine(kline)
if err != nil {
logrus.WithError(err).Errorf("converter %+v error, kline: %s", converter, kline.String())
continue
}
kline = convKline
}
return kline
}
func (c *ConverterManager) ConvertMarket(market types.Market) types.Market {
if len(c.converters) == 0 {
return market
}
for _, converter := range c.converters {
convMarket, err := converter.ConvertMarket(market)
if err != nil {
logrus.WithError(err).Errorf("converter %+v error, market: %+v", converter, market)
continue
}
market = convMarket
}
return market
}
func (c *ConverterManager) ConvertBalance(balance types.Balance) types.Balance {
if len(c.converters) == 0 {
return balance
}
for _, converter := range c.converters {
convBal, err := converter.ConvertBalance(balance)
if err != nil {
logrus.WithError(err).Errorf("converter %+v error, balance: %s", converter, balance.String())
continue
}
balance = convBal
}
return balance
}

View File

@ -0,0 +1,212 @@
package core
import (
"encoding/json"
"testing"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestInitializeConverter_ValidSymbolConverter(t *testing.T) {
setting := ConverterSetting{
SymbolConverter: &SymbolConverter{
FromSymbol: "MAXEXCHANGEUSDT",
ToSymbol: "MAXUSDT",
},
}
converter, err := setting.InitializeConverter()
assert.NoError(t, err)
assert.NotNil(t, converter)
}
func TestInitializeConverter_ValidCurrencyConverter(t *testing.T) {
setting := ConverterSetting{
CurrencyConverter: &CurrencyConverter{
FromCurrency: "MAXEXCHANGE",
ToCurrency: "MAX",
},
}
converter, err := setting.InitializeConverter()
assert.NoError(t, err)
assert.NotNil(t, converter)
}
func TestInitializeConverter_NoConverter(t *testing.T) {
setting := ConverterSetting{}
converter, err := setting.InitializeConverter()
assert.NoError(t, err)
assert.Nil(t, converter)
}
func TestInitialize_ValidConverters(t *testing.T) {
manager := ConverterManager{
ConverterSettings: []ConverterSetting{
{SymbolConverter: &SymbolConverter{
FromSymbol: "MAXEXCHANGEUSDT",
ToSymbol: "MAXUSDT",
}},
{CurrencyConverter: &CurrencyConverter{
FromCurrency: "MAXEXCHANGE",
ToCurrency: "MAX",
}},
},
}
err := manager.Initialize()
assert.NoError(t, err)
assert.Equal(t, 2, len(manager.converters))
}
func TestInitialize_NoConverters(t *testing.T) {
manager := ConverterManager{}
err := manager.Initialize()
assert.NoError(t, err)
assert.Equal(t, 0, len(manager.converters))
}
func TestConvertOrder_WithConverters(t *testing.T) {
jsonStr := `
{
"converters": [
{
"symbolConverter": {
"from": "MAXEXCHANGEUSDT",
"to": "MAXUSDT"
}
},
{
"currencyConverter": {
"from": "MAXEXCHANGE",
"to": "MAX"
}
}
]
}
`
manager := ConverterManager{}
err := json.Unmarshal([]byte(jsonStr), &manager)
assert.NoError(t, err)
order := types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "MAXEXCHANGEUSDT",
Market: types.Market{
Symbol: "MAXEXCHANGEUSDT",
QuoteCurrency: "USDT",
BaseCurrency: "MAXEXCHANGE",
},
},
}
err = manager.Initialize()
assert.NoError(t, err)
convertedOrder := manager.ConvertOrder(order)
assert.Equal(t, "MAXUSDT", convertedOrder.Symbol)
assert.Equal(t, "MAX", convertedOrder.Market.BaseCurrency)
assert.Equal(t, "USDT", convertedOrder.Market.QuoteCurrency)
assert.Equal(t, "MAXUSDT", convertedOrder.Market.Symbol)
}
func TestConvertOrder_NoConverters(t *testing.T) {
manager := ConverterManager{}
order := types.Order{}
err := manager.Initialize()
assert.NoError(t, err)
convertedOrder := manager.ConvertOrder(order)
assert.Equal(t, order, convertedOrder)
}
func TestConvertTrade_WithConverters(t *testing.T) {
manager := ConverterManager{}
converter := &CurrencyConverter{
FromCurrency: "MAXEXCHANGE",
ToCurrency: "MAX",
}
err := manager.Initialize()
assert.NoError(t, err)
manager.AddConverter(converter)
trade := types.Trade{}
convertedTrade := manager.ConvertTrade(trade)
assert.Equal(t, trade, convertedTrade)
}
func TestConvertTrade_NoConverters(t *testing.T) {
manager := ConverterManager{}
trade := types.Trade{}
err := manager.Initialize()
assert.NoError(t, err)
convertedTrade := manager.ConvertTrade(trade)
assert.Equal(t, trade, convertedTrade)
}
func TestConvertKLine_WithConverters(t *testing.T) {
manager := ConverterManager{}
converter := &CurrencyConverter{
FromCurrency: "MAXEXCHANGE",
ToCurrency: "MAX",
}
err := manager.Initialize()
assert.NoError(t, err)
manager.AddConverter(converter)
kline := types.KLine{}
convertedKline := manager.ConvertKLine(kline)
assert.Equal(t, kline, convertedKline)
}
func TestConvertKLine_NoConverters(t *testing.T) {
manager := ConverterManager{}
kline := types.KLine{}
err := manager.Initialize()
assert.NoError(t, err)
convertedKline := manager.ConvertKLine(kline)
assert.Equal(t, kline, convertedKline)
}
func TestConvertMarket_WithConverters(t *testing.T) {
manager := ConverterManager{}
converter := &CurrencyConverter{
FromCurrency: "MAXEXCHANGE",
ToCurrency: "MAX",
}
err := manager.Initialize()
assert.NoError(t, err)
manager.AddConverter(converter)
market := types.Market{}
convertedMarket := manager.ConvertMarket(market)
assert.Equal(t, market, convertedMarket)
}
func TestConvertMarket_NoConverters(t *testing.T) {
manager := ConverterManager{}
market := types.Market{}
err := manager.Initialize()
assert.NoError(t, err)
convertedMarket := manager.ConvertMarket(market)
assert.Equal(t, market, convertedMarket)
}
func TestConvertBalance_WithConverters(t *testing.T) {
manager := ConverterManager{}
converter := &CurrencyConverter{
FromCurrency: "MAXEXCHANGE",
ToCurrency: "MAX",
}
err := manager.Initialize()
assert.NoError(t, err)
manager.AddConverter(converter)
balance := types.Balance{}
convertedBalance := manager.ConvertBalance(balance)
assert.Equal(t, balance, convertedBalance)
}
func TestConvertBalance_NoConverters(t *testing.T) {
manager := ConverterManager{}
balance := types.Balance{}
err := manager.Initialize()
assert.NoError(t, err)
convertedBalance := manager.ConvertBalance(balance)
assert.Equal(t, balance, convertedBalance)
}

View File

@ -1,31 +0,0 @@
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)
}
}

View File

@ -0,0 +1,68 @@
package core
import (
"errors"
"github.com/c9s/bbgo/pkg/types"
)
type CurrencyConverter struct {
FromCurrency string `json:"from"`
ToCurrency string `json:"to"`
}
func NewCurrencyConverter(fromSymbol, toSymbol string) *CurrencyConverter {
return &CurrencyConverter{FromCurrency: fromSymbol, ToCurrency: toSymbol}
}
func (c *CurrencyConverter) Initialize() error {
if c.FromCurrency == "" {
return errors.New("FromCurrency can not be empty")
}
if c.ToCurrency == "" {
return errors.New("ToCurrency can not be empty")
}
return nil
}
func (c *CurrencyConverter) ConvertOrder(order types.Order) (types.Order, error) {
if order.SubmitOrder.Market.QuoteCurrency == c.FromCurrency {
order.SubmitOrder.Market.QuoteCurrency = c.ToCurrency
}
if order.SubmitOrder.Market.BaseCurrency == c.FromCurrency {
order.SubmitOrder.Market.BaseCurrency = c.ToCurrency
}
return order, nil
}
func (c *CurrencyConverter) ConvertTrade(trade types.Trade) (types.Trade, error) {
if trade.FeeCurrency == c.FromCurrency {
trade.FeeCurrency = c.ToCurrency
}
return trade, nil
}
func (c *CurrencyConverter) ConvertKLine(kline types.KLine) (types.KLine, error) {
return kline, nil
}
func (c *CurrencyConverter) ConvertMarket(mkt types.Market) (types.Market, error) {
if mkt.QuoteCurrency == c.FromCurrency {
mkt.QuoteCurrency = c.ToCurrency
}
if mkt.BaseCurrency == c.FromCurrency {
mkt.BaseCurrency = c.ToCurrency
}
return mkt, nil
}
func (c *CurrencyConverter) ConvertBalance(balance types.Balance) (types.Balance, error) {
if balance.Currency == c.FromCurrency {
balance.Currency = c.ToCurrency
}
return balance, nil
}

View File

@ -0,0 +1,124 @@
package core
import (
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
"testing"
)
// pkg/core/tradecollector_test.go
func TestInitialize_ValidCurrencies(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
err := converter.Initialize()
assert.NoError(t, err)
}
func TestInitialize_EmptyFromCurrency(t *testing.T) {
converter := NewCurrencyConverter("", "MAX")
err := converter.Initialize()
assert.Error(t, err)
assert.Equal(t, "FromCurrency can not be empty", err.Error())
}
func TestInitialize_EmptyToCurrency(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "")
err := converter.Initialize()
assert.Error(t, err)
assert.Equal(t, "ToCurrency can not be empty", err.Error())
}
func TestConvertOrder_ValidConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
order := types.Order{
SubmitOrder: types.SubmitOrder{
Market: types.Market{
QuoteCurrency: "MAXEXCHANGE",
BaseCurrency: "MAXEXCHANGE",
},
},
}
convertedOrder, err := converter.ConvertOrder(order)
assert.NoError(t, err)
assert.Equal(t, "MAX", convertedOrder.SubmitOrder.Market.QuoteCurrency)
assert.Equal(t, "MAX", convertedOrder.SubmitOrder.Market.BaseCurrency)
}
func TestConvertOrder_NoConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
order := types.Order{
SubmitOrder: types.SubmitOrder{
Market: types.Market{
QuoteCurrency: "JPY",
BaseCurrency: "JPY",
},
},
}
convertedOrder, err := converter.ConvertOrder(order)
assert.NoError(t, err)
assert.Equal(t, "JPY", convertedOrder.SubmitOrder.Market.QuoteCurrency)
assert.Equal(t, "JPY", convertedOrder.SubmitOrder.Market.BaseCurrency)
}
func TestConvertTrade_ValidConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
trade := types.Trade{
FeeCurrency: "MAXEXCHANGE",
}
convertedTrade, err := converter.ConvertTrade(trade)
assert.NoError(t, err)
assert.Equal(t, "MAX", convertedTrade.FeeCurrency)
}
func TestConvertTrade_NoConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
trade := types.Trade{
FeeCurrency: "JPY",
}
convertedTrade, err := converter.ConvertTrade(trade)
assert.NoError(t, err)
assert.Equal(t, "JPY", convertedTrade.FeeCurrency)
}
func TestConvertMarket_ValidConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
market := types.Market{
QuoteCurrency: "MAXEXCHANGE",
BaseCurrency: "MAXEXCHANGE",
}
convertedMarket, err := converter.ConvertMarket(market)
assert.NoError(t, err)
assert.Equal(t, "MAX", convertedMarket.QuoteCurrency)
assert.Equal(t, "MAX", convertedMarket.BaseCurrency)
}
func TestConvertMarket_NoConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
market := types.Market{
QuoteCurrency: "JPY",
BaseCurrency: "JPY",
}
convertedMarket, err := converter.ConvertMarket(market)
assert.NoError(t, err)
assert.Equal(t, "JPY", convertedMarket.QuoteCurrency)
assert.Equal(t, "JPY", convertedMarket.BaseCurrency)
}
func TestConvertBalance_ValidConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
balance := types.Balance{
Currency: "MAXEXCHANGE",
}
convertedBalance, err := converter.ConvertBalance(balance)
assert.NoError(t, err)
assert.Equal(t, "MAX", convertedBalance.Currency)
}
func TestConvertBalance_NoConversion(t *testing.T) {
converter := NewCurrencyConverter("MAXEXCHANGE", "MAX")
balance := types.Balance{
Currency: "JPY",
}
convertedBalance, err := converter.ConvertBalance(balance)
assert.NoError(t, err)
assert.Equal(t, "JPY", convertedBalance.Currency)
}

View File

@ -9,6 +9,9 @@ import (
type Converter interface {
OrderConverter
TradeConverter
KLineConverter
MarketConverter
BalanceConverter
Initialize() error
}
@ -22,12 +25,33 @@ type TradeConverter interface {
ConvertTrade(trade types.Trade) (types.Trade, error)
}
// KLineConverter converts the kline to another kline
type KLineConverter interface {
ConvertKLine(kline types.KLine) (types.KLine, error)
}
// MarketConverter converts the market to another market
type MarketConverter interface {
ConvertMarket(market types.Market) (types.Market, error)
}
// BalanceConverter converts the balance to another balance
type BalanceConverter interface {
ConvertBalance(balance types.Balance) (types.Balance, error)
}
type OrderConvertFunc func(order types.Order) (types.Order, error)
type TradeConvertFunc func(trade types.Trade) (types.Trade, error)
type KLineConvertFunc func(kline types.KLine) (types.KLine, error)
type MarketConvertFunc func(market types.Market) (types.Market, error)
type BalanceConvertFunc func(balance types.Balance) (types.Balance, error)
type DynamicConverter struct {
orderConverter OrderConvertFunc
tradeConverter TradeConvertFunc
orderConverter OrderConvertFunc
tradeConverter TradeConvertFunc
klineConverter KLineConvertFunc
marketConverter MarketConvertFunc
balanceConverter BalanceConvertFunc
}
func NewDynamicConverter(orderConverter OrderConvertFunc, tradeConverter TradeConvertFunc) *DynamicConverter {
@ -46,6 +70,18 @@ func (c *DynamicConverter) ConvertTrade(trade types.Trade) (types.Trade, error)
return c.tradeConverter(trade)
}
func (c *DynamicConverter) ConvertKLine(kline types.KLine) (types.KLine, error) {
return c.klineConverter(kline)
}
func (c *DynamicConverter) ConvertMarket(market types.Market) (types.Market, error) {
return c.marketConverter(market)
}
func (c *DynamicConverter) ConvertBalance(balance types.Balance) (types.Balance, error) {
return c.balanceConverter(balance)
}
// SymbolConverter converts the symbol to another symbol
type SymbolConverter struct {
FromSymbol string `json:"from"`
@ -73,6 +109,10 @@ func (c *SymbolConverter) ConvertOrder(order types.Order) (types.Order, error) {
order.Symbol = c.ToSymbol
}
if order.SubmitOrder.Market.Symbol == c.FromSymbol {
order.SubmitOrder.Market.Symbol = c.ToSymbol
}
return order, nil
}
@ -83,3 +123,22 @@ func (c *SymbolConverter) ConvertTrade(trade types.Trade) (types.Trade, error) {
return trade, nil
}
func (c *SymbolConverter) ConvertKLine(kline types.KLine) (types.KLine, error) {
if kline.Symbol == c.FromSymbol {
kline.Symbol = c.ToSymbol
}
return kline, nil
}
func (s *SymbolConverter) ConvertMarket(mkt types.Market) (types.Market, error) {
if mkt.Symbol == s.FromSymbol {
mkt.Symbol = s.ToSymbol
}
return mkt, nil
}
func (c *SymbolConverter) ConvertBalance(balance types.Balance) (types.Balance, error) {
return balance, nil
}

View File

@ -0,0 +1,60 @@
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",
Market: types.Market{
Symbol: "MAXEXCHANGEUSDT",
},
},
})
if assert.NoError(t, err) {
assert.Equal(t, "MAXUSDT", order.Symbol)
assert.Equal(t, "MAXUSDT", order.SubmitOrder.Symbol)
assert.Equal(t, "MAXUSDT", order.SubmitOrder.Market.Symbol)
}
kline, err := converter.ConvertKLine(types.KLine{
Symbol: "MAXEXCHANGEUSDT",
})
if assert.NoError(t, err) {
assert.Equal(t, "MAXUSDT", kline.Symbol)
}
market, err := converter.ConvertMarket(types.Market{
Symbol: "MAXEXCHANGEUSDT",
})
if assert.NoError(t, err) {
assert.Equal(t, "MAXUSDT", market.Symbol)
}
balance, err := converter.ConvertBalance(types.Balance{
Currency: "MAXEXCHANGE",
})
if assert.NoError(t, err) {
assert.Equal(t, "MAXEXCHANGE", balance.Currency)
}
}

View File

@ -12,94 +12,6 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
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 converter, err
}
// ConverterManager manages the converters for trade conversion
// It can be used to convert the trade symbol into the target symbol, or convert the price, volume into different units.
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
}
if converter != nil {
c.AddConverter(converter)
}
}
numConverters := len(c.converters)
logrus.Infof("%d converters loaded", numConverters)
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
type TradeCollector struct {
Symbol string