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: [indicator] adding a bunch of new v2 indicators #1366

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*.out

.idea
.vscode

# Dependency directories (remove the comment below to include it)
# vendor/
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module github.com/c9s/bbgo

go 1.18
go 1.20

require (
github.com/DATA-DOG/go-sqlmock v1.5.0
Expand Down
2 changes: 1 addition & 1 deletion pkg/bbgo/indicator_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package bbgo
import (
"github.com/sirupsen/logrus"

"github.com/c9s/bbgo/pkg/indicator/v2"
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
"github.com/c9s/bbgo/pkg/types"
)

Expand Down
23 changes: 23 additions & 0 deletions pkg/datatype/floats/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ func (s Slice) Max() float64 {
return floats.Max(s)
}

func (s Slice) IndexOfMaxValue() int {
maxIdx := 0
maxVal := s[0]
for i, val := range s {
if val > maxVal {
maxVal = val
maxIdx = i
}
}
return maxIdx
}

func (s Slice) Min() float64 {
return floats.Min(s)
}
Expand Down Expand Up @@ -87,6 +99,17 @@ func (s Slice) Mean() (mean float64) {
return s.Sum() / float64(length)
}

/* Calculates the variance across the dataset of float64s */
func (s Slice) Variance() float64 {
var variance = .0

for _, diff := range s {
variance += math.Pow(diff-s.Mean(), 2)
}

return variance / float64(len(s))
}

func (s Slice) Tail(size int) Slice {
length := len(s)
if length <= size {
Expand Down
1 change: 1 addition & 0 deletions pkg/exchange/bitget/bitgetapi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
OrderTypeMarket OrderType = "market"
)

// OrderSide represents the side of an order: Buy (long) or Sell (short).
type OrderSide string

const (
Expand Down
13 changes: 13 additions & 0 deletions pkg/fixedpoint/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ func Avg(values []Value) (avg Value) {
avg = s.Div(NewFromInt(int64(len(values))))
return avg
}

// maxDiff is the maximum deviation between a and b to consider them approximately equal
func ApproxEqual(a, b Value, maxDiff float64) bool {
// Calculate the absolute difference
diff := Abs(a.Sub(b))

// Define the small multiple
smallMultiple := a.Mul(NewFromFloat(maxDiff))

// Compare the absolute difference to the small multiple
cmp := diff.Compare(smallMultiple)
return cmp == -1 || cmp == 0
}
2 changes: 1 addition & 1 deletion pkg/indicator/util.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package indicator

func max(x, y int) int {
func Max(x, y int) int {
if x > y {
return x
}
Expand Down
68 changes: 68 additions & 0 deletions pkg/indicator/v2/abondoned_baby.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package indicatorv2

import (
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

type AbondonedBabyStream struct {
*types.Float64Series

window int
}

func AbondonedBaby(source KLineSubscription) *AbondonedBabyStream {
s := &AbondonedBabyStream{
Float64Series: types.NewFloat64Series(),
window: 3,
}

source.AddSubscriber(func(kLine types.KLine) {

var (
i = source.Length()
output = Neutral
)
if i < s.window {
s.PushAndEmit(output)
return
}
var (
one = source.Last(2)
two = source.Last(1)
three = source.Last(0)
abs = fixedpoint.Abs((two.Close.Sub(two.Open).Div(two.Open))).Float64()
)

if one.Open.Float64() < one.Close.Float64() {
if one.High.Float64() < two.Low.Float64() {
if abs < threshold {
if three.Open.Float64() < two.Low.Float64() &&
three.Close.Float64() < three.Open.Float64() {
output = -1.0
}
}
}
}

if one.Open.Float64() > one.Close.Float64() {
if one.Low.Float64() > two.High.Float64() {
if abs <= threshold {
if three.Open.Float64() > two.High.Float64() &&
three.Close.Float64() > three.Open.Float64() {
output = 1.0
}
}
}
}

s.Float64Series.PushAndEmit(output)

})

return s
}

func (s *AbondonedBabyStream) Truncate() {
s.Slice = s.Slice.Truncate(MaxNumOfPattern)
}
28 changes: 28 additions & 0 deletions pkg/indicator/v2/abondoned_baby_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package indicatorv2

import (
"testing"

"github.com/c9s/bbgo/pkg/types"
)

func TestAbandonedBaby(t *testing.T) {
ts := []types.KLine{
{Open: n(90), Low: n(85), High: n(105), Close: n(100)},
{Open: n(125), Low: n(120), High: n(135), Close: n(130)},
{Open: n(110), Low: n(92), High: n(115), Close: n(95)},
}

stream := &types.StandardStream{}
kLines := KLines(stream, "", "")
ind := AbondonedBaby(kLines)

for _, candle := range ts {
stream.EmitKLineClosed(candle)
}
expectedBear := -1.0

if ind.Last(0) != expectedBear {
t.Errorf("TestAbandonedBaby Bear unexpected result: got %v want %v", ind.Last(0), expectedBear)
}
}
47 changes: 47 additions & 0 deletions pkg/indicator/v2/accumulation_distiribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package indicatorv2

import (
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

// Accumulation/Distribution Indicator (A/D). Cumulative indicator
// that uses volume and price to assess whether a stock is
// being accumulated or distributed.
//
// MFM = ((Closing - Low) - (High - Closing)) / (High - Low)
// MFV = MFM * Period Volume
// AD = Previous AD + CMFV
type AccumulationDistributionStream struct {
*types.Float64Series
}

func AccumulationDistribution(source KLineSubscription) *AccumulationDistributionStream {
s := &AccumulationDistributionStream{
Float64Series: types.NewFloat64Series(),
}

source.AddSubscriber(func(v types.KLine) {
var (
i = s.Slice.Length()
output = fixedpoint.NewFromInt(0)
cl = v.Close.Sub(v.Low)
hc = v.High.Sub(v.Close)
hl = v.High.Sub(v.Low)
)

if i > 0 {
output = fixedpoint.NewFromFloat(s.Slice.Last(0))
}

output = output.Add(v.Volume.Mul(cl.Sub(hc).Div(hl)))

s.PushAndEmit(output.Float64())
})

return s
}

func (s *AccumulationDistributionStream) Truncate() {
s.Slice = s.Slice.Truncate(MaxNumOfMA)
}
41 changes: 41 additions & 0 deletions pkg/indicator/v2/accumulation_distiribution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package indicatorv2

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"

"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

func TestAccumulationDistribution(t *testing.T) {
high := []byte(`[62.3400,62.0500,62.2700,60.7900,59.9300,61.7500,60.0000,59.0000,59.0700,59.2200,58.7500,58.6500,58.4700,58.2500,58.3500,59.8600,59.5299,62.1000,62.1600,62.6700,62.3800,63.7300,63.8500,66.1500,65.3400,66.4800,65.2300,63.4000,63.1800,62.7000]`)
low := []byte(`[61.3700,60.6900,60.1000,58.6100,58.7120,59.8600,57.9700,58.0200,57.4800,58.3000,57.8276,57.8600,57.9100,57.8333,57.5300,58.5800,58.3000,58.5300,59.8000,60.9300,60.1500,62.2618,63.0000,63.5800,64.0700,65.2000,63.2100,61.8800,61.1100,61.2500]`)
close := []byte(`[62.1500,60.8100,60.4500,59.1800,59.2400,60.2000,58.4800,58.2400,58.6900,58.6500,58.4700,58.0200,58.1700,58.0700,58.1300,58.9400,59.1000,61.9200,61.3700,61.6800,62.0900,62.8900,63.5300,64.0100,64.7700,65.2200,63.2800,62.4000,61.5500,62.6900]`)
volume := []byte(`[7849.025,11692.075,10575.307,13059.128,20733.508,29630.096,17705.294,7259.203,10474.629,5203.714,3422.865,3962.15,4095.905,3766.006,4239.335,8039.979,6956.717,18171.552,22225.894,14613.509,12319.763,15007.69,8879.667,22693.812,10191.814,10074.152,9411.62,10391.69,8926.512,7459.575]`)
buildKLines := func(high, low, close, volume []fixedpoint.Value) (kLines []types.KLine) {
for i := range high {
kLines = append(kLines, types.KLine{High: high[i], Low: low[i], Close: close[i], Volume: volume[i]})
}
return kLines
}
var h, l, c, v []fixedpoint.Value
_ = json.Unmarshal(high, &h)
_ = json.Unmarshal(low, &l)
_ = json.Unmarshal(close, &c)
_ = json.Unmarshal(volume, &v)

expected := []float64{4774, -4855, -12019, -18249, -21006, -39976, -48785, -52785, -47317, -48561, -47216, -49574, -49866, -49354, -47389, -50907, -48813, -32474, -25128, -27144, -18028, -20193, -18000, -33099, -32056, -41816, -50575, -53856, -58988, -51631}
stream := &types.StandardStream{}
kLines := KLines(stream, "", "")
ind := AccumulationDistribution(kLines)
k := buildKLines(h, l, c, v)
for _, candle := range k {
stream.EmitKLineClosed(candle)
}
for i, v := range expected {
assert.InDelta(t, v, ind.Slice[i], 0.5, "Expected AccumulationDistribution.slice[%d] to be %v, but got %v", i, v, ind.Slice[i])
}
}
76 changes: 76 additions & 0 deletions pkg/indicator/v2/alma.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2022 The Coln Group Ltd
// SPDX-License-Identifier: MIT

package indicatorv2

import (
"math"

"github.com/c9s/bbgo/pkg/types"
)

const (
// DefaultALMAOffset is the default offset for the ALMA indicator.
DefaultALMAOffset = 0.85

// DefaultALMASigma is the default sigma for the ALMA indicator.
DefaultALMASigma = 6
)

type ALMAStream struct {
// embedded struct
*types.Float64Series

sample []float64
offset float64
sigma float64
window int
}

// ALMA is a modern low lag moving average.
// Ported from https://www.tradingview.com/pine-script-reference/#fun_alma
// NewALMA creates a new ALMA indicator with default parameters.
func ALMA(source types.Float64Source, window int) *ALMAStream {
return ALMAWithSigma(source, window, DefaultALMAOffset, DefaultALMASigma)
}

// NewALMAWithSigma creates a new ALMA indicator with the given offset and sigma.
func ALMAWithSigma(source types.Float64Source, window int, offset, sigma float64) *ALMAStream {
s := &ALMAStream{
Float64Series: types.NewFloat64Series(),
window: window,
offset: offset,
sigma: sigma,
}
s.Bind(source, s)
return s
}

func (s *ALMAStream) Calculate(v float64) float64 {

s.sample = WindowAppend(s.sample, s.window-1, v)

if s.window < 1 {
return v
}
var (
length = float64(s.window)
norm, sum float64
offset = s.offset * (length - 1)
m = math.Floor(offset)
sig = length / s.sigma
)
for i := 0; i < len(s.sample); i++ {
pow := math.Pow(float64(i)-m, 2) / (math.Pow(sig, 2) * 2)
weight := math.Exp(-1 * pow)
norm += weight
sum += s.sample[i] * weight
}
ma := sum / norm

return ma
}

func (s *ALMAStream) Truncate() {
s.Slice = s.Slice.Truncate(MaxNumOfMA)
}
Loading