Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [sentinel] add percentile rank #1889

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/sentinel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exchangeStrategies:
- on: *exchange
sentinel: &sentinel
symbol: BTCUSDT
interval: 1m
interval: 5m
threshold: 0.6
proportion: 0.05
numSamples: 1440
Expand Down
32 changes: 28 additions & 4 deletions pkg/strategy/sentinel/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
volume := volumes.Last(0)
mean := volumes.Mean()
std := volumes.Std()
if std == 0 {
log.Warnf("symbol: %s, interval: %s, volume std is zero.", s.Symbol, s.Interval)
return
}
// if the volume is not significantly above the mean, we don't need to calculate the isolation forest
if volume < mean+2*std {
log.Infof("Volume is not significantly above mean, skipping isolation forest calculation, symbol: %s, volume: %f, mean: %f, std: %f", s.Symbol, volume, mean, std)
Expand All @@ -136,9 +140,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
scores := s.IsolationForest.Score(samples)
score := scores[len(scores)-1]
quantile := iforest.Quantile(scores, 1-s.Proportion)
log.Infof("symbol: %s, volume: %f, mean: %f, std: %f, iforest score: %f, quantile: %f", s.Symbol, volume, mean, std, score, quantile)
pr := percentileRank(score, scores)
log.Infof("symbol: %s, volume: %f, mean: %f, std: %f, iforest score: %f, quantile: %f, percentile rank: %.2f%%", s.Symbol, volume, mean, std, score, quantile, pr*100)

s.notifyOnScoreThresholdExceeded(score, quantile)
s.notifyOnScoreThresholdExceeded(score, quantile, pr)
}))
return nil
}
Expand Down Expand Up @@ -205,7 +210,7 @@ func (s *Strategy) trainIsolationForest(samples [][]float64) {
log.Infof("Isolation forest fitted with %d samples and %d/%d trees", len(samples), len(s.IsolationForest.Trees), s.IsolationForest.NumTrees)
}

func (s *Strategy) notifyOnScoreThresholdExceeded(score float64, quantile float64) {
func (s *Strategy) notifyOnScoreThresholdExceeded(score float64, quantile float64, pr float64) {
// if the score is below the threshold, we don't need to notify
if score < s.Threshold {
return
Expand All @@ -221,7 +226,7 @@ func (s *Strategy) notifyOnScoreThresholdExceeded(score float64, quantile float6
return
}

bbgo.Notify("symbol: %s, iforest score: %f, threshold: %f, quantile: %f", s.Symbol, score, s.Threshold, quantile)
bbgo.Notify("symbol: %s, iforest score: %f, threshold: %f, quantile: %f, percentile rank: %.2f%%", s.Symbol, score, s.Threshold, quantile, pr*100)
}

func (s *Strategy) isNewKline(kline types.KLine) bool {
Expand All @@ -232,3 +237,22 @@ func (s *Strategy) isNewKline(kline types.KLine) bool {
lastKline := s.klines[len(s.klines)-1]
return lastKline.EndTime.Before(kline.EndTime.Time())
}

func rankOf(value float64, values []float64) int {
rank := 0
for _, v := range values {
if v > value {
rank++
}
}
return rank
}

func percentileRank(value float64, values []float64) float64 {
n := len(values)
if n == 0 {
return 0
}
rank := rankOf(value, values)
return float64(rank) / float64(n)
}
Loading