Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: improve: bollmaker: position stack #641

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 33 additions & 24 deletions config/bollmaker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ backtest:
# see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2022-01-01"
endTime: "2022-05-12"
endTime: "2022-05-31"
sessions:
- binance
symbols:
Expand All @@ -38,12 +38,21 @@ exchangeStrategies:
interval: 1m

# quantity is the base order quantity for your buy/sell order.
quantity: 0.05
# quantity: 0.05
amount: 20

# Position Stack, with longer stack length, may need more capital.
# Push position in stack is initiating a position to calculate base, average cost, etc.
# Pop position in stack is loading a previous position back.
positionStack:
enabled: true
pushThreshold: 25%
popThreshold: 5%

# useTickerPrice use the ticker api to get the mid price instead of the closed kline price.
# The back-test engine is kline-based, so the ticker price api is not supported.
# Turn this on if you want to do real trading.
useTickerPrice: true
useTickerPrice: false

# spread is the price spread from the middle price.
# For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread))
Expand Down Expand Up @@ -103,7 +112,7 @@ exchangeStrategies:
domain: [ -1, 1 ]
# when in down band, holds 1.0 by maximum
# when in up band, holds 0.05 by maximum
range: [ 10.0, 1.0 ]
range: [ 3.0, 0.5]

# DisableShort means you can don't want short position during the market making
# THe short here means you might sell some of your existing inventory.
Expand Down Expand Up @@ -136,25 +145,25 @@ exchangeStrategies:
# Set up your stop order, this is optional
# sometimes the stop order might decrease your total profit.
# you can setup multiple stop,
stops:
# stops:
# use trailing stop order
- trailingStop:
# callbackRate: when the price reaches -1% from the previous highest, we trigger the stop
callbackRate: 5.1%

# closePosition is how much position do you want to close
closePosition: 20%

# minProfit is how much profit you want to take.
# if you set this option, your stop will only be triggered above the average cost.
minProfit: 5%

# interval is the time interval for checking your stop
interval: 1m

# virtual means we don't place a a REAL stop order
# when virtual is on
# the strategy won't place a REAL stop order, instead if watches the close price,
# and if the condition matches, it submits a market order to close your position.
virtual: true
# - trailingStop:
# # callbackRate: when the price reaches -1% from the previous highest, we trigger the stop
# callbackRate: 5.1%
#
# # closePosition is how much position do you want to close
# closePosition: 20%
#
# # minProfit is how much profit you want to take.
# # if you set this option, your stop will only be triggered above the average cost.
# minProfit: 5%
#
# # interval is the time interval for checking your stop
# interval: 1m
#
# # virtual means we don't place a a REAL stop order
# # when virtual is on
# # the strategy won't place a REAL stop order, instead if watches the close price,
# # and if the condition matches, it submits a market order to close your position.
# virtual: true

6 changes: 3 additions & 3 deletions pkg/bbgo/smart_stops.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ func (c *TrailingStopController) Subscribe(session *ExchangeSession) {

func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) {
// store the position
c.position = tradeCollector.Position()
c.position = tradeCollector.Position().(*types.Position)
c.averageCost = c.position.AverageCost

// Use trade collector to get the position update event
tradeCollector.OnPositionUpdate(func(position *types.Position) {
tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
// update average cost if we have it.
c.averageCost = position.AverageCost
c.averageCost = position.(*types.Position).AverageCost
})

session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
Expand Down
8 changes: 4 additions & 4 deletions pkg/bbgo/tradecollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ type TradeCollector struct {

tradeStore *TradeStore
tradeC chan types.Trade
position *types.Position
position types.AnyPosition
orderStore *OrderStore
doneTrades map[types.TradeKey]struct{}

recoverCallbacks []func(trade types.Trade)
tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value)
positionUpdateCallbacks []func(position *types.Position)
positionUpdateCallbacks []func(position types.AnyPosition)
profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value)
}

func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector {
func NewTradeCollector(symbol string, position types.AnyPosition, orderStore *OrderStore) *TradeCollector {
return &TradeCollector{
Symbol: symbol,
orderSig: sigchan.New(1),
Expand All @@ -47,7 +47,7 @@ func (c *TradeCollector) OrderStore() *OrderStore {
}

// Position returns the position used by the trade collector
func (c *TradeCollector) Position() *types.Position {
func (c *TradeCollector) Position() types.AnyPosition {
return c.position
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/bbgo/tradecollector_callbacks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 42 additions & 9 deletions pkg/strategy/bollmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ type State struct {
ProfitStats types.ProfitStats `json:"profitStats,omitempty"`
}

type PositionStack struct {
Enabled bool `json:"enabled,omitempty"`
PushThreshold fixedpoint.Value `json:"pushThreshold,omitempty"`
PopThreshold fixedpoint.Value `json:"popThreshold,omitempty"`
}

type BollingerSetting struct {
types.IntervalWindow
BandWidth float64 `json:"bandWidth"`
Expand Down Expand Up @@ -225,11 +231,12 @@ type Strategy struct {
session *bbgo.ExchangeSession
book *types.StreamOrderBook

state *State
state *State
PositionStack PositionStack

// persistence fields
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
Position *types.PositionStack `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`

activeMakerOrders *bbgo.LocalActiveOrderBook
orderStore *bbgo.OrderStore
Expand Down Expand Up @@ -289,7 +296,7 @@ func (s *Strategy) Validate() error {
return nil
}

func (s *Strategy) CurrentPosition() *types.Position {
func (s *Strategy) CurrentPosition() *types.PositionStack {
return s.Position
}

Expand Down Expand Up @@ -652,9 +659,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
if s.Position == nil {
// fallback to the legacy position struct in the state
if s.state != nil && s.state.Position != nil {
s.Position = s.state.Position
s.Position.Position = s.state.Position
} else {
s.Position = types.NewPositionFromMarket(s.Market)
s.Position = types.NewPositionStackFromMarket(s.Market)
}
}

Expand Down Expand Up @@ -699,7 +706,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.ProfitStats.AddTrade(trade)

if profit.Compare(fixedpoint.Zero) == 0 {
s.Environment.RecordPosition(s.Position, trade, nil)
s.Environment.RecordPosition(s.Position.Position, trade, nil)
} else {
log.Infof("%s generated profit: %v", s.Symbol, profit)
p := s.Position.NewProfit(trade, profit, netProfit)
Expand All @@ -710,11 +717,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.ProfitStats.AddProfit(p)
s.Notify(&s.ProfitStats)

s.Environment.RecordPosition(s.Position, trade, &p)
s.Environment.RecordPosition(s.Position.Position, trade, &p)
}
})

s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})
Expand Down Expand Up @@ -773,6 +780,32 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.WithError(err).Errorf("graceful cancel order error")
}

//log.Error(len(s.Position.Stack), s.Position.AverageCost, kline.Close)
if s.Position.Position.AverageCost.Div(kline.Close).Compare(fixedpoint.One.Add(s.PositionStack.PushThreshold)) > 0 {
log.Infof("push position %s", s.Position)
s.Position = s.Position.Push(types.NewPositionFromMarket(s.Market))
}
// make it dust naturally by bollmaker
if len(s.Position.Stack) > 1 && s.Position.Stack[len(s.Position.Stack)-2].AverageCost.Compare(kline.Close) < 0 && s.Market.IsDustQuantity(s.Position.GetBase(), kline.Close) {
log.Infof("pop position %s", s.Position)
s.Position = s.Position.Pop()
}
// make it dust by TP
if !s.PositionStack.PopThreshold.IsZero() {
if len(s.Position.Stack) > 1 && s.Position.Stack[len(s.Position.Stack)-2].AverageCost.Compare(kline.Close) < 0 && s.Position.AverageCost.Div(kline.Close).Compare(fixedpoint.One.Sub(s.PositionStack.PopThreshold)) < 0 {
s.ClosePosition(ctx, fixedpoint.One)
log.Infof("pop position %s", s.Position)
log.Error("pop position")
s.Position = s.Position.Pop()
}
}

//if s.Position.AverageCost.Div(kline.Close).Compare(fixedpoint.One.Sub(s.PopThreshold)) < 0 && && !s.Position.AverageCost.IsZero() {
// //log.Error(len(s.Position.Stack), s.Position.AverageCost, kline.Close)
// log.Errorf("pop")
// s.ClosePosition(ctx, fixedpoint.One)
// s.Position = s.Position.Pop()
//}
// check if there is a canceled order had partially filled.
s.tradeCollector.Process()

Expand Down
2 changes: 1 addition & 1 deletion pkg/strategy/ewoDgtrd/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})

s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", position)
s.Notify(s.Position)
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/strategy/grid/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
*/

s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
s.Notifiability.Notify(position)
})
s.tradeCollector.BindStream(session.UserDataStream)
Expand Down
2 changes: 1 addition & 1 deletion pkg/strategy/pivotshort/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})

s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})
Expand Down
8 changes: 4 additions & 4 deletions pkg/strategy/support/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,13 +499,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se

if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() {
// Update trailing stop when the position changes
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}

if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position
if position.(*types.Position).Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position
// Cancel the original order
if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil {
log.WithError(err).Errorf("Can not cancel the original trailing stop order!")
Expand All @@ -515,12 +515,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Calculate minimum target price
var minTargetPrice = fixedpoint.Zero
if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 {
minTargetPrice = position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage))
minTargetPrice = position.(*types.Position).AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage))
}

// Place new order if the target price is higher than the minimum target price
if s.trailingStopControl.IsHigherThanMin(minTargetPrice) {
orderForm := s.trailingStopControl.GenerateStopOrder(position.Base)
orderForm := s.trailingStopControl.GenerateStopOrder(position.(*types.Position).Base)
orders, err := s.submitOrders(ctx, orderExecutor, orderForm)
if err != nil {
log.WithError(err).Error("submit profit trailing stop order error")
Expand Down
3 changes: 1 addition & 2 deletions pkg/strategy/wall/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})

s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})
Expand Down Expand Up @@ -340,7 +340,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.WithError(err).Errorf("can not place order")
}


if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/strategy/xmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
}
})

s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
s.Notifiability.Notify(position)
})
s.tradeCollector.OnRecover(func(trade types.Trade) {
Expand Down
34 changes: 34 additions & 0 deletions pkg/types/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ type PositionRisk struct {
LiquidationPrice fixedpoint.Value `json:"liquidationPrice"`
}

type AnyPosition interface {
AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool)
c9s marked this conversation as resolved.
Show resolved Hide resolved
GetBase() (base fixedpoint.Value)
}

type Position struct {
Symbol string `json:"symbol" db:"symbol"`
BaseCurrency string `json:"baseCurrency" db:"base"`
Expand Down Expand Up @@ -453,3 +458,32 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp

return fixedpoint.Zero, fixedpoint.Zero, false
}

type PositionStack struct {
*Position
Stack []*Position
}

func (stack *PositionStack) Push(pos *Position) *PositionStack {
stack.Position = pos
stack.Stack = append(stack.Stack, pos)
return stack

}

func (stack *PositionStack) Pop() *PositionStack {
if len(stack.Stack) < 1 {
return nil
}
stack.Position = stack.Stack[len(stack.Stack)-1]
stack.Stack = stack.Stack[:len(stack.Stack)-1]
return stack
}
c9s marked this conversation as resolved.
Show resolved Hide resolved

func NewPositionStackFromMarket(market Market) *PositionStack {
pos := NewPositionFromMarket(market)
return &PositionStack{
Position: pos,
Stack: []*Position{pos},
}
}