Working on Ichimoku cloud and updated some Series functions

This commit is contained in:
Luke I. Wilson 2023-05-19 22:09:56 -05:00
parent ba92a650fb
commit 4dfc94fd5f
5 changed files with 609 additions and 81 deletions

2
go.mod
View File

@ -14,6 +14,8 @@ require (
github.com/guptarohit/asciigraph v0.5.1 // indirect
github.com/mattn/go-runewidth v0.0.7 // indirect
github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spatialcurrent/go-math v0.0.0-20211120210754-b3872f7000fe // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect
)

3
go.sum
View File

@ -200,6 +200,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -218,6 +219,8 @@ github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvH
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spatialcurrent/go-math v0.0.0-20211120210754-b3872f7000fe h1:UFsicKS0k9MUcQ77fNxUunZsMXC4ONQkWuNjEU6QLFg=
github.com/spatialcurrent/go-math v0.0.0-20211120210754-b3872f7000fe/go.mod h1:Qi3hKb+gZcrrrNW43w2A1hd6bMJyn+XezTiyCZyB1FI=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=

View File

@ -30,3 +30,43 @@ func RSI(series Series, periods int) Series {
return float64(100. - 100./(1.+val.(float64)/loss))
})
}
// Ichimoku calculates the Ichimoku Cloud for a given Series. Returns a DataFrame of the same length as the input with float64 values. The series input must contain only float64 values, which are traditionally the close prices.
//
// The standard values:
// - convPeriod: 9
// - basePeriod: 26
// - leadingPeriods: 52
//
// DataFrame columns:
// - Conversion
// - Base
// - LeadingA
// - LeadingB
// - Lagging
func Ichimoku(series Series, convPeriod, basePeriod, leadingPeriods int) *DataFrame {
// Calculate the Conversion Line.
conv := series.Rolling(convPeriod).Max().Add(series.Rolling(convPeriod).Min()).
Map(func(i int, val any) any {
return val.(float64) / float64(2)
})
// Calculate the Base Line.
base := series.Rolling(basePeriod).Max().Add(series.Rolling(basePeriod).Min()).
Map(func(i int, val any) any {
return val.(float64) / float64(2)
})
// Calculate the Leading Span A.
leadingA := conv.Rolling(leadingPeriods).Max().Add(base.Rolling(leadingPeriods).Max()).
Map(func(i int, val any) any {
return val.(float64) / float64(2)
})
// Calculate the Leading Span B.
leadingB := series.Rolling(leadingPeriods).Max().Add(series.Rolling(leadingPeriods).Min()).
Map(func(i int, val any) any {
return val.(float64) / float64(2)
})
// Calculate the Lagging Span.
// lagging := series.Shift(-leadingPeriods)
// Return a DataFrame of the results.
return NewDataFrame(conv, base, leadingA, leadingB)
}

492
series.go
View File

@ -1,11 +1,11 @@
package autotrader
import (
"fmt"
"math"
"sort"
"time"
anymath "github.com/spatialcurrent/go-math/pkg/math"
"golang.org/x/exp/slices"
)
@ -41,12 +41,27 @@ type Series interface {
SetValue(i int, val any) Series
Push(val any) Series
// Operations.
// Add returns a new Series with the values of the original Series added to the values of the other Series. It will add each value up to the length of the original Series or the other Series, whichever contains fewer values. The number of values in the new Series will remain equal to the number of values in the original Series.
Add(other Series) Series
// Sub returns a new Series with the values of the original Series subtracted from the values of the other Series. It will subtract each value up to the length of the original Series or the other Series, whichever contains fewer values. The number of values in the new Series will remain equal to the number of values in the original Series.
Sub(other Series) Series
// Mul returns a new Series with the values of the original Series multiplied by the values of the other Series. It will multiply each value up to the length of the original Series or the other Series, whichever contains fewer values. The number of values in the new Series will remain equal to the number of values in the original Series.
Mul(other Series) Series
// Div returns a new Series with the values of the original Series divided by the values of the other Series. It will divide each value up to the length of the original Series or the other Series, whichever contains fewer values. The number of values in the new Series will remain equal to the number of values in the original Series.
Div(other Series) Series
// Functional.
Filter(f func(i int, val any) bool) Series // Where returns a new Series with only the values that return true for the given function.
Map(f func(i int, val any) any) Series // Map returns a new Series with the values modified by the given function.
MapReverse(f func(i int, val any) any) Series // MapReverse is the same as Map but it starts from the last item and works backwards.
ForEach(f func(i int, val any)) Series // ForEach calls f for each item in the Series.
MaxFloat() float64 // MaxFloat returns the maximum of all floats and integers as a float64.
MaxInt() int // MaxInt returns the maximum of all integers as an int.
MinFloat() float64 // MinFloat returns the minimum of all floats and integers as a float64.
MinInt() int // MinInt returns the minimum of all integers as an int.
// Statistical functions.
@ -117,6 +132,22 @@ func (s *AppliedSeries) Push(val any) Series {
return s
}
func (s *AppliedSeries) Add(other Series) Series {
return NewAppliedSeries(s.Series.Add(other), s.apply)
}
func (s *AppliedSeries) Sub(other Series) Series {
return NewAppliedSeries(s.Series.Sub(other), s.apply)
}
func (s *AppliedSeries) Mul(other Series) Series {
return NewAppliedSeries(s.Series.Mul(other), s.apply)
}
func (s *AppliedSeries) Div(other Series) Series {
return NewAppliedSeries(s.Series.Div(other), s.apply)
}
func (s *AppliedSeries) Filter(f func(i int, val any) bool) Series {
return NewAppliedSeries(s.Series.Filter(f), s.apply)
}
@ -183,6 +214,22 @@ func (s *RollingSeries) Push(val any) Series {
return s
}
func (s *RollingSeries) Add(other Series) Series {
return NewRollingSeries(s.Series.Add(other), s.period)
}
func (s *RollingSeries) Sub(other Series) Series {
return NewRollingSeries(s.Series.Sub(other), s.period)
}
func (s *RollingSeries) Mul(other Series) Series {
return NewRollingSeries(s.Series.Mul(other), s.period)
}
func (s *RollingSeries) Div(other Series) Series {
return NewRollingSeries(s.Series.Div(other), s.period)
}
func (s *RollingSeries) Filter(f func(i int, val any) bool) Series {
return NewRollingSeries(s.Series.Filter(f), s.period)
}
@ -200,110 +247,252 @@ func (s *RollingSeries) ForEach(f func(i int, val any)) Series {
return s
}
// Max returns an AppliedSeries that returns the maximum value of the rolling period as a float64 or 0 if the requested period is empty.
//
// Will work with all signed int and float types. Ignores all other values.
func (s *RollingSeries) Max() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v any) any {
switch v := v.(type) {
case []any:
if len(v) == 0 {
return nil
}
max := math.Inf(-1)
for _, v := range v {
switch v := v.(type) {
case float64:
if v > max {
max = v
}
case float32:
if float64(v) > max {
max = float64(v)
}
case int:
if float64(v) > max {
max = float64(v)
}
case int64:
if float64(v) > max {
max = float64(v)
}
case int32:
if float64(v) > max {
max = float64(v)
}
case int16:
if float64(v) > max {
max = float64(v)
}
case int8:
if float64(v) > max {
max = float64(v)
}
}
return max
}
}
panic("unreachable")
})
}
// Min returns an AppliedSeries that returns the minimum value of the rolling period as a float64 or 0 if the requested period is empty.
//
// Will work with all signed int and float types. Ignores all other values.
func (s *RollingSeries) Min() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v any) any {
switch v := v.(type) {
case []any:
if len(v) == 0 {
return nil
}
min := math.Inf(1)
for _, v := range v {
switch v := v.(type) {
case float64:
if v < min {
min = v
}
case float32:
if float64(v) < min {
min = float64(v)
}
case int:
if float64(v) < min {
min = float64(v)
}
case int64:
if float64(v) < min {
min = float64(v)
}
case int32:
if float64(v) < min {
min = float64(v)
}
case int16:
if float64(v) < min {
min = float64(v)
}
case int8:
if float64(v) < min {
min = float64(v)
}
}
return min
}
}
panic("unreachable")
})
}
// Average is an alias for Mean.
func (s *RollingSeries) Average() *AppliedSeries {
return s.Mean()
}
// Mean returns the mean of the rolling period as a float64 or 0 if the period requested is empty.
//
// Will work with all signed int and float types. Ignores all other values.
func (s *RollingSeries) Mean() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v any) any {
switch v := v.(type) {
case []any:
if len(v) == 0 {
return nil
return 0
}
switch v[0].(type) {
case float64:
var sum float64
for _, v := range v {
sum += v.(float64)
var sum float64
for _, v := range v {
switch v := v.(type) {
case float64:
sum += v
case float32:
sum += float64(v)
case int:
sum += float64(v)
case int64:
sum += float64(v)
case int32:
sum += float64(v)
case int16:
sum += float64(v)
case int8:
sum += float64(v)
}
return sum / float64(len(v))
case int64:
var sum int64
for _, v := range v {
sum += v.(int64)
}
return sum / int64(len(v))
default:
return v[len(v)-1] // Do nothing
}
default:
panic(fmt.Sprintf("expected a slice of values, got %t", v))
return sum / float64(len(v))
}
panic("unreachable")
})
}
// EMA returns the exponential moving average of the period as a float64 or 0 if the period requested is empty.
//
// Will work with all signed int and float types. Ignores all other values.
func (s *RollingSeries) EMA() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v any) any {
switch v := v.(type) {
case []any:
if len(v) == 0 {
return nil
return 0
}
switch v[0].(type) {
case float64:
ema := v[0].(float64)
for _, v := range v[1:] {
ema += (v.(float64) - ema) * 2 / (float64(s.period) + 1)
var ema float64
period := float64(s.period)
first := true
for _, v := range v {
var f float64
switch v := v.(type) {
case float64:
f = v
case float32:
f = float64(v)
case int:
f = float64(v)
case int64:
f = float64(v)
case int32:
f = float64(v)
case int16:
f = float64(v)
case int8:
f = float64(v)
default:
continue
}
return ema
case int64:
ema := v[0].(int64)
for _, v := range v[1:] {
ema += (v.(int64) - ema) * 2 / (int64(s.period) + 1)
if first { // Set as first value
ema = f
first = false
continue
}
return ema
default: // string, time.Time
return v[len(v)-1] // Do nothing
ema += (f - ema) * 2 / (period + 1)
}
default:
panic(fmt.Sprintf("expected a slice of values, got %t", v))
return ema
}
panic("unreachable")
})
}
// Median returns the median of the period as a float64 or 0 if the period requested is empty.
//
// Will work with float64 and int. Ignores all other values.
func (s *RollingSeries) Median() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v any) any {
switch v := v.(type) {
case []any:
if len(v) == 0 {
return nil
return 0
}
switch v[0].(type) {
var offenders int
slices.SortFunc(v, func(a, b any) bool {
less, offender := LessAny(a, b)
// Sort offenders to the end.
if offender == a {
offenders++
return false
} else if offender == b {
offenders++
return true
}
return less
})
v = v[:len(v)-offenders] // Cut out the offenders.
v1 := v[len(v)/2-1]
v2 := v[len(v)/2]
if len(v)%2 == 0 {
switch n1 := v1.(type) {
case float64:
switch n2 := v2.(type) {
case float64:
return (n1 + n2) / 2
case int:
return (n1 + float64(n2)) / 2
}
case int:
switch n2 := v2.(type) {
case float64:
return (float64(n1) + n2) / 2
case int:
return (float64(n1) + float64(n2)) / 2
}
default:
return 0
}
}
switch vMid := v[len(v)/2].(type) {
case float64:
if len(v) == 0 {
return float64(0)
}
slices.SortFunc(v, func(a, b any) bool {
x, y := a.(float64), b.(float64)
return x < y || (math.IsNaN(x) && !math.IsNaN(y))
})
if len(v)%2 == 0 {
return (v[len(v)/2-1].(float64) + v[len(v)/2].(float64)) / 2
}
return v[len(v)/2]
case int64:
if len(v) == 0 {
return int64(0)
}
slices.SortFunc(v, func(a, b any) bool {
x, y := a.(int64), b.(int64)
return x < y
})
if len(v)%2 == 0 {
return (v[len(v)/2-1].(int64) + v[len(v)/2].(int64)) / 2
}
return v[len(v)/2]
default: // string, time.Time
return v[len(v)-1] // Do nothing
return vMid
case int:
return float64(vMid)
default:
panic("unreachable") // Offenders are pushed to the back of the slice and ignored.
}
default:
panic(fmt.Sprintf("expected a slice of values, got %t", v))
}
panic("unreachable")
})
}
// StdDev returns the standard deviation of the period as a float64 or 0 if the period requested is empty.
func (s *RollingSeries) StdDev() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v any) any {
switch v := v.(type) {
@ -311,27 +500,36 @@ func (s *RollingSeries) StdDev() *AppliedSeries {
if len(v) == 0 {
return nil
}
switch v[0].(type) {
case float64:
mean := s.Mean().Value(i).(float64) // Take the mean of the last period values for the current index
var sum float64
for _, v := range v {
sum += (v.(float64) - mean) * (v.(float64) - mean)
mean := s.Mean().Value(i).(float64) // Take the mean of the last period values for the current index
var sum float64
var ignored int
for _, v := range v {
switch v := v.(type) {
case float64:
sum += (v - mean) * (v - mean)
case float32:
sum += (float64(v) - mean) * (float64(v) - mean)
case int:
sum += (float64(v) - mean) * (float64(v) - mean)
case int64:
sum += (float64(v) - mean) * (float64(v) - mean)
case int32:
sum += (float64(v) - mean) * (float64(v) - mean)
case int16:
sum += (float64(v) - mean) * (float64(v) - mean)
case int8:
sum += (float64(v) - mean) * (float64(v) - mean)
default:
ignored++
}
return math.Sqrt(sum / float64(len(v)))
case int64:
mean := s.Mean().Value(i).(int64)
var sum int64
for _, v := range v {
sum += (v.(int64) - mean) * (v.(int64) - mean)
}
return int64(math.Sqrt(float64(sum) / float64(len(v))))
default: // A slice of something else, just return the last value
return v[len(v)-1] // Do nothing
}
default:
panic(fmt.Sprintf("expected a slice of values, got %t", v))
if ignored >= len(v) {
return 0
}
return math.Sqrt(sum / float64(len(v)-ignored))
}
panic("unreachable")
})
}
@ -530,6 +728,58 @@ func (s *DataSeries) Time(i int) time.Time {
}
}
func (s *DataSeries) Add(other Series) Series {
rows := make([]any, 0, s.Len())
copy(rows, s.data)
for i := 0; i < s.Len() && i < other.Len(); i++ {
val, err := anymath.Add(s.value(i), other.Value(i))
if err != nil {
continue
}
rows[i] = val
}
return NewDataSeries(s.name, rows...)
}
func (s *DataSeries) Sub(other Series) Series {
rows := make([]any, 0, s.Len())
copy(rows, s.data)
for i := 0; i < s.Len() && i < other.Len(); i++ {
val, err := anymath.Subtract(s.value(i), other.Value(i))
if err != nil {
continue
}
rows[i] = val
}
return NewDataSeries(s.name, rows...)
}
func (s *DataSeries) Mul(other Series) Series {
rows := make([]any, 0, s.Len())
copy(rows, s.data)
for i := 0; i < s.Len() && i < other.Len(); i++ {
val, err := anymath.Multiply(s.value(i), other.Value(i))
if err != nil {
continue
}
rows[i] = val
}
return NewDataSeries(s.name, rows...)
}
func (s *DataSeries) Div(other Series) Series {
rows := make([]any, 0, s.Len())
copy(rows, s.data)
for i := 0; i < s.Len() && i < other.Len(); i++ {
val, err := anymath.Divide(s.value(i), other.Value(i))
if err != nil {
continue
}
rows[i] = val
}
return NewDataSeries(s.name, rows...)
}
func (s *DataSeries) Filter(f func(i int, val any) bool) Series {
series := NewDataSeries(s.name, make([]any, 0, s.Len())...)
for i := 0; i < s.Len(); i++ {
@ -565,6 +815,86 @@ func (s *DataSeries) ForEach(f func(i int, val any)) Series {
return s
}
func (s *DataSeries) MaxFloat() float64 {
if s.Len() == 0 {
return 0
}
max := math.Inf(-1)
for i := 0; i < s.Len(); i++ {
switch val := s.value(i).(type) {
case float64:
if val > max {
max = val
}
case int:
if float64(val) > max {
max = float64(val)
}
}
}
return max
}
func (s *DataSeries) MinFloat() float64 {
if s.Len() == 0 {
return 0
}
min := math.Inf(1)
for i := 0; i < s.Len(); i++ {
switch val := s.value(i).(type) {
case float64:
if val < min {
min = val
}
case int:
if float64(val) < min {
min = float64(val)
}
}
}
return min
}
func (s *DataSeries) MaxInt() int {
if s.Len() == 0 {
return 0
}
max := math.MinInt64
for i := 0; i < s.Len(); i++ {
switch val := s.value(i).(type) {
case int:
if val > max {
max = val
}
case float64:
if int(val) > max {
max = int(val)
}
}
}
return max
}
func (s *DataSeries) MinInt() int {
if s.Len() == 0 {
return 0
}
min := math.MaxInt64
for i := 0; i < s.Len(); i++ {
switch val := s.value(i).(type) {
case int:
if val < min {
min = val
}
case float64:
if int(val) < min {
min = int(val)
}
}
}
return min
}
func (s *DataSeries) Rolling(period int) *RollingSeries {
return NewRollingSeries(s, period)
}

153
utils.go
View File

@ -1,6 +1,7 @@
package autotrader
import (
"errors"
"math"
"os/exec"
"runtime"
@ -10,6 +11,8 @@ import (
const float64Tolerance = float64(1e-6)
var ErrNotASignedNumber = errors.New("not a signed number")
// Crossover returns true if the latest a value crosses above the latest b value, but only if it just happened. For example, if a series is [1, 2, 3, 4, 5] and b series is [1, 2, 3, 4, 3], then Crossover(a, b) returns false because the latest a value is 5 and the latest b value is 3. However, if a series is [1, 2, 3, 4, 5] and b series is [1, 2, 3, 4, 6], then Crossover(a, b) returns true because the latest a value is 5 and the latest b value is 6
func Crossover(a, b Series) bool {
return a.Float(-1) > b.Float(-1) && a.Float(-2) <= b.Float(-2)
@ -93,3 +96,153 @@ func Open(url string) error {
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
// LessAny returns true if a < b. a and b must be signed numbers. If a or b is not a signed number, then the function returns false, and the value that was first identified as not a signed number as the interface{} alias 'any'. The order of checking is a -> b. If a is not a signed number, then a is returned as the offender. Else if b is not a signed number, then b is returned as the offender. Else, nil is returned as the offender.
//
// A signed number is any of the following types:
//
// - float64
// - float32
// - int
// - int64
// - int32
// - int16
// - int8
func LessAny(a, b any) (less bool, offender any) {
switch a := a.(type) {
case float64:
switch b := b.(type) {
case float64:
return a < b, nil
case float32:
return a < float64(b), nil
case int:
return a < float64(b), nil
case int64:
return a < float64(b), nil
case int32:
return a < float64(b), nil
case int16:
return a < float64(b), nil
case int8:
return a < float64(b), nil
default:
return false, b
}
case float32:
switch b := b.(type) {
case float64:
return float64(a) < b, nil
case float32:
return a < b, nil
case int:
return float64(a) < float64(b), nil
case int64:
return float64(a) < float64(b), nil
case int32:
return a < float32(b), nil
case int16:
return a < float32(b), nil
case int8:
return a < float32(b), nil
default:
return false, b
}
case int:
switch b := b.(type) {
case float64:
return float64(a) < b, nil
case float32:
return float64(a) < float64(b), nil
case int:
return a < b, nil
case int64:
return int64(a) < b, nil
case int32:
return a < int(b), nil
case int16:
return a < int(b), nil
case int8:
return a < int(b), nil
default:
return false, b
}
case int64:
switch b := b.(type) {
case float64:
return float64(a) < b, nil
case float32:
return float64(a) < float64(b), nil
case int:
return a < int64(b), nil
case int64:
return a < b, nil
case int32:
return a < int64(b), nil
case int16:
return a < int64(b), nil
case int8:
return a < int64(b), nil
default:
return false, b
}
case int32:
switch b := b.(type) {
case float64:
return float64(a) < b, nil
case float32:
return float32(a) < float32(b), nil
case int:
return int(a) < b, nil
case int64:
return int64(a) < b, nil
case int32:
return a < b, nil
case int16:
return a < int32(b), nil
case int8:
return a < int32(b), nil
default:
return false, b
}
case int16:
switch b := b.(type) {
case float64:
return float64(a) < b, nil
case float32:
return float32(a) < b, nil
case int:
return int(a) < b, nil
case int64:
return int64(a) < b, nil
case int32:
return int32(a) < b, nil
case int16:
return a < b, nil
case int8:
return a < int16(b), nil
default:
return false, b
}
case int8:
switch b := b.(type) {
case float64:
return float64(a) < b, nil
case float32:
return float32(a) < b, nil
case int:
return int(a) < b, nil
case int64:
return int64(a) < b, nil
case int32:
return int32(a) < b, nil
case int16:
return int16(a) < b, nil
case int8:
return a < b, nil
default:
return false, b
}
}
return false, a
}