mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
all: moving common strategy functionality to strategy/base
This commit is contained in:
parent
0891859b98
commit
62d394d183
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
58
pkg/strategy/base/strategy.go
Normal file
58
pkg/strategy/base/strategy.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user