Compare commits

...

12 Commits

Author SHA1 Message Date
bailantaotao
030ba5290a
Merge 5e3b0238d8 into 7d034d1ba8 2024-09-03 18:21:01 +08:00
c9s
7d034d1ba8
bbgo: add stringer method to the quota struct
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
2024-09-03 03:26:47 +08:00
c9s
7135895006
xmaker: fix MaxExposurePosition check condition 2024-09-03 03:25:37 +08:00
c9s
f12ba1adb9
bbgo: add comments to the quota methods
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
2024-09-02 22:18:13 +08:00
c9s
294e529a98
xmaker: add more logs 2024-09-02 16:08:51 +08:00
c9s
f30aca1b5a
xmaker: update position metrics when restored 2024-09-02 15:51:31 +08:00
c9s
f9b9832fff
add more logs 2024-09-02 15:51:31 +08:00
edwin
5e3b0238d8 pkg/core: move convert manager to independent file 2024-08-23 17:30:30 +08:00
edwin
bc24e8dcaa pkg/core: add more tests for convert manager 2024-08-23 17:30:30 +08:00
edwin
4fefbaf0e7 pkg/core: add currency converter 2024-08-23 17:30:30 +08:00
edwin
b683550f44 rename converter.go to symbol_converter 2024-08-23 17:30:30 +08:00
edwin
26149103f0 pkg/core: support more function to symbol converter 2024-08-23 17:30:21 +08:00
11 changed files with 730 additions and 130 deletions

View File

@ -12,12 +12,16 @@ type Quota struct {
Locked fixedpoint.Value
}
// Add adds the fund to the available quota
func (q *Quota) Add(fund fixedpoint.Value) {
q.mu.Lock()
q.Available = q.Available.Add(fund)
q.mu.Unlock()
}
// Lock locks the fund from the available quota
// returns true if the fund is locked successfully
// returns false if the fund is not enough
func (q *Quota) Lock(fund fixedpoint.Value) bool {
if fund.Compare(q.Available) > 0 {
return false
@ -31,12 +35,15 @@ func (q *Quota) Lock(fund fixedpoint.Value) bool {
return true
}
// Commit commits the locked fund
func (q *Quota) Commit() {
q.mu.Lock()
q.Locked = fixedpoint.Zero
q.mu.Unlock()
}
// Rollback rolls back the locked fund
// this will move the locked fund to the available quota
func (q *Quota) Rollback() {
q.mu.Lock()
q.Available = q.Available.Add(q.Locked)
@ -44,12 +51,21 @@ func (q *Quota) Rollback() {
q.mu.Unlock()
}
func (q *Quota) String() string {
q.mu.Lock()
defer q.mu.Unlock()
return q.Locked.String() + "/" + q.Available.String()
}
// QuotaTransaction is a transactional quota manager
type QuotaTransaction struct {
mu sync.Mutex
BaseAsset Quota
QuoteAsset Quota
}
// Commit commits the transaction
func (m *QuotaTransaction) Commit() bool {
m.mu.Lock()
m.BaseAsset.Commit()
@ -58,6 +74,7 @@ func (m *QuotaTransaction) Commit() bool {
return true
}
// Rollback rolls back the transaction
func (m *QuotaTransaction) Rollback() bool {
m.mu.Lock()
m.BaseAsset.Rollback()

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

View File

@ -426,10 +426,10 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if s.CircuitBreaker != nil {
now := time.Now()
if reason, halted := s.CircuitBreaker.IsHalted(now); halted {
s.logger.Warnf("[arbWorker] strategy is halted, reason: %s", reason)
s.logger.Warnf("strategy %s is halted, reason: %s", ID, reason)
if s.circuitBreakerAlertLimiter.AllowN(now, 1) {
bbgo.Notify("Strategy is halted, reason: %s", reason)
bbgo.Notify("Strategy %s is halted, reason: %s", ID, reason)
}
return
@ -438,6 +438,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk()
if !hasPrice {
s.logger.Warnf("no valid price, skip quoting")
return
}
@ -474,15 +475,21 @@ func (s *Strategy) updateQuote(ctx context.Context) {
// check maker's balance quota
// we load the balances from the account while we're generating the orders,
// the balance may have a chance to be deducted by other strategies or manual orders submitted by the user
makerBalances := s.makerSession.GetAccount().Balances()
makerBalances := s.makerSession.GetAccount().Balances().NotZero()
s.logger.Infof("maker balances: %+v", makerBalances)
makerQuota := &bbgo.QuotaTransaction{}
if b, ok := makerBalances[s.makerMarket.BaseCurrency]; ok {
if b.Available.Compare(s.makerMarket.MinQuantity) > 0 {
makerQuota.BaseAsset.Add(b.Available)
} else {
if s.makerMarket.IsDustQuantity(b.Available, s.lastPrice) {
disableMakerAsk = true
s.logger.Infof("%s maker ask disabled: insufficient base balance %s", s.Symbol, b.String())
} else {
makerQuota.BaseAsset.Add(b.Available)
}
} else {
disableMakerAsk = true
s.logger.Infof("%s maker ask disabled: base balance %s not found", s.Symbol, b.String())
}
if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok {
@ -492,8 +499,13 @@ func (s *Strategy) updateQuote(ctx context.Context) {
disableMakerBid = true
s.logger.Infof("%s maker bid disabled: insufficient quote balance %s", s.Symbol, b.String())
}
} else {
disableMakerBid = true
s.logger.Infof("%s maker bid disabled: quote balance %s not found", s.Symbol, b.String())
}
s.logger.Infof("maker quota: %+v", makerQuota)
// if
// 1) the source session is a margin session
// 2) the min margin level is configured
@ -511,6 +523,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
hedgeAccount.MarginLevel.String(),
s.MinMarginLevel.String())
// TODO: should consider base asset debt as well.
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
quoteDebt := quote.Debt()
if quoteDebt.Sign() > 0 {
@ -611,12 +624,14 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if s.MaxExposurePosition.Sign() > 0 {
pos := s.Position.GetBase()
if pos.Compare(s.MaxExposurePosition.Neg()) > 0 {
if pos.Compare(s.MaxExposurePosition.Neg()) <= 0 {
// stop sell if we over-sell
disableMakerAsk = true
} else if pos.Compare(s.MaxExposurePosition) > 0 {
s.logger.Warnf("%s ask maker is disabled: %f exceeded max exposure %f", s.Symbol, pos.Float64(), s.MaxExposurePosition.Float64())
} else if pos.Compare(s.MaxExposurePosition) >= 0 {
// stop buy if we over buy
disableMakerBid = true
s.logger.Warnf("%s bid maker is disabled: %f exceeded max exposure %f", s.Symbol, pos.Float64(), s.MaxExposurePosition.Float64())
}
}
@ -840,6 +855,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, orderCreateCallback, formattedOrders...)
if err != nil {
log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders)
return
}
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
@ -1240,7 +1256,7 @@ func (s *Strategy) CrossRun(
// restore state
s.groupID = util.FNV32(instanceID)
log.Infof("using group id %d from fnv(%s)", s.groupID, 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))
@ -1252,8 +1268,12 @@ func (s *Strategy) CrossRun(
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.UpdateMetrics()
bbgo.Notify("xmaker: %s position is restored", s.Symbol, s.Position)
if s.ProfitStats == nil {

View File

@ -656,6 +656,12 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp
return fixedpoint.Zero, fixedpoint.Zero, false
}
func (p *Position) UpdateMetrics() {
p.Lock()
p.updateMetrics()
p.Unlock()
}
func (p *Position) updateMetrics() {
// update the position metrics only if the position defines the strategy ID
if p.StrategyInstanceID == "" || p.Strategy == "" {