all: moving common strategy functionality to strategy/base

This commit is contained in:
c9s 2023-07-09 15:47:22 +08:00
parent 0891859b98
commit 62d394d183
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
4 changed files with 109 additions and 55 deletions

View File

@ -61,7 +61,7 @@ func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator)
st := reflect.TypeOf(obj) st := reflect.TypeOf(obj)
if st.Kind() != reflect.Ptr { if st.Kind() != reflect.Ptr {
return fmt.Errorf("f should be a pointer of a struct, %s given", st) return fmt.Errorf("obj should be a pointer of a struct, %s given", st)
} }
// for pointer, check if it's nil // for pointer, check if it's nil

View File

@ -8,6 +8,16 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type TestEmbedded struct {
Foo int `persistence:"foo"`
Bar int `persistence:"bar"`
}
type TestA struct {
*TestEmbedded
Outer int `persistence:"outer"`
}
func TestIterateFields(t *testing.T) { func TestIterateFields(t *testing.T) {
t.Run("basic", func(t *testing.T) { t.Run("basic", func(t *testing.T) {
@ -100,4 +110,22 @@ func TestIterateFieldsByTag(t *testing.T) {
assert.Equal(t, 2, cnt) assert.Equal(t, 2, cnt)
assert.Equal(t, []string{"a", "b"}, collectedTags) assert.Equal(t, []string{"a", "b"}, collectedTags)
}) })
t.Run("embedded", func(t *testing.T) {
a := &TestA{
TestEmbedded: &TestEmbedded{Foo: 1, Bar: 2},
Outer: 3,
}
collectedTags := []string{}
cnt := 0
err := IterateFieldsByTag(a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error {
cnt++
collectedTags = append(collectedTags, tag)
return nil
})
assert.NoError(t, err)
assert.Equal(t, 3, cnt)
assert.Equal(t, []string{"foo", "bar", "outer"}, collectedTags)
})
} }

View File

@ -0,0 +1,58 @@
package base
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types"
)
// LongShortStrategy provides the core functionality that is required by a long/short strategy.
type LongShortStrategy struct {
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
parent, ctx context.Context
cancel context.CancelFunc
Environ *bbgo.Environment
Session *bbgo.ExchangeSession
OrderExecutor *bbgo.GeneralOrderExecutor
}
func (s *LongShortStrategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) {
s.parent = ctx
s.ctx, s.cancel = context.WithCancel(ctx)
s.Environ = environ
s.Session = session
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(market)
}
if s.Position == nil {
s.Position = types.NewPositionFromMarket(market)
}
// Always update the position fields
s.Position.Strategy = strategyID
s.Position.StrategyInstanceID = instanceID
// if anyone of the fee rate is defined, this assumes that both are defined.
// so that zero maker fee could be applied
if session.MakerFeeRate.Sign() > 0 || session.TakerFeeRate.Sign() > 0 {
s.Position.SetExchangeFeeRate(session.ExchangeName, types.ExchangeFee{
MakerFeeRate: session.MakerFeeRate,
TakerFeeRate: session.TakerFeeRate,
})
}
s.OrderExecutor = bbgo.NewGeneralOrderExecutor(session, market.Symbol, strategyID, instanceID, s.Position)
s.OrderExecutor.BindEnvironment(environ)
s.OrderExecutor.BindProfitStats(s.ProfitStats)
s.OrderExecutor.Bind()
s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
})
}

View File

@ -12,6 +12,7 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/risk/riskcontrol"
"github.com/c9s/bbgo/pkg/strategy/base"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -36,6 +37,8 @@ func init() {
// Strategy scmaker is a stable coin market maker // Strategy scmaker is a stable coin market maker
type Strategy struct { type Strategy struct {
*base.LongShortStrategy
Environment *bbgo.Environment Environment *bbgo.Environment
Market types.Market Market types.Market
@ -64,11 +67,6 @@ type Strategy struct {
CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"`
CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"`
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook
book *types.StreamOrderBook book *types.StreamOrderBook
@ -102,9 +100,9 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
} }
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
instanceID := s.InstanceID() s.LongShortStrategy = &base.LongShortStrategy{}
s.LongShortStrategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
s.session = session
s.book = types.NewStreamBook(s.Symbol) s.book = types.NewStreamBook(s.Symbol)
s.book.BindStream(session.UserDataStream) s.book.BindStream(session.UserDataStream)
@ -114,39 +112,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
s.adjustmentOrderBook.BindStream(session.UserDataStream) s.adjustmentOrderBook.BindStream(session.UserDataStream)
// If position is nil, we need to allocate a new position for calculation
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
// Always update the position fields
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
// if anyone of the fee rate is defined, this assumes that both are defined.
// so that zero maker fee could be applied
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
MakerFeeRate: s.session.MakerFeeRate,
TakerFeeRate: s.session.TakerFeeRate,
})
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.Bind()
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
})
if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() {
log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...")
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.orderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity)
} }
if !s.CircuitBreakLossThreshold.IsZero() { if !s.CircuitBreakLossThreshold.IsZero() {
@ -194,10 +162,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange)
logErr(err, "unable to cancel liquidity orders") logErr(err, "unable to cancel liquidity orders")
err = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) err = s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange)
logErr(err, "unable to cancel adjustment orders") logErr(err, "unable to cancel adjustment orders")
}) })
@ -237,24 +205,24 @@ func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession)
} }
func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
_ = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) _ = s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange)
if s.Position.IsDust() { if s.Position.IsDust() {
return return
} }
ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol)
if logErr(err, "unable to query ticker") { if logErr(err, "unable to query ticker") {
return return
} }
if _, err := s.session.UpdateAccount(ctx); err != nil { if _, err := s.Session.UpdateAccount(ctx); err != nil {
logErr(err, "unable to update account") logErr(err, "unable to update account")
return return
} }
baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency)
quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency)
var adjOrders []types.SubmitOrder var adjOrders []types.SubmitOrder
@ -262,7 +230,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
tickSize := s.Market.TickSize tickSize := s.Market.TickSize
if s.Position.IsShort() { if s.Position.IsShort() {
price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.session.MakerFeeRate, s.MinProfit) price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.Session.MakerFeeRate, s.MinProfit)
quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available)
bidQuantity := quoteQuantity.Div(price) bidQuantity := quoteQuantity.Div(price)
@ -280,7 +248,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
TimeInForce: types.TimeInForceGTC, TimeInForce: types.TimeInForceGTC,
}) })
} else if s.Position.IsLong() { } else if s.Position.IsLong() {
price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.session.MakerFeeRate, s.MinProfit) price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.Session.MakerFeeRate, s.MinProfit)
askQuantity := fixedpoint.Min(posSize, baseBal.Available) askQuantity := fixedpoint.Min(posSize, baseBal.Available)
if s.Market.IsDustQuantity(askQuantity, price) { if s.Market.IsDustQuantity(askQuantity, price) {
@ -298,7 +266,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
}) })
} }
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, adjOrders...) createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, adjOrders...)
if logErr(err, "unable to place liquidity orders") { if logErr(err, "unable to place liquidity orders") {
return return
} }
@ -312,12 +280,12 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
return return
} }
err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange)
if logErr(err, "unable to cancel orders") { if logErr(err, "unable to cancel orders") {
return return
} }
ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol)
if logErr(err, "unable to query ticker") { if logErr(err, "unable to query ticker") {
return return
} }
@ -331,13 +299,13 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
ticker.Sell = ticker.Buy.Add(s.Market.TickSize) ticker.Sell = ticker.Buy.Add(s.Market.TickSize)
} }
if _, err := s.session.UpdateAccount(ctx); err != nil { if _, err := s.Session.UpdateAccount(ctx); err != nil {
logErr(err, "unable to update account") logErr(err, "unable to update account")
return return
} }
baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency)
quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency)
spread := ticker.Sell.Sub(ticker.Buy) spread := ticker.Sell.Sub(ticker.Buy)
tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize) tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize)
@ -497,7 +465,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
makerQuota.Commit() makerQuota.Commit()
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, liqOrders...) createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, liqOrders...)
if logErr(err, "unable to place liquidity orders") { if logErr(err, "unable to place liquidity orders") {
return return
} }