mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Compare commits
12 Commits
24bf561026
...
030ba5290a
Author | SHA1 | Date | |
---|---|---|---|
|
030ba5290a | ||
|
7d034d1ba8 | ||
|
7135895006 | ||
|
f12ba1adb9 | ||
|
294e529a98 | ||
|
f30aca1b5a | ||
|
f9b9832fff | ||
|
5e3b0238d8 | ||
|
bc24e8dcaa | ||
|
4fefbaf0e7 | ||
|
b683550f44 | ||
|
26149103f0 |
|
@ -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
153
pkg/core/convert_manager.go
Normal 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
|
||||
}
|
212
pkg/core/convert_manager_test.go
Normal file
212
pkg/core/convert_manager_test.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
68
pkg/core/currency_converter.go
Normal file
68
pkg/core/currency_converter.go
Normal 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
|
||||
}
|
124
pkg/core/currency_converter_test.go
Normal file
124
pkg/core/currency_converter_test.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
60
pkg/core/symbol_converter_test.go
Normal file
60
pkg/core/symbol_converter_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
Loading…
Reference in New Issue
Block a user