diff --git a/indicators.go b/indicators.go index 2ea0ab4..be316b0 100644 --- a/indicators.go +++ b/indicators.go @@ -1,6 +1,9 @@ package autotrader -import "math" +import ( + "math" + "time" +) // RSI calculates the Relative Strength Index for a given Series. Typically, the input series is the Close column of a DataFrame. Returns a Series of RSI values of the same length as the input. // @@ -22,6 +25,7 @@ func RSI(series *FloatSeries, periods int) *FloatSeries { avgLoss := &FloatSeries{delta.Copy(). Map(func(i int, val float64) float64 { return math.Abs(math.Min(val, 0)) }). Rolling(periods).Average()} + // Calculate the RSI. return avgGain.Map(func(i int, val float64) float64 { loss := avgLoss.Float(i) @@ -29,7 +33,7 @@ func RSI(series *FloatSeries, periods int) *FloatSeries { return float64(100) } return float64(100 - 100/(1+val/loss)) - }) + }).SetName("RSI") } // 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. @@ -45,29 +49,40 @@ func RSI(series *FloatSeries, periods int) *FloatSeries { // - LeadingA // - LeadingB // - Lagging -func Ichimoku(series *FloatSeries, convPeriod, basePeriod, leadingPeriods int) *Frame { +func Ichimoku(series *IndexedSeries[UnixTime], convPeriod, basePeriod, leadingPeriods int) *IndexedFrame[UnixTime] { + // TODO: make this run concurrently. + // Calculate the Conversion Line. conv := series.Copy().Rolling(convPeriod).Max().Add(series.Copy().Rolling(convPeriod).Min()). - Map(func(i int, val any) any { + Map(func(_ UnixTime, _ int, val any) any { return val.(float64) / float64(2) }) // Calculate the Base Line. base := series.Copy().Rolling(basePeriod).Max().Add(series.Copy().Rolling(basePeriod).Min()). - Map(func(i int, val any) any { + Map(func(_ UnixTime, _ int, val any) any { return val.(float64) / float64(2) }) + // Calculate the Leading Span A. leadingA := conv.Copy().Rolling(leadingPeriods).Max().Add(base.Copy().Rolling(leadingPeriods).Max()). - Map(func(i int, val any) any { + Map(func(_ UnixTime, _ int, val any) any { return val.(float64) / float64(2) }) // Calculate the Leading Span B. leadingB := series.Copy().Rolling(leadingPeriods).Max().Add(series.Copy().Rolling(leadingPeriods).Min()). - Map(func(i int, val any) any { + Map(func(_ UnixTime, _ int, val any) any { return val.(float64) / float64(2) }) + // Calculate the Lagging Span. - // lagging := series.Shift(-leadingPeriods) + lagging := series.Copy().ShiftIndex(-leadingPeriods, UnixTimeStep(time.Hour)) + // Return a DataFrame of the results. - return NewFrame(conv, base, leadingA, leadingB) + return NewIndexedFrame( + conv.SetName("Conversion"), + base.SetName("Base"), + leadingA.SetName("LeadingA"), + leadingB.SetName("LeadingB"), + lagging.SetName("Lagging"), + ) } diff --git a/series.go b/series.go index e3c29cf..3d68931 100644 --- a/series.go +++ b/series.go @@ -435,14 +435,14 @@ func NewRollingSeries(series *Series, period int) *RollingSeries { return &RollingSeries{series, period} } -// Period returns a slice of 'any' values with a length up to the period of the RollingSeries. The last item in the slice is the item at i. If i is out of bounds, nil is returned. -func (s *RollingSeries) Period(i int) []any { +// Period returns a slice of 'any' values with a length up to the period of the RollingSeries. The last item in the slice is the item at row. If row is out of bounds, nil is returned. +func (s *RollingSeries) Period(row int) []any { items := make([]any, 0, s.period) - i = EasyIndex(i, s.series.Len()) - if i < 0 || i >= s.series.Len() { + row = EasyIndex(row, s.series.Len()) + if row < 0 || row >= s.series.Len() { return items } - for j := i; j > i-s.period && j >= 0; j-- { + for j := row; j > row-s.period && j >= 0; j-- { items = slices.Insert(items, 0, s.series.Value(j)) } return items diff --git a/series_indexed.go b/series_indexed.go index 435b9ea..992a7b1 100644 --- a/series_indexed.go +++ b/series_indexed.go @@ -89,6 +89,18 @@ func (s *IndexedSeries[I]) Filter(f func(i int, val any) bool) *IndexedSeries[I] return s } +func (s *IndexedSeries[I]) Float(i int) float64 { + return s.series.Float(i) +} + +func (s *IndexedSeries[I]) FloatIndex(index I) float64 { + row := s.Row(index) + if row < 0 { + return 0.0 + } + return s.series.Float(row) +} + func (s *IndexedSeries[I]) ForEach(f func(i int, val any)) *IndexedSeries[I] { _ = s.series.ForEach(f) return s @@ -240,6 +252,10 @@ func (s *IndexedSeries[I]) ReverseIndexes() *IndexedSeries[I] { return s } +func (s *IndexedSeries[I]) Rolling(period int) *IndexedRollingSeries[I] { + return NewIndexedRollingSeries(s, period) +} + func (s *IndexedSeries[I]) SetName(name string) *IndexedSeries[I] { _ = s.series.SetName(name) return s @@ -314,3 +330,51 @@ func (s *IndexedSeries[I]) Values() []any { func (s *IndexedSeries[I]) ValueRange(start, count int) []any { return s.series.ValueRange(start, count) } + +type IndexedRollingSeries[I comparable] struct { + rolling *RollingSeries + series *IndexedSeries[I] +} + +func NewIndexedRollingSeries[I comparable](series *IndexedSeries[I], period int) *IndexedRollingSeries[I] { + return &IndexedRollingSeries[I]{NewRollingSeries(series.series, period), series} +} + +func (s *IndexedRollingSeries[I]) Period(row int) []any { + return s.rolling.Period(row) +} + +func (s *IndexedRollingSeries[I]) Max() *IndexedSeries[I] { + _ = s.rolling.Max() // Mutate the underlying series. + return s.series +} + +func (s *IndexedRollingSeries[I]) Min() *IndexedSeries[I] { + _ = s.rolling.Min() // Mutate the underlying series. + return s.series +} + +func (s *IndexedRollingSeries[I]) Average() *IndexedSeries[I] { + _ = s.rolling.Average() // Mutate the underlying series. + return s.series +} + +func (s *IndexedRollingSeries[I]) Mean() *IndexedSeries[I] { + _ = s.rolling.Mean() // Mutate the underlying series. + return s.series +} + +func (s *IndexedRollingSeries[I]) Median() *IndexedSeries[I] { + _ = s.rolling.Median() // Mutate the underlying series. + return s.series +} + +func (s *IndexedRollingSeries[I]) EMA() *IndexedSeries[I] { + _ = s.rolling.EMA() // Mutate the underlying series. + return s.series +} + +func (s *IndexedRollingSeries[I]) StdDev() *IndexedSeries[I] { + _ = s.rolling.StdDev() // Mutate the underlying series. + return s.series +} diff --git a/utils.go b/utils.go index 40294d0..eabeb41 100644 --- a/utils.go +++ b/utils.go @@ -14,10 +14,18 @@ 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 { +func Crossover(a, b *Series) bool { return a.Float(-1) > b.Float(-1) && a.Float(-2) <= b.Float(-2) } +func CrossoverIndex[I comparable](index I, a, b *IndexedSeries[I]) bool { + aRow, bRow := a.Row(index), b.Row(index) + if aRow < 1 || bRow < 1 { + return false + } + return a.Float(aRow) > b.Float(bRow) && a.Float(aRow-1) <= b.Float(bRow-1) +} + // EasyIndex returns an index to the `n` -length object that allows for negative indexing. For example, EasyIndex(-1, 5) returns 4. This is similar to Python's negative indexing. The return value may be less than zero if (-i) > n. func EasyIndex(i, n int) int { if i < 0 {