mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #1121 from andycheng123/feature/hhllstop
Feature/hhllstop
This commit is contained in:
commit
68f54c032a
|
@ -80,19 +80,35 @@ exchangeStrategies:
|
||||||
exits:
|
exits:
|
||||||
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||||
- roiStopLoss:
|
- roiStopLoss:
|
||||||
percentage: 4.6%
|
percentage: 2%
|
||||||
- protectiveStopLoss:
|
|
||||||
activationRatio: 3.5%
|
|
||||||
stopLossRatio: 2.9%
|
|
||||||
placeStopOrder: false
|
|
||||||
- protectiveStopLoss:
|
|
||||||
activationRatio: 11.1%
|
|
||||||
stopLossRatio: 9.5%
|
|
||||||
placeStopOrder: false
|
|
||||||
- trailingStop:
|
- trailingStop:
|
||||||
callbackRate: 1.1%
|
callbackRate: 2%
|
||||||
#activationRatio: 20%
|
#activationRatio: 20%
|
||||||
minProfit: 19.5%
|
minProfit: 10%
|
||||||
interval: 1m
|
interval: 1m
|
||||||
side: both
|
side: both
|
||||||
closePosition: 100%
|
closePosition: 100%
|
||||||
|
- higherHighLowerLowStopLoss:
|
||||||
|
# interval is the kline interval used by this exit
|
||||||
|
interval: 15
|
||||||
|
# window is used as the range to determining higher highs and lower lows
|
||||||
|
window: 5
|
||||||
|
# highLowWindow is the range to calculate the number of higher highs and lower lows
|
||||||
|
highLowWindow: 12
|
||||||
|
# If the number of higher highs or lower lows with in HighLowWindow is less than MinHighLow, the exit is
|
||||||
|
# triggered. 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0
|
||||||
|
minHighLow: 2
|
||||||
|
# If the number of higher highs or lower lows with in HighLowWindow is more than MaxHighLow, the exit is
|
||||||
|
# triggered. 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0
|
||||||
|
maxHighLow: 0
|
||||||
|
# ActivationRatio is the trigger condition
|
||||||
|
# When the price goes higher (lower for short position) than this ratio, the stop will be activated.
|
||||||
|
# You can use this to combine several exits
|
||||||
|
activationRatio: 0.5%
|
||||||
|
# DeactivationRatio is the kill condition
|
||||||
|
# When the price goes higher (lower for short position) than this ratio, the stop will be deactivated.
|
||||||
|
# You can use this to combine several exits
|
||||||
|
deactivationRatio: 10%
|
||||||
|
# If true, looking for lower lows in long position and higher highs in short position. If false, looking for
|
||||||
|
# higher highs in long position and lower lows in short position
|
||||||
|
oppositeDirectionAsPosition: false
|
||||||
|
|
|
@ -29,16 +29,17 @@ func (s *ExitMethodSet) Bind(session *ExchangeSession, orderExecutor *GeneralOrd
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExitMethod struct {
|
type ExitMethod struct {
|
||||||
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
|
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
|
||||||
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
|
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
|
||||||
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
|
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
|
||||||
TrailingStop *TrailingStop2 `json:"trailingStop"`
|
TrailingStop *TrailingStop2 `json:"trailingStop"`
|
||||||
|
HigherHighLowerLowStop *HigherHighLowerLowStop `json:"higherHighLowerLowStopLoss"`
|
||||||
|
|
||||||
// Exit methods for short positions
|
// Exit methods for short positions
|
||||||
// =================================================
|
// =================================================
|
||||||
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
|
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
|
||||||
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
|
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
|
||||||
SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"`
|
SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ExitMethod) String() string {
|
func (e ExitMethod) String() string {
|
||||||
|
@ -78,6 +79,11 @@ func (e ExitMethod) String() string {
|
||||||
buf.WriteString("supportTakeProfit: " + string(b) + ", ")
|
buf.WriteString("supportTakeProfit: " + string(b) + ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.HigherHighLowerLowStop != nil {
|
||||||
|
b, _ := json.Marshal(e.HigherHighLowerLowStop)
|
||||||
|
buf.WriteString("hhllStop: " + string(b) + ", ")
|
||||||
|
}
|
||||||
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,4 +141,8 @@ func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderE
|
||||||
if m.TrailingStop != nil {
|
if m.TrailingStop != nil {
|
||||||
m.TrailingStop.Bind(session, orderExecutor)
|
m.TrailingStop.Bind(session, orderExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.HigherHighLowerLowStop != nil {
|
||||||
|
m.HigherHighLowerLowStop.Bind(session, orderExecutor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
202
pkg/bbgo/exit_hh_ll_stop.go
Normal file
202
pkg/bbgo/exit_hh_ll_stop.go
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
package bbgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HigherHighLowerLowStop struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
|
||||||
|
// Interval is the kline interval used by this exit. Window is used as the range to determining higher highs and
|
||||||
|
// lower lows
|
||||||
|
types.IntervalWindow
|
||||||
|
|
||||||
|
// HighLowWindow is the range to calculate the number of higher highs and lower lows
|
||||||
|
HighLowWindow int `json:"highLowWindow"`
|
||||||
|
|
||||||
|
// If the number of higher highs or lower lows with in HighLowWindow is more than MaxHighLow, the exit is triggered.
|
||||||
|
// 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0
|
||||||
|
MaxHighLow int `json:"maxHighLow"`
|
||||||
|
|
||||||
|
// If the number of higher highs or lower lows with in HighLowWindow is less than MinHighLow, the exit is triggered.
|
||||||
|
// 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0
|
||||||
|
MinHighLow int `json:"minHighLow"`
|
||||||
|
|
||||||
|
// ActivationRatio is the trigger condition
|
||||||
|
// When the price goes higher (lower for short position) than this ratio, the stop will be activated.
|
||||||
|
// You can use this to combine several exits
|
||||||
|
ActivationRatio fixedpoint.Value `json:"activationRatio"`
|
||||||
|
|
||||||
|
// DeactivationRatio is the kill condition
|
||||||
|
// When the price goes higher (lower for short position) than this ratio, the stop will be deactivated.
|
||||||
|
// You can use this to combine several exits
|
||||||
|
DeactivationRatio fixedpoint.Value `json:"deactivationRatio"`
|
||||||
|
|
||||||
|
// If true, looking for lower lows in long position and higher highs in short position. If false, looking for higher
|
||||||
|
// highs in long position and lower lows in short position
|
||||||
|
OppositeDirectionAsPosition bool `json:"oppositeDirectionAsPosition"`
|
||||||
|
|
||||||
|
klines types.KLineWindow
|
||||||
|
|
||||||
|
// activated: when the price reaches the min profit price, we set the activated to true to enable hhll stop
|
||||||
|
activated bool
|
||||||
|
|
||||||
|
highLows []types.Direction
|
||||||
|
|
||||||
|
session *ExchangeSession
|
||||||
|
orderExecutor *GeneralOrderExecutor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe required k-line stream
|
||||||
|
func (s *HigherHighLowerLowStop) Subscribe(session *ExchangeSession) {
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateActivated checks the position cost against the close price, activation ratio, and deactivation ratio to
|
||||||
|
// determine whether this stop should be activated
|
||||||
|
func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, closePrice fixedpoint.Value) {
|
||||||
|
if position.IsClosed() || position.IsDust(closePrice) {
|
||||||
|
s.activated = false
|
||||||
|
} else if s.activated {
|
||||||
|
if position.IsLong() {
|
||||||
|
r := fixedpoint.One.Add(s.DeactivationRatio)
|
||||||
|
if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 {
|
||||||
|
s.activated = false
|
||||||
|
Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s:", s.Symbol, s.DeactivationRatio.Percentage())
|
||||||
|
}
|
||||||
|
} else if position.IsShort() {
|
||||||
|
r := fixedpoint.One.Sub(s.DeactivationRatio)
|
||||||
|
// for short position, if the close price is less than the activation price then this is a profit position.
|
||||||
|
if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 {
|
||||||
|
s.activated = false
|
||||||
|
Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s:", s.Symbol, s.DeactivationRatio.Percentage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if position.IsLong() {
|
||||||
|
r := fixedpoint.One.Add(s.ActivationRatio)
|
||||||
|
if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 {
|
||||||
|
s.activated = true
|
||||||
|
Notify("[hhllStop] Stop of %s activated for long position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage())
|
||||||
|
}
|
||||||
|
} else if position.IsShort() {
|
||||||
|
r := fixedpoint.One.Sub(s.ActivationRatio)
|
||||||
|
// for short position, if the close price is less than the activation price then this is a profit position.
|
||||||
|
if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 {
|
||||||
|
s.activated = true
|
||||||
|
Notify("[hhllStop] Stop of %s activated for short position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) {
|
||||||
|
s.klines.Truncate(s.Window - 1)
|
||||||
|
|
||||||
|
if s.klines.Len() >= s.Window-1 {
|
||||||
|
if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 {
|
||||||
|
s.highLows = append(s.highLows, types.DirectionUp)
|
||||||
|
log.Debugf("[hhllStop] new higher high for %s", s.Symbol)
|
||||||
|
} else if s.klines.GetLow().Compare(kline.GetLow()) > 0 {
|
||||||
|
s.highLows = append(s.highLows, types.DirectionDown)
|
||||||
|
log.Debugf("[hhllStop] new lower low for %s", s.Symbol)
|
||||||
|
} else {
|
||||||
|
s.highLows = append(s.highLows, types.DirectionNone)
|
||||||
|
}
|
||||||
|
// Truncate highLows
|
||||||
|
if len(s.highLows) > s.HighLowWindow {
|
||||||
|
end := len(s.highLows)
|
||||||
|
start := end - s.HighLowWindow
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
kn := s.highLows[start:]
|
||||||
|
s.highLows = kn
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.highLows = append(s.highLows, types.DirectionNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.klines.Add(kline)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HigherHighLowerLowStop) shouldStop(position *types.Position) bool {
|
||||||
|
if s.klines.Len() < s.Window || len(s.highLows) < s.HighLowWindow {
|
||||||
|
log.Debugf("[hhllStop] not enough data for %s yet", s.Symbol)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.activated {
|
||||||
|
highs := 0
|
||||||
|
lows := 0
|
||||||
|
for _, hl := range s.highLows {
|
||||||
|
switch hl {
|
||||||
|
case types.DirectionUp:
|
||||||
|
highs++
|
||||||
|
case types.DirectionDown:
|
||||||
|
lows++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("[hhllStop] %d higher highs and %d lower lows in window of %d", highs, lows, s.HighLowWindow)
|
||||||
|
|
||||||
|
// Check higher highs
|
||||||
|
if (position.IsLong() && !s.OppositeDirectionAsPosition) || (position.IsShort() && s.OppositeDirectionAsPosition) {
|
||||||
|
if (s.MinHighLow > 0 && highs < s.MinHighLow) || (s.MaxHighLow > 0 && highs > s.MaxHighLow) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check lower lows
|
||||||
|
} else if (position.IsShort() && !s.OppositeDirectionAsPosition) || (position.IsLong() && s.OppositeDirectionAsPosition) {
|
||||||
|
if (s.MinHighLow > 0 && lows < s.MinHighLow) || (s.MaxHighLow > 0 && lows > s.MaxHighLow) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
|
||||||
|
// Check parameters
|
||||||
|
if s.Window <= 0 {
|
||||||
|
panic(fmt.Errorf("[hhllStop] window must be larger than zero"))
|
||||||
|
}
|
||||||
|
if s.HighLowWindow <= 0 {
|
||||||
|
panic(fmt.Errorf("[hhllStop] highLowWindow must be larger than zero"))
|
||||||
|
}
|
||||||
|
if s.MaxHighLow <= 0 && s.MinHighLow <= 0 {
|
||||||
|
panic(fmt.Errorf("[hhllStop] either maxHighLow or minHighLow must be larger than zero"))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.session = session
|
||||||
|
s.orderExecutor = orderExecutor
|
||||||
|
|
||||||
|
position := orderExecutor.Position()
|
||||||
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||||
|
s.updateActivated(position, kline.GetClose())
|
||||||
|
|
||||||
|
s.updateHighLowNumber(kline)
|
||||||
|
|
||||||
|
// Close position & reset
|
||||||
|
if s.shouldStop(position) {
|
||||||
|
err := s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "hhllStop")
|
||||||
|
if err != nil {
|
||||||
|
Notify("[hhllStop] Stop of %s triggered but failed to close %s position:", s.Symbol, err)
|
||||||
|
} else {
|
||||||
|
s.activated = false
|
||||||
|
Notify("[hhllStop] Stop of %s triggered and position closed", s.Symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Make sure the stop is reset when position is closed or dust
|
||||||
|
orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
|
||||||
|
if position.IsClosed() || position.IsDust(position.AverageCost) {
|
||||||
|
s.activated = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user