mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #846 from c9s/strategy/pivotshort
strategy/pivotshort: refactor breaklow + add fake break stop
This commit is contained in:
commit
3aeb6912c9
|
@ -42,13 +42,13 @@ exchangeStrategies:
|
|||
# Notice: When marketOrder is set, bounceRatio will not be used.
|
||||
# bounceRatio: 0.1%
|
||||
|
||||
# stopEMARange is the price range we allow short.
|
||||
# stopEMA is the price range we allow short.
|
||||
# Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
|
||||
# Higher the stopEMARange than higher the chance to open a short
|
||||
stopEMARange: 2%
|
||||
stopEMA:
|
||||
interval: 1h
|
||||
window: 99
|
||||
range: 2%
|
||||
|
||||
trendEMA:
|
||||
interval: 1d
|
||||
|
|
|
@ -72,7 +72,9 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
|
|||
cqv.Float64(),
|
||||
s.MinQuoteVolume.Float64(), kline.Close.Float64())
|
||||
|
||||
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
|
||||
if err := orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") ; err != nil {
|
||||
log.WithError(err).Errorf("close position error")
|
||||
}
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
|
|
@ -25,7 +25,7 @@ type StandardIndicatorSet struct {
|
|||
// interval -> window
|
||||
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
|
||||
stoch map[types.IntervalWindow]*indicator.STOCH
|
||||
simples map[types.IntervalWindow]indicator.Simple
|
||||
simples map[types.IntervalWindow]indicator.KLinePusher
|
||||
|
||||
stream types.Stream
|
||||
store *MarketDataStore
|
||||
|
@ -36,7 +36,7 @@ func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDa
|
|||
Symbol: symbol,
|
||||
store: store,
|
||||
stream: stream,
|
||||
simples: make(map[types.IntervalWindow]indicator.Simple),
|
||||
simples: make(map[types.IntervalWindow]indicator.KLinePusher),
|
||||
|
||||
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
|
||||
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
|
||||
|
@ -53,7 +53,7 @@ func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, iw types.I
|
|||
s.stream.OnKLineClosed(types.KLineWith(s.Symbol, iw.Interval, inc.PushK))
|
||||
}
|
||||
|
||||
func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.Simple, iw types.IntervalWindow) indicator.Simple {
|
||||
func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow) indicator.KLinePusher {
|
||||
inc, ok := s.simples[iw]
|
||||
if ok {
|
||||
return inc
|
||||
|
|
|
@ -11,9 +11,10 @@ import (
|
|||
|
||||
//go:generate callbackgen -type PivotLow
|
||||
type PivotLow struct {
|
||||
types.IntervalWindow
|
||||
types.SeriesBase
|
||||
|
||||
types.IntervalWindow
|
||||
|
||||
Lows types.Float64Slice
|
||||
Values types.Float64Slice
|
||||
EndTime time.Time
|
||||
|
@ -71,15 +72,11 @@ func calculatePivotLow(lows types.Float64Slice, window int) (float64, error) {
|
|||
return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window)
|
||||
}
|
||||
|
||||
var pv types.Float64Slice
|
||||
for _, low := range lows {
|
||||
pv.Push(low)
|
||||
end := length - 1
|
||||
min := lows[end-(window-1):].Min()
|
||||
if min == lows.Index(int(window/2.)-1) {
|
||||
return min, nil
|
||||
}
|
||||
|
||||
pl := 0.
|
||||
if lows.Min() == lows.Index(int(window/2.)-1) {
|
||||
pl = lows.Min()
|
||||
}
|
||||
|
||||
return pl, nil
|
||||
return 0., nil
|
||||
}
|
||||
|
|
|
@ -164,6 +164,7 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
|
|||
leverage = fixedpoint.NewFromInt(3)
|
||||
}
|
||||
|
||||
|
||||
baseBalance, _ := session.Account.Balance(market.BaseCurrency)
|
||||
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency)
|
||||
|
||||
|
@ -185,11 +186,11 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
|
|||
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings")
|
||||
}
|
||||
|
||||
// using leverage -- starts from here
|
||||
if !quantity.IsZero() {
|
||||
return quantity, nil
|
||||
}
|
||||
|
||||
// using leverage -- starts from here
|
||||
logrus.Infof("calculating available leveraged base quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance)
|
||||
|
||||
// calculate the quantity automatically
|
||||
|
|
|
@ -10,6 +10,19 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type StopEMA struct {
|
||||
types.IntervalWindow
|
||||
Range fixedpoint.Value `json:"range"`
|
||||
}
|
||||
|
||||
type TrendEMA struct {
|
||||
types.IntervalWindow
|
||||
}
|
||||
|
||||
type FakeBreakStop struct {
|
||||
types.IntervalWindow
|
||||
}
|
||||
|
||||
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
|
||||
type BreakLow struct {
|
||||
Symbol string
|
||||
|
@ -26,22 +39,28 @@ type BreakLow struct {
|
|||
// limit sell price = breakLowPrice * (1 + BounceRatio)
|
||||
BounceRatio fixedpoint.Value `json:"bounceRatio"`
|
||||
|
||||
Leverage fixedpoint.Value `json:"leverage"`
|
||||
Quantity fixedpoint.Value `json:"quantity"`
|
||||
StopEMARange fixedpoint.Value `json:"stopEMARange"`
|
||||
StopEMA *types.IntervalWindow `json:"stopEMA"`
|
||||
Leverage fixedpoint.Value `json:"leverage"`
|
||||
Quantity fixedpoint.Value `json:"quantity"`
|
||||
|
||||
TrendEMA *types.IntervalWindow `json:"trendEMA"`
|
||||
StopEMA *StopEMA `json:"stopEMA"`
|
||||
|
||||
TrendEMA *TrendEMA `json:"trendEMA"`
|
||||
|
||||
FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"`
|
||||
|
||||
lastLow fixedpoint.Value
|
||||
|
||||
// lastBreakLow is the low that the price just break
|
||||
lastBreakLow fixedpoint.Value
|
||||
|
||||
pivotLow *indicator.PivotLow
|
||||
pivotLowPrices []fixedpoint.Value
|
||||
|
||||
lastLow fixedpoint.Value
|
||||
pivot *indicator.PivotLow
|
||||
stopEWMA *indicator.EWMA
|
||||
|
||||
trendEWMA *indicator.EWMA
|
||||
trendEWMALast, trendEWMACurrent float64
|
||||
|
||||
pivotLowPrices []fixedpoint.Value
|
||||
|
||||
orderExecutor *bbgo.GeneralOrderExecutor
|
||||
session *bbgo.ExchangeSession
|
||||
}
|
||||
|
@ -57,6 +76,10 @@ func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
|
|||
if s.TrendEMA != nil {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval})
|
||||
}
|
||||
|
||||
if s.FakeBreakStop != nil {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.FakeBreakStop.Interval})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
|
||||
|
@ -69,14 +92,14 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
|
|||
|
||||
s.lastLow = fixedpoint.Zero
|
||||
|
||||
s.pivot = standardIndicator.PivotLow(s.IntervalWindow)
|
||||
s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow)
|
||||
|
||||
if s.StopEMA != nil {
|
||||
s.stopEWMA = standardIndicator.EWMA(*s.StopEMA)
|
||||
s.stopEWMA = standardIndicator.EWMA(s.StopEMA.IntervalWindow)
|
||||
}
|
||||
|
||||
if s.TrendEMA != nil {
|
||||
s.trendEWMA = standardIndicator.EWMA(*s.TrendEMA)
|
||||
s.trendEWMA = standardIndicator.EWMA(s.TrendEMA.IntervalWindow)
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.TrendEMA.Interval, func(kline types.KLine) {
|
||||
s.trendEWMALast = s.trendEWMACurrent
|
||||
|
@ -86,58 +109,52 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
|
|||
|
||||
// update pivot low data
|
||||
session.MarketDataStream.OnStart(func() {
|
||||
lastLow := fixedpoint.NewFromFloat(s.pivot.Lows.Last())
|
||||
if lastLow.IsZero() {
|
||||
return
|
||||
if s.updatePivotLow() {
|
||||
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last())
|
||||
}
|
||||
|
||||
if lastLow.Compare(s.lastLow) != 0 {
|
||||
bbgo.Notify("%s found new pivot low: %f", s.Symbol, s.pivot.Lows.Last())
|
||||
}
|
||||
|
||||
s.lastLow = lastLow
|
||||
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
|
||||
|
||||
log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f",
|
||||
s.lastLow.Float64(),
|
||||
s.Quantity.Float64(),
|
||||
s.Leverage.Float64())
|
||||
|
||||
quantity, err := risk.CalculateBaseQuantity(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("quantity calculation error")
|
||||
}
|
||||
|
||||
if quantity.IsZero() {
|
||||
log.WithError(err).Errorf("quantity is zero, can not submit order")
|
||||
return
|
||||
}
|
||||
|
||||
bbgo.Notify("%s %f quantity will be used for shorting", s.Symbol, quantity.Float64())
|
||||
s.pilotQuantityCalculation()
|
||||
})
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
|
||||
lastLow := fixedpoint.NewFromFloat(s.pivot.Lows.Last())
|
||||
if lastLow.IsZero() {
|
||||
return
|
||||
if s.updatePivotLow() {
|
||||
// when position is opened, do not send pivot low notify
|
||||
if position.IsOpened(kline.Close) {
|
||||
return
|
||||
}
|
||||
|
||||
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last())
|
||||
}
|
||||
|
||||
if lastLow.Compare(s.lastLow) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
s.lastLow = lastLow
|
||||
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
|
||||
|
||||
// when position is opened, do not send pivot low notify
|
||||
if position.IsOpened(kline.Close) {
|
||||
return
|
||||
}
|
||||
|
||||
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivot.Lows.Last())
|
||||
}))
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, types.Interval1m, func(kline types.KLine) {
|
||||
if s.FakeBreakStop != nil {
|
||||
// if the position is already opened, and we just break the low, this checks if the kline closed above the low,
|
||||
// so that we can close the position earlier
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.FakeBreakStop.Interval, func(k types.KLine) {
|
||||
// make sure the position is opened, and it's a short position
|
||||
if !position.IsOpened(k.Close) || !position.IsShort() {
|
||||
return
|
||||
}
|
||||
|
||||
// make sure we recorded the last break low
|
||||
if s.lastBreakLow.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
// the kline opened below the last break low, and closed above the last break low
|
||||
if k.Open.Compare(s.lastBreakLow) < 0 && k.Close.Compare(s.lastBreakLow) > 0 {
|
||||
bbgo.Notify("kLine closed above the last break low, triggering stop earlier")
|
||||
if err := s.orderExecutor.ClosePosition(context.Background(), one, "kLineClosedStop"); err != nil {
|
||||
log.WithError(err).Error("position close error")
|
||||
}
|
||||
|
||||
// reset to zero
|
||||
s.lastBreakLow = fixedpoint.Zero
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {
|
||||
if len(s.pivotLowPrices) == 0 {
|
||||
log.Infof("currently there is no pivot low prices, can not check break low...")
|
||||
return
|
||||
|
@ -170,6 +187,10 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
|
|||
|
||||
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
|
||||
|
||||
if s.lastBreakLow.IsZero() || previousLow.Compare(s.lastBreakLow) < 0 {
|
||||
s.lastBreakLow = previousLow
|
||||
}
|
||||
|
||||
if position.IsOpened(kline.Close) {
|
||||
log.Infof("position is already opened, skip short")
|
||||
return
|
||||
|
@ -193,9 +214,9 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
|
|||
return
|
||||
}
|
||||
|
||||
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange))
|
||||
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMA.Range))
|
||||
if closePrice.Compare(emaStopShortPrice) < 0 {
|
||||
log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.StopEMA, ema.Float64())
|
||||
log.Infof("stopEMA protection: close price %f < EMA(%v %f) * (1 - RANGE %f) = %f", closePrice.Float64(), s.StopEMA, ema.Float64(), s.StopEMA.Range.Float64(), emaStopShortPrice.Float64())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -241,3 +262,33 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
|
|||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *BreakLow) pilotQuantityCalculation() {
|
||||
log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f",
|
||||
s.lastLow.Float64(),
|
||||
s.Quantity.Float64(),
|
||||
s.Leverage.Float64())
|
||||
|
||||
quantity, err := risk.CalculateBaseQuantity(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("quantity calculation error")
|
||||
}
|
||||
|
||||
if quantity.IsZero() {
|
||||
log.WithError(err).Errorf("quantity is zero, can not submit order")
|
||||
return
|
||||
}
|
||||
|
||||
bbgo.Notify("%s %f quantity will be used for shorting", s.Symbol, quantity.Float64())
|
||||
}
|
||||
|
||||
func (s *BreakLow) updatePivotLow() bool {
|
||||
lastLow := fixedpoint.NewFromFloat(s.pivotLow.Last())
|
||||
if lastLow.IsZero() || lastLow.Compare(s.lastLow) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
s.lastLow = lastLow
|
||||
s.pivotLowPrices = append(s.pivotLowPrices, lastLow)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
|
|||
s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow)
|
||||
|
||||
// use the last kline from the history before we get the next closed kline
|
||||
s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Lows.Last()))
|
||||
s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Last()))
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||
position := s.orderExecutor.Position()
|
||||
|
@ -77,7 +77,7 @@ func tail(arr []float64, length int) []float64 {
|
|||
func (s *ResistanceShort) updateCurrentResistancePrice(closePrice fixedpoint.Value) bool {
|
||||
minDistance := s.MinDistance.Float64()
|
||||
groupDistance := s.GroupDistance.Float64()
|
||||
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6))
|
||||
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, s.resistancePivot.Values.Tail(6))
|
||||
if len(resistancePrices) == 0 {
|
||||
return false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user