Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
32 changes: 32 additions & 0 deletions helper/skip_last.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package helper

// SkipLast skips the specified number of elements
// from the end of the given channel.
//
// Example:
//
// c := helper.SliceToChan([]int{2, 4, 6, 8})
// actual := helper.SkipLast(c, 2)
// fmt.Println(helper.ChanToSlice(actual)) // [2, 4]
func SkipLast[T any](c <-chan T, count int) <-chan T {
result := make(chan T, cap(c))

go func() {
defer close(result)

// Buffer to hold the last "count" elements
buf := make([]T, 0, count)

for v := range c {
buf = append(buf, v)
if len(buf) > count {
// send the oldest value
result <- buf[0]
buf = buf[1:]
}
}
// drop the last `count` elements automatically
}()

return result
}
23 changes: 23 additions & 0 deletions helper/skip_last_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2021-2024 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package helper_test

import (
"testing"

"github.com/cinar/indicator/v2/helper"
)

func TestSkipLast(t *testing.T) {
input := helper.SliceToChan([]int{2, 4, 6, 8})
expected := helper.SliceToChan([]int{2, 4})

actual := helper.SkipLast(input, 2)

err := helper.CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
75 changes: 75 additions & 0 deletions trend/dpo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2021-2024 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package trend

import (
"fmt"

"github.com/cinar/indicator/v2/helper"
)

// DefaultDpoPeriod is the default period for DPO calculation.
const DefaultDpoPeriod = 20

// Dpo computes the Detrended Price Oscillator.
//
// Formula (common approximation):
//
// k = period/2 + 1
// DPO = Price - SMA(Price shifted by k)
//
// Example:
//
// dpo := trend.NewDpo[float64]()
// dpo.Period = 20
// out := dpo.Compute(c)
type Dpo[T helper.Number] struct {
Period int
}

// NewDpo creates a new DPO instance with default parameters.
func NewDpo[T helper.Number]() *Dpo[T] {
return &Dpo[T]{
Period: DefaultDpoPeriod,
}
}

// NewDpoWithPeriod function initializes a new DPO instance with the given period.
func NewDpoWithPeriod[T helper.Number](period int) *Dpo[T] {
dpo := NewDpo[T]()
dpo.Period = period

return dpo

}

// Compute calculates the DPO indicator over the input price channel.
func (d *Dpo[T]) Compute(closing <-chan T) <-chan T {
closingSplice := helper.Duplicate(closing, 2)

// compute SMA on the first duplicated stream
sma := NewSma[T]()
sma.Period = d.Period
smaOut := sma.Compute(closingSplice[0])

// align the original price stream and the SMA stream according to DPO formula
skippedClosing := helper.Skip(closingSplice[1], d.IdlePeriod())
smaDelayed := helper.SkipLast(smaOut, d.Period/2+1)

// DPO = Price - shifted SMA
return helper.Operate(skippedClosing, smaDelayed, func(price, shiftedSma T) T {
return price - shiftedSma
})
}

// IdlePeriod is the initial period that DPO yield any results.
func (d *Dpo[T]) IdlePeriod() int {
return (d.Period - 1) + (d.Period/2 + 1)
}

// String is the string representation of the DPO.
func (d *Dpo[T]) String() string {
return fmt.Sprintf("DPO(%d)", d.Period)
}
42 changes: 42 additions & 0 deletions trend/dpo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package trend_test

import (
"testing"

"github.com/cinar/indicator/v2/helper"
"github.com/cinar/indicator/v2/trend"
)

func TestDpo(t *testing.T) {
type DpoData struct {
Close float64
Dpo float64
}

input, err := helper.ReadFromCsvFile[DpoData]("testdata/dpo.csv")
if err != nil {
t.Fatal(err)
}

inputs := helper.Duplicate(input, 2)
closing := helper.Map(inputs[0], func(d *DpoData) float64 { return d.Close })
expected := helper.Map(inputs[1], func(d *DpoData) float64 { return d.Dpo })

dpo := trend.NewDpo[float64]()
actual := helper.RoundDigits(dpo.Compute(closing), 2)
expected = helper.Skip(expected, dpo.IdlePeriod())

err = helper.CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}

func TestDpoString(t *testing.T) {
expected := "DPO(10)"
actual := trend.NewDpoWithPeriod[float64](10).String()

if actual != expected {
t.Fatalf("actual %v expected %v", actual, expected)
}
}
128 changes: 128 additions & 0 deletions trend/testdata/dpo.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
Close,Dpo
1111.55,00.0
1062.70,00.0
1082.90,00.0
1085.15,00.0
1082.85,00.0
1111.70,00.0
1083.25,00.0
1070.40,00.0
1080.35,00.0
1078.95,00.0
1069.40,00.0
1053.05,00.0
1057.95,00.0
1112.70,00.0
1136.20,00.0
1144.50,00.0
1142.90,00.0
1135.00,00.0
1128.30,00.0
1119.20,00.0
1137.15,00.0
1137.15,00.0
1173.50,00.0
1177.45,00.0
1188.80,00.0
1200.80,00.0
1182.45,00.0
1181.60,00.0
1196.75,00.0
1182.95,00.0
1174.75,77.30
1195.45,96.72
1199.90,97.45
1148.35,41.37
1110.65,-0.95
1131.35,14.45
1131.80,10.45
1164.55,38.24
1210.80,78.93
1232.90,95.21
1259.40,116.51
1243.10,94.94
1233.20,77.92
1239.10,76.72
1236.40,72.24
1193.00,30.12
1209.90,47.68
1216.40,54.73
1216.50,53.36
1263.10,95.83
1347.10,174.15
1320.80,141.73
1338.20,153.84
1333.10,145.75
1306.30,115.87
1362.10,169.29
1369.20,176.78
1371.80,178.01
1406.20,210.66
1407.40,210.88
1398.30,197.77
1384.60,175.45
1384.60,169.19
1371.40,149.07
1395.40,163.83
1400.20,158.85
1404.20,151.31
1411.00,146.24
1438.60,163.48
1432.80,147.91
1468.00,174.39
1432.30,131.74
1436.20,128.57
1456.70,141.50
1471.70,149.88
1467.10,137.33
1473.80,133.67
1456.40,106.56
1445.80,86.23
1405.00,34.32
1400.60,21.44
1392.30,7.09
1372.60,-18.18
1338.00,-57.68
1349.30,-52.56
1354.80,-55.33
1389.50,-25.88
1393.10,-27.51
1429.30,4.46
1440.20,13.38
1450.20,23.50
1447.00,20.18
1443.90,16.70
1431.10,4.50
1424.40,-0.53
1435.60,12.97
1448.50,28.14
1443.00,23.38
1443.10,24.37
1431.70,13.44
1433.00,14.37
1445.10,27.36
1456.70,38.22
1452.50,33.64
1443.10,25.52
1450.50,35.28
1420.70,7.06
1420.80,8.42
1411.60,-0.11
1396.30,-15.27
1376.00,-36.91
1395.90,-18.63
1394.00,-23.17
1373.10,-48.27
1347.10,-80.00
1388.90,-42.89
1358.10,-78.47
1367.10,-71.03
1345.40,-94.12
1325.00,-113.63
1339.40,-97.04
1330.50,-102.23
1319.60,-110.57
1300.30,-127.38
1327.20,-97.58
1369.40,-51.51
1370.60,-47.98