mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-23 15:25:14 +00:00
bollmaker: clean up position stack
This commit is contained in:
parent
54d60b9890
commit
3f939461cf
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
@ -226,14 +232,12 @@ type Strategy struct {
|
|||
book *types.StreamOrderBook
|
||||
|
||||
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")
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user