2023-03-16 10:30:48 +00:00
package bbgo
import (
"context"
2023-04-11 07:11:11 +00:00
"fmt"
2023-06-07 08:45:46 +00:00
2023-03-16 10:30:48 +00:00
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
2023-03-16 10:39:27 +00:00
type HigherHighLowerLowStop struct {
2023-03-16 10:30:48 +00:00
Symbol string ` json:"symbol" `
2023-04-11 08:02:54 +00:00
// Interval is the kline interval used by this exit. Window is used as the range to determining higher highs and
// lower lows
2023-03-16 10:30:48 +00:00
types . IntervalWindow
2023-04-11 08:02:54 +00:00
// HighLowWindow is the range to calculate the number of higher highs and lower lows
2023-03-16 10:30:48 +00:00
HighLowWindow int ` json:"highLowWindow" `
2023-04-11 08:02:54 +00:00
// 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
2023-03-16 10:30:48 +00:00
MaxHighLow int ` json:"maxHighLow" `
2023-04-11 08:02:54 +00:00
// 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
2023-03-16 10:30:48 +00:00
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" `
2023-04-11 08:02:54 +00:00
// 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
2023-03-16 10:30:48 +00:00
OppositeDirectionAsPosition bool ` json:"oppositeDirectionAsPosition" `
klines types . KLineWindow
2023-04-11 08:02:54 +00:00
// activated: when the price reaches the min profit price, we set the activated to true to enable hhll stop
2023-03-16 10:30:48 +00:00
activated bool
highLows [ ] types . Direction
session * ExchangeSession
orderExecutor * GeneralOrderExecutor
}
// Subscribe required k-line stream
2023-03-16 10:39:27 +00:00
func ( s * HigherHighLowerLowStop ) Subscribe ( session * ExchangeSession ) {
2023-03-16 10:30:48 +00:00
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
2023-03-16 10:39:27 +00:00
func ( s * HigherHighLowerLowStop ) updateActivated ( position * types . Position , closePrice fixedpoint . Value ) {
2023-06-30 06:03:46 +00:00
// deactivate when no position
2023-03-16 10:30:48 +00:00
if position . IsClosed ( ) || position . IsDust ( closePrice ) {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
s . activated = false
2023-06-30 05:55:07 +00:00
return
2023-06-30 05:51:47 +00:00
2023-06-30 05:55:07 +00:00
}
2023-06-30 06:03:46 +00:00
// activation/deactivation price
2023-06-30 06:10:25 +00:00
var priceDeactive fixedpoint . Value
var priceActive fixedpoint . Value
2023-06-30 06:03:46 +00:00
if position . IsLong ( ) {
2023-06-30 06:10:25 +00:00
priceDeactive = position . AverageCost . Mul ( fixedpoint . One . Add ( s . DeactivationRatio ) )
priceActive = position . AverageCost . Mul ( fixedpoint . One . Add ( s . ActivationRatio ) )
2023-06-30 06:03:46 +00:00
} else {
2023-06-30 06:10:25 +00:00
priceDeactive = position . AverageCost . Mul ( fixedpoint . One . Sub ( s . DeactivationRatio ) )
priceActive = position . AverageCost . Mul ( fixedpoint . One . Sub ( s . ActivationRatio ) )
2023-06-30 06:03:46 +00:00
}
2023-06-30 05:55:07 +00:00
if s . activated {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
if position . IsLong ( ) {
2023-06-30 05:51:47 +00:00
2023-06-30 06:10:25 +00:00
if closePrice . Compare ( priceDeactive ) >= 0 {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
s . activated = false
2023-06-30 05:42:10 +00:00
Notify ( "[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s" , s . Symbol , s . DeactivationRatio . Percentage ( ) )
2023-06-30 05:51:47 +00:00
2023-06-30 06:10:25 +00:00
} else if closePrice . Compare ( priceActive ) < 0 {
2023-06-30 05:51:47 +00:00
2023-06-30 05:42:10 +00:00
s . activated = false
Notify ( "[hhllStop] Stop of %s deactivated for long position, activation ratio %s" , s . Symbol , s . ActivationRatio . Percentage ( ) )
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
} else if position . IsShort ( ) {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
// for short position, if the close price is less than the activation price then this is a profit position.
2023-06-30 06:10:25 +00:00
if closePrice . Compare ( priceDeactive ) <= 0 {
2023-06-30 05:51:47 +00:00
2023-06-30 05:42:10 +00:00
s . activated = false
Notify ( "[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s" , s . Symbol , s . DeactivationRatio . Percentage ( ) )
2023-06-30 05:51:47 +00:00
2023-06-30 06:10:25 +00:00
} else if closePrice . Compare ( priceActive ) > 0 {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
s . activated = false
2023-06-30 05:42:10 +00:00
Notify ( "[hhllStop] Stop of %s deactivated for short position, activation ratio %s" , s . Symbol , s . ActivationRatio . Percentage ( ) )
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
} else {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
if position . IsLong ( ) {
2023-06-30 05:51:47 +00:00
2023-06-30 06:10:25 +00:00
if closePrice . Compare ( priceActive ) >= 0 && closePrice . Compare ( priceDeactive ) < 0 {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
s . activated = true
2023-06-30 05:42:10 +00:00
Notify ( "[hhllStop] %s stop is activated for long position, activation ratio %s, deactivation ratio %s" , s . Symbol , s . ActivationRatio . Percentage ( ) , s . DeactivationRatio . Percentage ( ) )
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
} else if position . IsShort ( ) {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
// for short position, if the close price is less than the activation price then this is a profit position.
2023-06-30 06:10:25 +00:00
if closePrice . Compare ( priceActive ) <= 0 && closePrice . Compare ( priceDeactive ) > 0 {
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
s . activated = true
2023-06-30 05:42:10 +00:00
Notify ( "[hhllStop] %s stop is activated for short position, activation ratio %s, deactivation ratio %s" , s . Symbol , s . ActivationRatio . Percentage ( ) , s . DeactivationRatio . Percentage ( ) )
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
2023-06-30 05:51:47 +00:00
2023-03-16 10:30:48 +00:00
}
}
2023-03-16 10:39:27 +00:00
func ( s * HigherHighLowerLowStop ) updateHighLowNumber ( kline types . KLine ) {
2023-03-20 07:56:51 +00:00
s . klines . Truncate ( s . Window - 1 )
2023-04-11 06:55:32 +00:00
if s . klines . Len ( ) >= s . Window - 1 {
2023-06-07 08:45:46 +00:00
high := kline . GetHigh ( )
low := kline . GetLow ( )
if s . klines . GetHigh ( ) . Compare ( high ) < 0 {
2023-03-16 11:44:58 +00:00
s . highLows = append ( s . highLows , types . DirectionUp )
2023-06-07 08:45:46 +00:00
Notify ( "[hhllStop] detected %s new higher high %f" , s . Symbol , high . Float64 ( ) )
} else if s . klines . GetLow ( ) . Compare ( low ) > 0 {
2023-03-16 11:44:58 +00:00
s . highLows = append ( s . highLows , types . DirectionDown )
2023-06-07 08:45:46 +00:00
Notify ( "[hhllStop] detected %s new lower low %f" , s . Symbol , low . Float64 ( ) )
2023-03-16 11:44:58 +00:00
} else {
s . highLows = append ( s . highLows , types . DirectionNone )
}
2023-06-07 08:45:46 +00:00
2023-03-16 11:44:58 +00:00
// 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
}
2023-06-07 08:45:46 +00:00
2023-03-16 10:30:48 +00:00
} else {
s . highLows = append ( s . highLows , types . DirectionNone )
}
s . klines . Add ( kline )
}
2023-03-16 10:39:27 +00:00
func ( s * HigherHighLowerLowStop ) shouldStop ( position * types . Position ) bool {
2023-03-20 07:56:51 +00:00
if s . klines . Len ( ) < s . Window || len ( s . highLows ) < s . HighLowWindow {
log . Debugf ( "[hhllStop] not enough data for %s yet" , s . Symbol )
return false
}
2023-03-16 10:30:48 +00:00
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
}
2023-03-16 10:39:27 +00:00
func ( s * HigherHighLowerLowStop ) Bind ( session * ExchangeSession , orderExecutor * GeneralOrderExecutor ) {
2023-04-11 07:11:11 +00:00
// 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" ) )
}
2023-03-16 10:30:48 +00:00
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
2023-04-11 06:55:32 +00:00
if s . shouldStop ( position ) {
2023-06-07 08:45:46 +00:00
defer func ( ) {
s . activated = false
} ( )
2023-03-16 10:30:48 +00:00
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 )
2023-06-07 08:45:46 +00:00
return
2023-03-16 10:30:48 +00:00
}
2023-06-07 08:45:46 +00:00
Notify ( "[hhllStop] Stop of %s triggered and position closed" , s . Symbol )
2023-03-16 10:30:48 +00:00
}
} ) )
// 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
}
} )
}