bollmaker: clean up position stack

This commit is contained in:
austin362667 2022-05-30 18:16:20 +08:00
parent 54d60b9890
commit 3f939461cf
12 changed files with 49 additions and 165 deletions

View File

@ -16,7 +16,7 @@ backtest:
# for testing max draw down (MDD) at 03-12
# see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2022-05-01"
startTime: "2022-01-01"
endTime: "2022-05-31"
sessions:
- binance
@ -26,7 +26,7 @@ backtest:
binance:
balances:
ETH: 0.0
USDT: 100_000.0
USDT: 10_000.0
exchangeStrategies:
@ -44,13 +44,15 @@ exchangeStrategies:
# 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.
pushThreshold: 10%
# popThreshold : 1%
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))
@ -110,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.

View File

@ -62,7 +62,7 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi
c.averageCost = c.position.AverageCost
// Use trade collector to get the position update event
tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
// update average cost if we have it.
c.averageCost = position.(*types.Position).AverageCost
})

View File

@ -18,17 +18,17 @@ type TradeCollector struct {
tradeStore *TradeStore
tradeC chan types.Trade
position types.PositionInterface
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.PositionInterface)
positionUpdateCallbacks []func(position types.AnyPosition)
profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value)
}
func NewTradeCollector(symbol string, position types.PositionInterface, orderStore *OrderStore) *TradeCollector {
func NewTradeCollector(symbol string, position types.AnyPosition, orderStore *OrderStore) *TradeCollector {
return &TradeCollector{
Symbol: symbol,
orderSig: sigchan.New(1),
@ -47,7 +47,7 @@ func (c *TradeCollector) OrderStore() *OrderStore {
}
// Position returns the position used by the trade collector
func (c *TradeCollector) Position() types.PositionInterface {
func (c *TradeCollector) Position() types.AnyPosition {
return c.position
}

View File

@ -27,11 +27,11 @@ func (c *TradeCollector) EmitTrade(trade types.Trade, profit fixedpoint.Value, n
}
}
func (c *TradeCollector) OnPositionUpdate(cb func(position types.PositionInterface)) {
func (c *TradeCollector) OnPositionUpdate(cb func(position types.AnyPosition)) {
c.positionUpdateCallbacks = append(c.positionUpdateCallbacks, cb)
}
func (c *TradeCollector) EmitPositionUpdate(position types.PositionInterface) {
func (c *TradeCollector) EmitPositionUpdate(position types.AnyPosition) {
for _, cb := range c.positionUpdateCallbacks {
cb(position)
}

View File

@ -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"`
@ -225,15 +231,13 @@ type Strategy struct {
session *bbgo.ExchangeSession
book *types.StreamOrderBook
state *State
state *State
PositionStack PositionStack
// persistence fields
Position *types.PositionStack `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
PushThreshold fixedpoint.Value `json:"pushThreshold,omitempty"`
PopThreshold fixedpoint.Value `json:"popThreshold,omitempty"`
activeMakerOrders *bbgo.LocalActiveOrderBook
orderStore *bbgo.OrderStore
tradeCollector *bbgo.TradeCollector
@ -717,7 +721,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})
s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})
@ -775,19 +779,27 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
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.PushThreshold)) > 0 {
log.Errorf("push")
log.Error(s.Position)
//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))
}
// &&
if len(s.Position.Stack) > 1 && s.Position.Stack[len(s.Position.Stack)-2].AverageCost.Compare(kline.Close) < 0 && s.Market.IsDustQuantity(s.Position.Position.GetBase(), kline.Close) {
log.Errorf("pop")
log.Error(s.Position)
// 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")

View File

@ -834,7 +834,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})
s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", position)
s.Notify(s.Position)
})

View File

@ -620,7 +620,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
*/
s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
s.Notifiability.Notify(position)
})
s.tradeCollector.BindStream(session.UserDataStream)

View File

@ -179,7 +179,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})
s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})

View File

@ -499,7 +499,7 @@ 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.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return

View File

@ -302,7 +302,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})
s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})

View File

@ -781,7 +781,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
}
})
s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) {
s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) {
s.Notifiability.Notify(position)
})
s.tradeCollector.OnRecover(func(trade types.Trade) {

View File

@ -29,8 +29,9 @@ type PositionRisk struct {
LiquidationPrice fixedpoint.Value `json:"liquidationPrice"`
}
type PositionInterface interface {
type AnyPosition interface {
AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool)
GetBase() (base fixedpoint.Value)
}
type Position struct {
@ -480,140 +481,9 @@ func (stack *PositionStack) Pop() *PositionStack {
}
func NewPositionStackFromMarket(market Market) *PositionStack {
pos := &Position{
Symbol: market.Symbol,
BaseCurrency: market.BaseCurrency,
QuoteCurrency: market.QuoteCurrency,
Market: market,
TotalFee: make(map[string]fixedpoint.Value),
}
pos := NewPositionFromMarket(market)
return &PositionStack{
Position: pos,
Stack: []*Position{pos},
}
}
func (p *PositionStack) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) {
price := td.Price
quantity := td.Quantity
quoteQuantity := td.QuoteQuantity
fee := td.Fee
// calculated fee in quote (some exchange accounts may enable platform currency fee discount, like BNB)
// convert platform fee token into USD values
var feeInQuote fixedpoint.Value = fixedpoint.Zero
switch td.FeeCurrency {
case p.BaseCurrency:
quantity = quantity.Sub(fee)
case p.QuoteCurrency:
quoteQuantity = quoteQuantity.Sub(fee)
default:
if p.ExchangeFeeRates != nil {
if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok {
if td.IsMaker {
feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity))
} else {
feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity))
}
}
} else if p.FeeRate != nil {
if td.IsMaker {
feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity))
} else {
feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity))
}
}
}
p.Lock()
defer p.Unlock()
// update changedAt field before we unlock in the defer func
defer func() {
p.ChangedAt = td.Time.Time()
}()
p.addTradeFee(td)
// Base > 0 means we're in long position
// Base < 0 means we're in short position
switch td.Side {
case SideTypeBuy:
if p.Base.Sign() < 0 {
// convert short position to long position
if p.Base.Add(quantity).Sign() > 0 {
profit = p.AverageCost.Sub(price).Mul(p.Base.Neg())
netProfit = p.ApproximateAverageCost.Sub(price).Mul(p.Base.Neg()).Sub(feeInQuote)
p.Base = p.Base.Add(quantity)
p.Quote = p.Quote.Sub(quoteQuantity)
p.AverageCost = price
p.ApproximateAverageCost = price
p.AccumulatedProfit = p.AccumulatedProfit.Add(profit)
return profit, netProfit, true
} else {
// covering short position
p.Base = p.Base.Add(quantity)
p.Quote = p.Quote.Sub(quoteQuantity)
profit = p.AverageCost.Sub(price).Mul(quantity)
netProfit = p.ApproximateAverageCost.Sub(price).Mul(quantity).Sub(feeInQuote)
p.AccumulatedProfit = p.AccumulatedProfit.Add(profit)
return profit, netProfit, true
}
}
divisor := p.Base.Add(quantity)
p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base).
Add(quoteQuantity).
Add(feeInQuote).
Div(divisor)
p.AverageCost = p.AverageCost.Mul(p.Base).Add(quoteQuantity).Div(divisor)
p.Base = p.Base.Add(quantity)
p.Quote = p.Quote.Sub(quoteQuantity)
return fixedpoint.Zero, fixedpoint.Zero, false
case SideTypeSell:
if p.Base.Sign() > 0 {
// convert long position to short position
if p.Base.Compare(quantity) < 0 {
profit = price.Sub(p.AverageCost).Mul(p.Base)
netProfit = price.Sub(p.ApproximateAverageCost).Mul(p.Base).Sub(feeInQuote)
p.Base = p.Base.Sub(quantity)
p.Quote = p.Quote.Add(quoteQuantity)
p.AverageCost = price
p.ApproximateAverageCost = price
p.AccumulatedProfit = p.AccumulatedProfit.Add(profit)
return profit, netProfit, true
} else {
p.Base = p.Base.Sub(quantity)
p.Quote = p.Quote.Add(quoteQuantity)
profit = price.Sub(p.AverageCost).Mul(quantity)
netProfit = price.Sub(p.ApproximateAverageCost).Mul(quantity).Sub(feeInQuote)
p.AccumulatedProfit = p.AccumulatedProfit.Add(profit)
return profit, netProfit, true
}
}
// handling short position, since Base here is negative we need to reverse the sign
divisor := quantity.Sub(p.Base)
p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base.Neg()).
Add(quoteQuantity).
Sub(feeInQuote).
Div(divisor)
p.AverageCost = p.AverageCost.Mul(p.Base.Neg()).
Add(quoteQuantity).
Div(divisor)
p.Base = p.Base.Sub(quantity)
p.Quote = p.Quote.Add(quoteQuantity)
return fixedpoint.Zero, fixedpoint.Zero, false
}
return fixedpoint.Zero, fixedpoint.Zero, false
}