Add indexed frame shifting

This commit is contained in:
Luke I. Wilson 2023-05-21 12:52:25 -05:00
parent ee67d5c170
commit 30b6482fbf
4 changed files with 228 additions and 112 deletions

View File

@ -20,6 +20,12 @@ func (t UnixTime) String() string {
return t.Time().String()
}
func UnixTimeStep(frequency time.Duration) func(UnixTime, int) UnixTime {
return func(t UnixTime, amt int) UnixTime {
return UnixTime(t.Time().Add(frequency * time.Duration(amt)).Unix())
}
}
// It is worth mentioning that if you want to use time.Time as an index type, then you should use the public UnixTime as a Unix int64 time which can be converted back into a time.Time easily. See [time.Time](https://pkg.go.dev/time#Time) for more information on why you should not compare Time with == (or a map, which is what the IndexedFrame uses).
type IndexedFrame[I comparable] struct {
*SignalManager
@ -28,12 +34,12 @@ type IndexedFrame[I comparable] struct {
// It is worth mentioning that if you want to use time.Time as an index type, then you should use int64 as a Unix time. See [time.Time](https://pkg.go.dev/time#Time) for more information on why you should not compare Time with == (or a map, which is what the IndexedFrame uses).
func NewIndexedFrame[I comparable](series ...*IndexedSeries[I]) *IndexedFrame[I] {
d := &IndexedFrame[I]{
f := &IndexedFrame[I]{
&SignalManager{},
make(map[string]*IndexedSeries[I], len(series)),
}
d.PushSeries(series...)
return d
f.PushSeries(series...)
return f
}
// NewDOHLCVIndexedFrame returns a IndexedFrame with empty Date, Open, High, Low, Close, and Volume columns.
@ -49,8 +55,8 @@ func NewDOHLCVIndexedFrame[I comparable]() *IndexedFrame[I] {
}
// Copy is the same as CopyRange(0, -1)
func (d *IndexedFrame[I]) Copy() *IndexedFrame[I] {
return d.CopyRange(0, -1)
func (f *IndexedFrame[I]) Copy() *IndexedFrame[I] {
return f.CopyRange(0, -1)
}
// Copy returns a new IndexedFrame with a copy of the original series. start is an EasyIndex and count is the number of rows to copy from start onward. If count is negative then all rows from start to the end of the IndexedFrame are copied. If there are not enough rows to copy then the maximum amount is returned. If there are no items to copy then a IndexedFrame will be returned with a length of zero but with the same column names as the original.
@ -60,21 +66,21 @@ func (d *IndexedFrame[I]) Copy() *IndexedFrame[I] {
// Copy(0, 10) - copy the first 10 rows
// Copy(-1, 1) - copy the last row
// Copy(-10, -1) - copy the last 10 rows
func (d *IndexedFrame[I]) CopyRange(start, count int) *IndexedFrame[I] {
func (f *IndexedFrame[I]) CopyRange(start, count int) *IndexedFrame[I] {
out := &IndexedFrame[I]{SignalManager: &SignalManager{}}
for _, s := range d.series {
for _, s := range f.series {
out.PushSeries(s.CopyRange(start, count))
}
return out
}
// Len returns the number of rows in the IndexedFrame or 0 if the IndexedFrame has no rows. If the IndexedFrame has series of different lengths, then the longest length series is returned.
func (d *IndexedFrame[I]) Len() int {
if len(d.series) == 0 {
func (f *IndexedFrame[I]) Len() int {
if len(f.series) == 0 {
return 0
}
var length int
for _, s := range d.series {
for _, s := range f.series {
if s.Len() > length {
length = s.Len()
}
@ -83,10 +89,10 @@ func (d *IndexedFrame[I]) Len() int {
}
// Select returns a new IndexedFrame with the selected Series. The series are not copied so the returned IndexedFrame will be a reference to the current IndexedFrame. If a series name is not found, it is ignored.
func (d *IndexedFrame[I]) Select(names ...string) *IndexedFrame[I] {
func (f *IndexedFrame[I]) Select(names ...string) *IndexedFrame[I] {
out := &IndexedFrame[I]{SignalManager: &SignalManager{}}
for _, name := range names {
if s := d.Series(name); s != nil {
if s := f.Series(name); s != nil {
out.PushSeries(s)
}
}
@ -103,19 +109,19 @@ func (d *IndexedFrame[I]) Select(names ...string) *IndexedFrame[I] {
// The order of the columns is not defined.
//
// If the IndexedFrame has more than 20 rows, the output will include the first ten rows and the last ten rows.
func (d *IndexedFrame[I]) String() string {
if d == nil {
return fmt.Sprintf("%T[nil]", d)
func (f *IndexedFrame[I]) String() string {
if f == nil {
return fmt.Sprintf("%T[nil]", f)
}
names := d.Names() // Defines the order of the columns.
names := f.Names() // Defines the order of the columns.
series := make([]*IndexedSeries[I], len(names))
for i, name := range names {
series[i] = d.Series(name)
series[i] = f.Series(name)
}
buffer := new(bytes.Buffer)
t := tabwriter.NewWriter(buffer, 0, 0, 2, ' ', 0)
fmt.Fprintf(t, "%T[%dx%d]\n", d, d.Len(), len(series))
fmt.Fprintf(t, "%T[%dx%d]\n", f, f.Len(), len(series))
fmt.Fprintf(t, "[Row]\t[Index]\t%s\t\n", strings.Join(names, "\t"))
printRow := func(row int, index I) {
@ -136,7 +142,7 @@ func (d *IndexedFrame[I]) String() string {
indexes := maps.Keys(series[0].index)
// Print the first ten rows and the last ten rows if the IndexedFrame has more than 20 rows.
if d.Len() > 20 {
if f.Len() > 20 {
for i := 0; i < 10; i++ {
printRow(i, indexes[i])
}
@ -149,7 +155,7 @@ func (d *IndexedFrame[I]) String() string {
printRow(i, indexes[len(indexes)-i])
}
} else {
for i := 0; i < d.Len(); i++ {
for i := 0; i < f.Len(); i++ {
printRow(i, indexes[i])
}
}
@ -158,9 +164,9 @@ func (d *IndexedFrame[I]) String() string {
return buffer.String()
}
func (d *IndexedFrame[I]) Index(row int) *I {
func (f *IndexedFrame[I]) Index(row int) *I {
var index *I
d.ForEachSeries(func(s *IndexedSeries[I]) {
f.ForEachSeries(func(s *IndexedSeries[I]) {
if index == nil {
index = s.Index(row)
} else if i := s.Index(row); i == nil || *index != *i {
@ -171,89 +177,89 @@ func (d *IndexedFrame[I]) Index(row int) *I {
}
// Date returns the value of the Date column at index i. i is an EasyIndex. If i is out of bounds, time.Time{} is returned. This is equivalent to calling Index(i).
func (d *IndexedFrame[I]) Date(i int) *I {
return d.Index(i)
func (f *IndexedFrame[I]) Date(i int) *I {
return f.Index(i)
}
// Open returns the open price of the candle at index i. i is an EasyIndex. If i is out of bounds, 0 is returned. This is the equivalent to calling Float("Open", i).
func (d *IndexedFrame[I]) Open(i int) float64 {
return d.Float("Open", i)
func (f *IndexedFrame[I]) Open(i int) float64 {
return f.Float("Open", i)
}
func (d *IndexedFrame[I]) OpenIndex(index I) float64 {
return d.FloatIndex("Open", index)
func (f *IndexedFrame[I]) OpenIndex(index I) float64 {
return f.FloatIndex("Open", index)
}
// High returns the high price of the candle at index i. i is an EasyIndex. If i is out of bounds, 0 is returned. This is the equivalent to calling Float("High", i).
func (d *IndexedFrame[I]) High(i int) float64 {
return d.Float("High", i)
func (f *IndexedFrame[I]) High(i int) float64 {
return f.Float("High", i)
}
func (d *IndexedFrame[I]) HighIndex(index I) float64 {
return d.FloatIndex("High", index)
func (f *IndexedFrame[I]) HighIndex(index I) float64 {
return f.FloatIndex("High", index)
}
// Low returns the low price of the candle at index i. i is an EasyIndex. If i is out of bounds, 0 is returned. This is the equivalent to calling Float("Low", i).
func (d *IndexedFrame[I]) Low(i int) float64 {
return d.Float("Low", i)
func (f *IndexedFrame[I]) Low(i int) float64 {
return f.Float("Low", i)
}
func (d *IndexedFrame[I]) LowIndex(index I) float64 {
return d.FloatIndex("Low", index)
func (f *IndexedFrame[I]) LowIndex(index I) float64 {
return f.FloatIndex("Low", index)
}
// Close returns the close price of the candle at index i. i is an EasyIndex. If i is out of bounds, 0 is returned. This is the equivalent to calling Float("Close", i).
func (d *IndexedFrame[I]) Close(i int) float64 {
return d.Float("Close", i)
func (f *IndexedFrame[I]) Close(i int) float64 {
return f.Float("Close", i)
}
func (d *IndexedFrame[I]) CloseIndex(index I) float64 {
return d.FloatIndex("Close", index)
func (f *IndexedFrame[I]) CloseIndex(index I) float64 {
return f.FloatIndex("Close", index)
}
// Volume returns the volume of the candle at index i. i is an EasyIndex. If i is out of bounds, 0 is returned. This is the equivalent to calling Float("Volume", i).
func (d *IndexedFrame[I]) Volume(i int) int {
return d.Int("Volume", i)
func (f *IndexedFrame[I]) Volume(i int) int {
return f.Int("Volume", i)
}
func (d *IndexedFrame[I]) VolumeIndex(index I) int {
return d.IntIndex("Volume", index)
func (f *IndexedFrame[I]) VolumeIndex(index I) int {
return f.IntIndex("Volume", index)
}
// Dates returns a Series of all the dates in the IndexedFrame. This is equivalent to calling Series("Date").
func (d *IndexedFrame[I]) Dates() *IndexedSeries[I] {
return d.Series("Date")
func (f *IndexedFrame[I]) Dates() *IndexedSeries[I] {
return f.Series("Date")
}
// Opens returns a FloatSeries of all the open prices in the IndexedFrame. This is equivalent to calling Series("Open").
func (d *IndexedFrame[I]) Opens() *IndexedSeries[I] {
return d.Series("Open")
func (f *IndexedFrame[I]) Opens() *IndexedSeries[I] {
return f.Series("Open")
}
// Highs returns a FloatSeries of all the high prices in the IndexedFrame. This is equivalent to calling Series("High").
func (d *IndexedFrame[I]) Highs() *IndexedSeries[I] {
return d.Series("High")
func (f *IndexedFrame[I]) Highs() *IndexedSeries[I] {
return f.Series("High")
}
// Lows returns a FloatSeries of all the low prices in the IndexedFrame. This is equivalent to calling Series("Low").
func (d *IndexedFrame[I]) Lows() *IndexedSeries[I] {
return d.Series("Low")
func (f *IndexedFrame[I]) Lows() *IndexedSeries[I] {
return f.Series("Low")
}
// Closes returns a FloatSeries of all the close prices in the IndexedFrame. This is equivalent to calling Series("Close").
func (d *IndexedFrame[I]) Closes() *IndexedSeries[I] {
return d.Series("Close")
func (f *IndexedFrame[I]) Closes() *IndexedSeries[I] {
return f.Series("Close")
}
// Volumes returns a Series of all the volumes in the IndexedFrame. This is equivalent to calling Series("Volume").
func (d *IndexedFrame[I]) Volumes() *IndexedSeries[I] {
return d.Series("Volume")
func (f *IndexedFrame[I]) Volumes() *IndexedSeries[I] {
return f.Series("Volume")
}
// Contains returns true if the IndexedFrame contains all the given series names. Remember that names are case sensitive.
func (d *IndexedFrame[I]) Contains(names ...string) bool {
func (f *IndexedFrame[I]) Contains(names ...string) bool {
for _, name := range names {
if _, ok := d.series[name]; !ok {
if _, ok := f.series[name]; !ok {
return false
}
}
@ -261,79 +267,79 @@ func (d *IndexedFrame[I]) Contains(names ...string) bool {
}
// ContainsDOHLCV returns true if the IndexedFrame contains the series "Date", "Open", "High", "Low", "Close", and "Volume". This is equivalent to calling Contains("Date", "Open", "High", "Low", "Close", "Volume").
func (d *IndexedFrame[I]) ContainsDOHLCV() bool {
return d.Contains("Open", "High", "Low", "Close", "Volume")
func (f *IndexedFrame[I]) ContainsDOHLCV() bool {
return f.Contains("Open", "High", "Low", "Close", "Volume")
}
// PushCandle pushes a candlestick to the IndexedFrame. If the IndexedFrame does not contain the series "Date", "Open", "High", "Low", "Close", and "Volume", an error is returned.
func (d *IndexedFrame[I]) PushCandle(date I, open, high, low, close float64, volume int64) error {
if !d.ContainsDOHLCV() {
func (f *IndexedFrame[I]) PushCandle(date I, open, high, low, close float64, volume int64) error {
if !f.ContainsDOHLCV() {
return fmt.Errorf("IndexedFrame does not contain Open, High, Low, Close, Volume columns")
}
d.series["Open"].Push(date, open)
d.series["High"].Push(date, high)
d.series["Low"].Push(date, low)
d.series["Close"].Push(date, close)
d.series["Volume"].Push(date, volume)
f.series["Open"].Push(date, open)
f.series["High"].Push(date, high)
f.series["Low"].Push(date, low)
f.series["Close"].Push(date, close)
f.series["Volume"].Push(date, volume)
return nil
}
// PushSeries adds the given series to the IndexedFrame. If the IndexedFrame already contains a series with the same name, an error is returned.
func (d *IndexedFrame[I]) PushSeries(series ...*IndexedSeries[I]) error {
if d.series == nil {
d.series = make(map[string]*IndexedSeries[I], len(series))
func (f *IndexedFrame[I]) PushSeries(series ...*IndexedSeries[I]) error {
if f.series == nil {
f.series = make(map[string]*IndexedSeries[I], len(series))
}
for _, s := range series {
name := s.Name()
if _, ok := d.series[name]; ok {
if _, ok := f.series[name]; ok {
return fmt.Errorf("IndexedFrame already contains column %q", name)
}
s.SignalConnect("NameChanged", d, d.onSeriesNameChanged, name)
d.series[name] = s
s.SignalConnect("NameChanged", f, f.onSeriesNameChanged, name)
f.series[name] = s
}
return nil
}
// RemoveSeries removes the given series from the IndexedFrame. If the IndexedFrame does not contain a series with a given name, nothing happens.
func (d *IndexedFrame[I]) RemoveSeries(names ...string) {
func (f *IndexedFrame[I]) RemoveSeries(names ...string) {
for _, name := range names {
s, ok := d.series[name]
s, ok := f.series[name]
if !ok {
return
}
s.SignalDisconnect("NameChanged", d, d.onSeriesNameChanged)
delete(d.series, name)
s.SignalDisconnect("NameChanged", f, f.onSeriesNameChanged)
delete(f.series, name)
}
}
func (d *IndexedFrame[I]) onSeriesNameChanged(args ...any) {
func (f *IndexedFrame[I]) onSeriesNameChanged(args ...any) {
if len(args) != 2 {
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
}
newName := args[0].(string)
oldName := args[1].(string)
d.series[newName] = d.series[oldName]
delete(d.series, oldName)
f.series[newName] = f.series[oldName]
delete(f.series, oldName)
// Reconnect our signal handlers to update the name we use in the handlers.
d.series[newName].SignalDisconnect("NameChanged", d, d.onSeriesNameChanged)
d.series[newName].SignalConnect("NameChanged", d, d.onSeriesNameChanged, newName)
f.series[newName].SignalDisconnect("NameChanged", f, f.onSeriesNameChanged)
f.series[newName].SignalConnect("NameChanged", f, f.onSeriesNameChanged, newName)
}
// Names returns a slice of the names of the series in the IndexedFrame.
func (d *IndexedFrame[I]) Names() []string {
return maps.Keys(d.series)
func (f *IndexedFrame[I]) Names() []string {
return maps.Keys(f.series)
}
// Series returns a Series of the column with the given name. If the column does not exist, nil is returned.
func (d *IndexedFrame[I]) Series(name string) *IndexedSeries[I] {
if len(d.series) == 0 {
func (f *IndexedFrame[I]) Series(name string) *IndexedSeries[I] {
if len(f.series) == 0 {
return nil
}
v, ok := d.series[name]
v, ok := f.series[name]
if !ok {
return nil
}
@ -341,37 +347,37 @@ func (d *IndexedFrame[I]) Series(name string) *IndexedSeries[I] {
}
// Value returns the value of the column at index i. i is an EasyIndex. If i is out of bounds, nil is returned.
func (d *IndexedFrame[I]) Value(column string, i int) any {
if len(d.series) == 0 {
func (f *IndexedFrame[I]) Value(column string, i int) any {
if len(f.series) == 0 {
return nil
}
if s, ok := d.series[column]; ok {
if s, ok := f.series[column]; ok {
return s.Value(i)
}
return nil
}
func (d *IndexedFrame[I]) ValueIndex(column string, index I) any {
if len(d.series) == 0 {
func (f *IndexedFrame[I]) ValueIndex(column string, index I) any {
if len(f.series) == 0 {
return nil
}
if s, ok := d.series[column]; ok {
if s, ok := f.series[column]; ok {
return s.ValueIndex(index)
}
return nil
}
// Float returns the float64 value of the column at index i. i is an EasyIndex. If i is out of bounds or the value was not a float64, then 0 is returned.
func (d *IndexedFrame[I]) Float(column string, i int) float64 {
val, ok := d.Value(column, i).(float64)
func (f *IndexedFrame[I]) Float(column string, i int) float64 {
val, ok := f.Value(column, i).(float64)
if !ok {
return 0
}
return val
}
func (d *IndexedFrame[I]) FloatIndex(column string, index I) float64 {
val, ok := d.ValueIndex(column, index).(float64)
func (f *IndexedFrame[I]) FloatIndex(column string, index I) float64 {
val, ok := f.ValueIndex(column, index).(float64)
if !ok {
return 0
}
@ -379,16 +385,16 @@ func (d *IndexedFrame[I]) FloatIndex(column string, index I) float64 {
}
// Int returns the int value of the column at index i. i is an EasyIndex. If i is out of bounds or the value was not an int, then 0 is returned.
func (d *IndexedFrame[I]) Int(column string, i int) int {
val, ok := d.Value(column, i).(int)
func (f *IndexedFrame[I]) Int(column string, i int) int {
val, ok := f.Value(column, i).(int)
if !ok {
return 0
}
return val
}
func (d *IndexedFrame[I]) IntIndex(column string, index I) int {
val, ok := d.ValueIndex(column, index).(int)
func (f *IndexedFrame[I]) IntIndex(column string, index I) int {
val, ok := f.ValueIndex(column, index).(int)
if !ok {
return 0
}
@ -396,16 +402,16 @@ func (d *IndexedFrame[I]) IntIndex(column string, index I) int {
}
// Str returns the string value of the column at index i. i is an EasyIndex. If i is out of bounds or the value was not a string, then the empty string "" is returned.
func (d *IndexedFrame[I]) Str(column string, i int) string {
val, ok := d.Value(column, i).(string)
func (f *IndexedFrame[I]) Str(column string, i int) string {
val, ok := f.Value(column, i).(string)
if !ok {
return ""
}
return val
}
func (d *IndexedFrame[I]) StrIndex(column string, index I) string {
val, ok := d.ValueIndex(column, index).(string)
func (f *IndexedFrame[I]) StrIndex(column string, index I) string {
val, ok := f.ValueIndex(column, index).(string)
if !ok {
return ""
}
@ -413,24 +419,38 @@ func (d *IndexedFrame[I]) StrIndex(column string, index I) string {
}
// Time returns the time.Time value of the column at index i. i is an EasyIndex. If i is out of bounds or the value was not a Time, then time.Time{} is returned. Use Time.IsZero() to check if the value was valid.
func (d *IndexedFrame[I]) Time(column string, i int) time.Time {
val, ok := d.Value(column, i).(time.Time)
func (f *IndexedFrame[I]) Time(column string, i int) time.Time {
val, ok := f.Value(column, i).(time.Time)
if !ok {
return time.Time{}
}
return val
}
func (d *IndexedFrame[I]) TimeIndex(column string, index I) time.Time {
val, ok := d.ValueIndex(column, index).(time.Time)
func (f *IndexedFrame[I]) TimeIndex(column string, index I) time.Time {
val, ok := f.ValueIndex(column, index).(time.Time)
if !ok {
return time.Time{}
}
return val
}
func (d *IndexedFrame[I]) ForEachSeries(f func(*IndexedSeries[I])) {
for _, s := range d.series {
f(s)
func (f *IndexedFrame[I]) ForEachSeries(fn func(*IndexedSeries[I])) {
for _, s := range f.series {
fn(s)
}
}
func (f *IndexedFrame[I]) Shift(periods int, nilValue any) *IndexedFrame[I] {
for _, s := range f.series {
_ = s.Shift(periods, nilValue)
}
return f
}
func (f *IndexedFrame[I]) ShiftIndex(periods int, step func(prev I, amt int) I) *IndexedFrame[I] {
for _, s := range f.series {
_ = s.ShiftIndex(periods, step)
}
return f
}

View File

@ -106,6 +106,58 @@ func TestIndexedFrame(t *testing.T) {
t.Log(data.String())
}
func TestIndexedFrameFunctions(t *testing.T) {
data := NewDOHLCVIndexedFrame[UnixTime]()
data.PushCandle(UnixTime(time.Date(2021, 5, 13, 0, 0, 0, 0, time.UTC).Unix()), 0.8, 1.2, 0.6, 1.0, 1)
data.PushCandle(UnixTime(time.Date(2021, 5, 14, 0, 0, 0, 0, time.UTC).Unix()), 1.0, 1.4, 0.8, 1.2, 1)
data.PushCandle(UnixTime(time.Date(2021, 5, 15, 0, 0, 0, 0, time.UTC).Unix()), 1.2, 1.6, 1.0, 1.4, 1)
data.ShiftIndex(2, UnixTimeStep(time.Hour*24)) // Shift 2 days
if data.Len() != 3 {
t.Fatalf("Expected 3 rows, got %d", data.Len())
}
if data.Close(-1) != 1.4 {
t.Fatalf("Expected latest close to be 1.4, got %f", data.Close(-1))
}
if !data.Date(0).Time().Equal(time.Date(2021, 5, 15, 0, 0, 0, 0, time.UTC)) {
t.Fatalf("Expected first date to be 2021-05-15, got %v", data.Date(0))
}
if !data.Date(-1).Time().Equal(time.Date(2021, 5, 17, 0, 0, 0, 0, time.UTC)) {
t.Fatalf("Expected latest date to be 2021-05-17, got %v", data.Date(-1))
}
data.Shift(-2, 0.0) // Shift all rows up by 2 and clear the last 2 rows with zero.
if data.Len() != 3 {
t.Fatalf("Expected 3 rows, got %d", data.Len())
}
if data.Close(0) != 1.4 {
t.Fatalf("Expected latest close to be 1.4, got %f", data.Close(0))
}
if data.Close(-1) != 0.0 {
t.Fatalf("Expected latest close to be 0.0, got %f", data.Close(-1))
}
if !data.Date(0).Time().Equal(time.Date(2021, 5, 15, 0, 0, 0, 0, time.UTC)) {
t.Fatalf("Expected first date to be 2021-05-15, got %v", data.Date(0))
}
data.Shift(1, 0.0) // Shift all rows down by 1 and clear the first row with zero.
if data.Len() != 3 {
t.Fatalf("Expected 3 rows, got %d", data.Len())
}
if data.Close(0) != 0.0 {
t.Fatalf("Expected latest close to be 0.0, got %f", data.Close(0))
}
if data.Close(1) != 1.4 {
t.Fatalf("Expected latest close to be 1.4, got %f", data.Close(1))
}
if !data.Date(0).Time().Equal(time.Date(2021, 5, 15, 0, 0, 0, 0, time.UTC)) {
t.Fatalf("Expected first date to be 2021-05-15, got %v", data.Date(0))
}
}
func TestDOHLCVDataFrame(t *testing.T) {
data := NewDOHLCVFrame()
if !data.ContainsDOHLCV() {

View File

@ -400,6 +400,32 @@ func (s *Series) Rolling(period int) *RollingSeries {
return NewRollingSeries(s, period)
}
func (s *Series) Shift(periods int, nilVal any) *Series {
if periods == 0 {
return s
} else if periods > 0 {
// Shift values forward.
for i := s.Len() - 1; i >= periods; i-- {
s.data[i] = s.data[i-periods]
}
// Fill in nil values.
for i := 0; i < periods; i++ {
s.data[i] = nilVal
}
} else {
periods = -periods
// Shift values backward.
for i := 0; i < periods; i++ {
s.data[i] = s.data[periods-i]
}
// Fill in nil values.
for i := periods; i < s.Len(); i++ {
s.data[i] = nilVal
}
}
return s
}
type RollingSeries struct {
series *Series
period int

View File

@ -259,6 +259,24 @@ func (s *IndexedSeries[I]) SetValueIndex(index I, val any) *IndexedSeries[I] {
return s.SetValue(row, val)
}
func (s *IndexedSeries[I]) Shift(periods int, nilValue any) *IndexedSeries[I] {
_ = s.series.Shift(periods, nilValue)
return s
}
func (s *IndexedSeries[I]) ShiftIndex(periods int, step func(prev I, amt int) I) *IndexedSeries[I] {
if periods == 0 {
return s
}
// Shift the indexes.
newIndexes := make(map[I]int, len(s.index))
for index, i := range s.index {
newIndexes[step(index, periods)] = i
}
s.index = newIndexes
return s
}
// Sub subtracts the other series values from this series values. The other series must have the same index type. The values are subtracted by comparing their indexes. For example, subtracting two IndexedSeries that share no indexes will result in no change of values.
func (s *IndexedSeries[I]) Sub(other *IndexedSeries[I]) *IndexedSeries[I] {
for index, row := range s.index {