mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 00:13:51 +00:00
Gave data.go much needed love and split it up
This commit is contained in:
parent
2d4c5df187
commit
a68542d34b
784
data.go
784
data.go
@ -1,800 +1,16 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
df "github.com/rocketlaunchr/dataframe-go"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type Series interface {
|
||||
Signaler
|
||||
|
||||
// Reading data.
|
||||
Copy(start, end int) Series
|
||||
Len() int
|
||||
Name() string // Name returns the immutable name of the Series.
|
||||
Float(i int) float64
|
||||
Int(i int) int64
|
||||
Str(i int) string
|
||||
Time(i int) time.Time
|
||||
Value(i int) interface{}
|
||||
ValueRange(start, end int) []interface{}
|
||||
Values() []interface{} // Values is the same as ValueRange(0, -1).
|
||||
|
||||
// Writing data.
|
||||
SetName(name string) Series
|
||||
SetValue(i int, val interface{}) Series
|
||||
Push(val interface{}) Series
|
||||
|
||||
// Statistical functions.
|
||||
Rolling(period int) *RollingSeries
|
||||
|
||||
// WithValueFunc is used to implement other types of Series that may modify the values by applying a function before returning them, for example. This returns a Series that is a copy of the original with the new value function used whenever a value is requested outside of the Value() method, which will still return the original value.
|
||||
WithValueFunc(value func(i int) interface{}) Series
|
||||
}
|
||||
|
||||
type Frame interface {
|
||||
// Reading data.
|
||||
Contains(names ...string) bool // Contains returns true if the frame contains all the columns specified.
|
||||
Copy(start, end int) Frame
|
||||
Len() int
|
||||
Names() []string
|
||||
Series(name string) Series
|
||||
String() string
|
||||
Value(column string, i int) interface{}
|
||||
Float(column string, i int) float64
|
||||
Int(column string, i int) int64
|
||||
Str(column string, i int) string
|
||||
Time(column string, i int) time.Time
|
||||
|
||||
// Writing data.
|
||||
PushSeries(s ...Series) error
|
||||
PushValues(values map[string]interface{}) error
|
||||
RemoveSeries(name string)
|
||||
|
||||
// Easy access functions for common columns.
|
||||
ContainsDOHLCV() bool // ContainsDOHLCV returns true if the frame contains all the columns: Date, Open, High, Low, Close, and Volume.
|
||||
Date(i int) time.Time
|
||||
Open(i int) float64
|
||||
High(i int) float64
|
||||
Low(i int) float64
|
||||
Close(i int) float64
|
||||
Volume(i int) float64
|
||||
Dates() Series
|
||||
Opens() Series
|
||||
Highs() Series
|
||||
Lows() Series
|
||||
Closes() Series
|
||||
Volumes() Series
|
||||
PushCandle(date time.Time, open, high, low, close float64, volume int64) error
|
||||
}
|
||||
|
||||
// AppliedSeries is like Series, but it applies a function to each row of data before returning it.
|
||||
type AppliedSeries struct {
|
||||
Series
|
||||
apply func(i int, val interface{}) interface{}
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) Value(i int) interface{} {
|
||||
return s.apply(EasyIndex(i, s.Len()), s.Series.Value(i))
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) WithValueFunc(value func(i int) interface{}) Series {
|
||||
return &AppliedSeries{Series: s.Series.WithValueFunc(value), apply: s.apply}
|
||||
}
|
||||
|
||||
func NewAppliedSeries(s Series, apply func(i int, val interface{}) interface{}) *AppliedSeries {
|
||||
appliedSeries := &AppliedSeries{apply: apply}
|
||||
appliedSeries.Series = s.WithValueFunc(appliedSeries.Value)
|
||||
return appliedSeries
|
||||
}
|
||||
|
||||
type RollingSeries struct {
|
||||
Series
|
||||
period int
|
||||
}
|
||||
|
||||
// Average is an alias for Mean.
|
||||
func (s *RollingSeries) Average() *AppliedSeries {
|
||||
return s.Mean()
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Mean() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(_ int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch v[0].(type) {
|
||||
case float64:
|
||||
var sum float64
|
||||
for _, v := range v {
|
||||
sum += v.(float64)
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) EMA() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(i int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch v[0].(type) {
|
||||
case float64:
|
||||
ema := v[0].(float64)
|
||||
for _, v := range v[1:] {
|
||||
ema += (v.(float64) - ema) * 2 / (float64(s.period) + 1)
|
||||
}
|
||||
return ema
|
||||
case int64:
|
||||
ema := v[0].(int64)
|
||||
for _, v := range v[1:] {
|
||||
ema += (v.(int64) - ema) * 2 / (int64(s.period) + 1)
|
||||
}
|
||||
return ema
|
||||
default: // string, time.Time
|
||||
return v[len(v)-1] // Do nothing
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("expected a slice of values, got %t", v))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Median() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(_ int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch v[0].(type) {
|
||||
case float64:
|
||||
if len(v) == 0 {
|
||||
return float64(0)
|
||||
}
|
||||
slices.SortFunc(v, func(a, b interface{}) 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 interface{}) 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
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("expected a slice of values, got %t", v))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) StdDev() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(i int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Value returns []interface{} up to `period` long. The last item in the slice is the item at i. If i is out of bounds, nil is returned.
|
||||
func (s *RollingSeries) Value(i int) interface{} {
|
||||
items := make([]interface{}, 0, s.period)
|
||||
i = EasyIndex(i, s.Len())
|
||||
if i < 0 || i >= s.Len() {
|
||||
return items
|
||||
}
|
||||
for j := i; j > i-s.period && j >= 0; j-- {
|
||||
// items = append(items, s.Series.Value(j))
|
||||
items = slices.Insert(items, 0, s.Series.Value(j))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *RollingSeries) WithValueFunc(value func(i int) interface{}) Series {
|
||||
return &RollingSeries{Series: s.Series.WithValueFunc(value), period: s.period}
|
||||
}
|
||||
|
||||
// DataSeries is a Series that wraps a column of data. The data can be of the following types: float64, int64, string, or time.Time.
|
||||
//
|
||||
// Signals:
|
||||
// - LengthChanged(int) - when the data is appended or an item is removed.
|
||||
// - NameChanged(string) - when the name is changed.
|
||||
type DataSeries struct {
|
||||
SignalManager
|
||||
data df.Series
|
||||
value func(i int) interface{}
|
||||
}
|
||||
|
||||
// Copy copies the Series from start to end (inclusive). If end is -1, it will copy to the end of the Series. If start is out of bounds, nil is returned.
|
||||
func (s *DataSeries) Copy(start, end int) Series {
|
||||
var _end *int
|
||||
if start < 0 || start >= s.Len() {
|
||||
return nil
|
||||
} else if end >= 0 {
|
||||
if end < start {
|
||||
return nil
|
||||
}
|
||||
_end = &end
|
||||
}
|
||||
return &DataSeries{
|
||||
SignalManager: SignalManager{},
|
||||
data: s.data.Copy(df.Range{Start: &start, End: _end}),
|
||||
value: s.value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Name() string {
|
||||
return s.data.Name()
|
||||
}
|
||||
|
||||
func (s *DataSeries) SetName(name string) Series {
|
||||
if name == s.Name() {
|
||||
return s
|
||||
}
|
||||
s.data.Rename(name)
|
||||
s.SignalEmit("NameChanged", name)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DataSeries) Len() int {
|
||||
if s.data == nil {
|
||||
return 0
|
||||
}
|
||||
return s.data.NRows()
|
||||
}
|
||||
|
||||
func (s *DataSeries) Rolling(period int) *RollingSeries {
|
||||
rollingSeries := &RollingSeries{period: period}
|
||||
rollingSeries.Series = s.WithValueFunc(rollingSeries.Value)
|
||||
return rollingSeries
|
||||
}
|
||||
|
||||
func (s *DataSeries) Push(value interface{}) Series {
|
||||
if s.data != nil {
|
||||
s.data.Append(value)
|
||||
s.SignalEmit("LengthChanged", s.Len())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DataSeries) SetValue(i int, val interface{}) Series {
|
||||
if s.data != nil {
|
||||
s.data.Update(EasyIndex(i, s.Len()), val)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DataSeries) Value(i int) interface{} {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
i = EasyIndex(i, s.Len()) // Allow for negative indexing.
|
||||
return s.data.Value(i)
|
||||
}
|
||||
|
||||
// ValueRange returns a slice of values from start to end, including start and end. The first value is at index 0. A negative value for start or end can be used to get values from the latest, like Python's negative indexing. If end is less than zero, it will be sliced from start to the last item. If start or end is out of bounds, nil is returned. If start is greater than end, nil is returned.
|
||||
func (s *DataSeries) ValueRange(start, end int) []interface{} {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
start = EasyIndex(start, s.Len())
|
||||
if end < 0 {
|
||||
end = s.Len() - 1
|
||||
}
|
||||
if start < 0 || start >= s.Len() || end >= s.Len() || start > end {
|
||||
return nil
|
||||
}
|
||||
|
||||
items := make([]interface{}, end-start+1)
|
||||
for i := start; i <= end; i++ {
|
||||
items[i-start] = s.value(i)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *DataSeries) Values() []interface{} {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
return s.ValueRange(0, -1)
|
||||
}
|
||||
|
||||
func (s *DataSeries) Float(i int) float64 {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case float64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Int(i int) int64 {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case int64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Str(i int) string {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
return val
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Time(i int) time.Time {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case time.Time:
|
||||
return val
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) WithValueFunc(value func(i int) interface{}) Series {
|
||||
return &DataSeries{
|
||||
SignalManager: s.SignalManager,
|
||||
data: s.data,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDataSeries(data df.Series) *DataSeries {
|
||||
dataSeries := &DataSeries{
|
||||
SignalManager: SignalManager{},
|
||||
data: data,
|
||||
}
|
||||
dataSeries.value = dataSeries.Value
|
||||
return dataSeries
|
||||
}
|
||||
|
||||
type DataFrame struct {
|
||||
series map[string]Series
|
||||
rowCounts map[string]int
|
||||
// data *df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
|
||||
}
|
||||
|
||||
func NewDataFrame(series ...Series) *DataFrame {
|
||||
d := &DataFrame{}
|
||||
d.PushSeries(series...)
|
||||
return d
|
||||
}
|
||||
|
||||
// NewDOHLCVDataFrame returns a DataFrame with empty Date, Open, High, Low, Close, and Volume columns.
|
||||
// Use the PushCandle method to add candlesticks in an easy and type-safe way.
|
||||
func NewDOHLCVDataFrame() *DataFrame {
|
||||
return NewDataFrame(
|
||||
NewDataSeries(df.NewSeriesTime("Date", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Open", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("High", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Low", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Close", nil)),
|
||||
NewDataSeries(df.NewSeriesInt64("Volume", nil)),
|
||||
)
|
||||
}
|
||||
|
||||
// Copy copies the DataFrame from start to end (inclusive). If end is -1, it will copy to the end of the DataFrame. If start is out of bounds, nil is returned.
|
||||
func (d *DataFrame) Copy(start, end int) Frame {
|
||||
out := &DataFrame{}
|
||||
for _, v := range d.series {
|
||||
newSeries := v.Copy(start, end)
|
||||
out.PushSeries(newSeries)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of rows in the DataFrame or 0 if the DataFrame is nil. A value less than zero means the
|
||||
// DataFrame has Series of varying lengths.
|
||||
func (d *DataFrame) Len() int {
|
||||
if len(d.series) == 0 {
|
||||
return 0
|
||||
}
|
||||
// Check if all the Series have the same length.
|
||||
var length int
|
||||
for _, v := range d.rowCounts {
|
||||
if length == 0 {
|
||||
length = v
|
||||
} else if length != v {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func (d *DataFrame) String() string {
|
||||
if d == nil {
|
||||
return fmt.Sprintf("%T[nil]", d)
|
||||
}
|
||||
names := d.Names() // Defines the order of the columns.
|
||||
series := make([]Series, len(names))
|
||||
for i, name := range names {
|
||||
series[i] = d.Series(name)
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := tabwriter.NewWriter(buffer, 0, 0, 1, ' ', 0)
|
||||
fmt.Fprintf(t, "%T[%dx%d]\n", d, d.Len(), len(d.series))
|
||||
fmt.Fprintln(t, "\t", strings.Join(names, "\t"), "\t")
|
||||
|
||||
printRow := func(i int) {
|
||||
row := make([]string, len(series))
|
||||
for j, s := range series {
|
||||
switch typ := s.Value(i).(type) {
|
||||
case time.Time:
|
||||
row[j] = typ.Format("2006-01-02 15:04:05")
|
||||
case string:
|
||||
row[j] = fmt.Sprintf("%q", typ)
|
||||
default:
|
||||
row[j] = fmt.Sprintf("%v", typ)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(t, strconv.Itoa(i), "\t", strings.Join(row, "\t"), "\t")
|
||||
}
|
||||
|
||||
// Print the first ten rows and the last ten rows if the DataFrame has more than 20 rows.
|
||||
if d.Len() > 20 {
|
||||
for i := 0; i < 10; i++ {
|
||||
printRow(i)
|
||||
}
|
||||
fmt.Fprintf(t, "...\t")
|
||||
for range names {
|
||||
fmt.Fprint(t, "\t") // Keeps alignment.
|
||||
}
|
||||
fmt.Fprintln(t) // Print new line character.
|
||||
for i := 10; i > 0; i-- {
|
||||
printRow(d.Len() - i)
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < d.Len(); i++ {
|
||||
printRow(i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Flush()
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// Date returns the value of the Date column at index i. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Time("Date", i).
|
||||
func (d *DataFrame) Date(i int) time.Time {
|
||||
return d.Time("Date", i)
|
||||
}
|
||||
|
||||
// Open returns the open price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Open", i).
|
||||
func (d *DataFrame) Open(i int) float64 {
|
||||
return d.Float("Open", i)
|
||||
}
|
||||
|
||||
// High returns the high price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("High", i).
|
||||
func (d *DataFrame) High(i int) float64 {
|
||||
return d.Float("High", i)
|
||||
}
|
||||
|
||||
// Low returns the low price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Low", i).
|
||||
func (d *DataFrame) Low(i int) float64 {
|
||||
return d.Float("Low", i)
|
||||
}
|
||||
|
||||
// Close returns the close price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Close", i).
|
||||
func (d *DataFrame) Close(i int) float64 {
|
||||
return d.Float("Close", i)
|
||||
}
|
||||
|
||||
// Volume returns the volume of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Volume", i).
|
||||
func (d *DataFrame) Volume(i int) float64 {
|
||||
return d.Float("Volume", i)
|
||||
}
|
||||
|
||||
// Dates returns a Series of all the dates in the DataFrame.
|
||||
func (d *DataFrame) Dates() Series {
|
||||
return d.Series("Date")
|
||||
}
|
||||
|
||||
// Opens returns a Series of all the open prices in the DataFrame.
|
||||
func (d *DataFrame) Opens() Series {
|
||||
return d.Series("Open")
|
||||
}
|
||||
|
||||
// Highs returns a Series of all the high prices in the DataFrame.
|
||||
func (d *DataFrame) Highs() Series {
|
||||
return d.Series("High")
|
||||
}
|
||||
|
||||
// Lows returns a Series of all the low prices in the DataFrame.
|
||||
func (d *DataFrame) Lows() Series {
|
||||
return d.Series("Low")
|
||||
}
|
||||
|
||||
// Closes returns a Series of all the close prices in the DataFrame.
|
||||
func (d *DataFrame) Closes() Series {
|
||||
return d.Series("Close")
|
||||
}
|
||||
|
||||
// Volumes returns a Series of all the volumes in the DataFrame.
|
||||
func (d *DataFrame) Volumes() Series {
|
||||
return d.Series("Volume")
|
||||
}
|
||||
|
||||
func (d *DataFrame) Contains(names ...string) bool {
|
||||
for _, name := range names {
|
||||
if _, ok := d.series[name]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *DataFrame) ContainsDOHLCV() bool {
|
||||
return d.Contains("Date", "Open", "High", "Low", "Close", "Volume")
|
||||
}
|
||||
|
||||
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close float64, volume int64) error {
|
||||
if len(d.series) == 0 {
|
||||
d.PushSeries(
|
||||
NewDataSeries(df.NewSeriesTime("Date", nil, date)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Open", nil, open)),
|
||||
NewDataSeries(df.NewSeriesFloat64("High", nil, high)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Low", nil, low)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Close", nil, close)),
|
||||
NewDataSeries(df.NewSeriesInt64("Volume", nil, volume)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !d.ContainsDOHLCV() {
|
||||
return fmt.Errorf("DataFrame does not contain Date, Open, High, Low, Close, Volume columns")
|
||||
}
|
||||
d.series["Date"].Push(date)
|
||||
d.series["Open"].Push(open)
|
||||
d.series["High"].Push(high)
|
||||
d.series["Low"].Push(low)
|
||||
d.series["Close"].Push(close)
|
||||
d.series["Volume"].Push(volume)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFrame) PushValues(values map[string]interface{}) error {
|
||||
if len(d.series) == 0 {
|
||||
return fmt.Errorf("DataFrame has no columns") // TODO: could create the columns here.
|
||||
}
|
||||
for name, value := range values {
|
||||
if _, ok := d.series[name]; !ok {
|
||||
return fmt.Errorf("DataFrame does not contain column %q", name)
|
||||
}
|
||||
d.series[name].Push(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFrame) PushSeries(series ...Series) error {
|
||||
if d.series == nil {
|
||||
d.series = make(map[string]Series, len(series))
|
||||
d.rowCounts = make(map[string]int, len(series))
|
||||
}
|
||||
|
||||
for _, s := range series {
|
||||
name := s.Name()
|
||||
s.SignalConnect("LengthChanged", d.onSeriesLengthChanged, name)
|
||||
s.SignalConnect("NameChanged", d.onSeriesNameChanged, name)
|
||||
d.series[name] = s
|
||||
d.rowCounts[name] = s.Len()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFrame) RemoveSeries(name string) {
|
||||
s, ok := d.series[name]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
|
||||
s.SignalDisconnect("NameChanged", d.onSeriesNameChanged)
|
||||
delete(d.series, name)
|
||||
delete(d.rowCounts, name)
|
||||
}
|
||||
|
||||
func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) {
|
||||
if len(args) != 2 {
|
||||
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
|
||||
}
|
||||
newLen := args[0].(int)
|
||||
name := args[1].(string)
|
||||
d.rowCounts[name] = newLen
|
||||
}
|
||||
|
||||
func (d *DataFrame) onSeriesNameChanged(args ...interface{}) {
|
||||
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]
|
||||
d.rowCounts[newName] = d.rowCounts[oldName]
|
||||
delete(d.series, oldName)
|
||||
delete(d.rowCounts, oldName)
|
||||
|
||||
// Reconnect our signal handlers to update the name we use in the handlers.
|
||||
d.series[newName].SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
|
||||
d.series[newName].SignalDisconnect("NameChanged", d.onSeriesNameChanged)
|
||||
d.series[newName].SignalConnect("LengthChanged", d.onSeriesLengthChanged, newName)
|
||||
d.series[newName].SignalConnect("NameChanged", d.onSeriesNameChanged, newName)
|
||||
}
|
||||
|
||||
func (d *DataFrame) Names() []string {
|
||||
return maps.Keys(d.series)
|
||||
}
|
||||
|
||||
// Series returns a Series of the column with the given name. If the column does not exist, nil is returned.
|
||||
func (d *DataFrame) Series(name string) Series {
|
||||
if len(d.series) == 0 {
|
||||
return nil
|
||||
}
|
||||
v, ok := d.series[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Value returns the value of the column at index i. The first value is at index 0. A negative value for i can be used to get i values from the latest, like Python's negative indexing. If i is out of bounds, nil is returned.
|
||||
func (d *DataFrame) Value(column string, i int) interface{} {
|
||||
if len(d.series) == 0 {
|
||||
return nil
|
||||
}
|
||||
i = EasyIndex(i, d.Len()) // Allow for negative indexing.
|
||||
if i < 0 || i >= d.Len() { // Prevent out of bounds access.
|
||||
return nil
|
||||
}
|
||||
return d.series[column].Value(i)
|
||||
}
|
||||
|
||||
// Float returns the value of the column at index i casted to float64. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
func (d *DataFrame) Float(column string, i int) float64 {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case float64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Int returns the value of the column at index i casted to int. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
func (d *DataFrame) Int(column string, i int) int64 {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case int64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the value of the column at index i casted to string. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, "" is returned.
|
||||
func (d *DataFrame) Str(column string, i int) string {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
return val
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Time returns the value of the column at index i casted to time.Time. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, time.Time{} is returned.
|
||||
func (d *DataFrame) Time(column string, i int) time.Time {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case time.Time:
|
||||
return val
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
type DataCSVLayout struct {
|
||||
LatestFirst bool // Whether the latest data is first in the dataframe. If false, the latest data is last.
|
||||
DateFormat string // The format of the date column. Example: "03/22/2006". See https://pkg.go.dev/time#pkg-constants for more information.
|
||||
|
88
data_test.go
88
data_test.go
@ -3,99 +3,13 @@ package autotrader
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rocketlaunchr/dataframe-go"
|
||||
)
|
||||
|
||||
func newTestingDataFrame() *DataFrame {
|
||||
func TestReadDataCSV(t *testing.T) {
|
||||
data, err := EURUSD()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestAppliedSeries(t *testing.T) {
|
||||
// Test rolling average.
|
||||
series := NewDataSeries(dataframe.NewSeriesFloat64("test", nil, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
|
||||
sma5Expected := []float64{1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8}
|
||||
sma5 := (Series)(series.Rolling(5).Average()) // Take the 5 period moving average and cast it to Series.
|
||||
if sma5.Len() != 10 {
|
||||
t.Fatalf("Expected 10 rows, got %d", sma5.Len())
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
// Calling Float instead of Value is very important. Value will call the AppliedSeries.Value method
|
||||
// while Float calls Series.Float which is what most people will use and is the most likely to be
|
||||
// problematic as it is supposed to route through the DataSeries.value method.
|
||||
if val := sma5.Float(i); !EqualApprox(val, sma5Expected[i]) {
|
||||
t.Errorf("(%d)\tExpected %f, got %v", i, sma5Expected[i], val)
|
||||
}
|
||||
}
|
||||
|
||||
ema5Expected := []float64{1, 1.3333333333333333, 1.8888888888888888, 2.5925925925925926, 3.3950617283950617, 4.395061728395062, 5.395061728395062, 6.395061728395062, 7.395061728395062, 8.395061728395062}
|
||||
ema5 := (Series)(series.Rolling(5).EMA()) // Take the 5 period exponential moving average.
|
||||
if ema5.Len() != 10 {
|
||||
t.Fatalf("Expected 10 rows, got %d", ema5.Len())
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if val := ema5.Float(i); !EqualApprox(val, ema5Expected[i]) {
|
||||
t.Errorf("(%d)\tExpected %f, got %v", i, ema5Expected[i], val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataSeries(t *testing.T) {
|
||||
data := newTestingDataFrame()
|
||||
|
||||
dates, closes := data.Dates(), data.Closes()
|
||||
|
||||
if dates.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", dates.Len())
|
||||
}
|
||||
if closes.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", closes.Len())
|
||||
}
|
||||
|
||||
sma10 := closes.Rolling(10).Mean()
|
||||
if sma10.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", sma10.Len())
|
||||
}
|
||||
if sma10.Value(-1) != 1.10039 { // Latest closing price averaged over 10 periods.
|
||||
t.Fatalf("Expected 1.10039, got %f", sma10.Value(-1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataFrame(t *testing.T) {
|
||||
data := newTestingDataFrame()
|
||||
|
||||
if data.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", data.Len())
|
||||
}
|
||||
if data.Close(-1) != 1.0967 {
|
||||
t.Fatalf("Expected 1.0967, got %f", data.Close(-1))
|
||||
}
|
||||
|
||||
date := data.Date(2) // Get the 3rd earliest date from the Date column.
|
||||
if date.Year() != 2013 || date.Month() != 5 || date.Day() != 13 {
|
||||
t.Fatalf("Expected 2013-05-13, got %s", date.Format(time.DateOnly))
|
||||
}
|
||||
|
||||
err := data.PushCandle(time.Date(2023, 5, 14, 0, 0, 0, 0, time.UTC), 1.0, 1.0, 1.0, 1.0, 1)
|
||||
if err != nil {
|
||||
t.Log(data.Names())
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if data.Len() != 2611 {
|
||||
t.Fatalf("Expected 2611 rows, got %d", data.Len())
|
||||
}
|
||||
if data.Close(-1) != 1.0 {
|
||||
t.Fatalf("Expected latest close to be 1.0, got %f", data.Close(-1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDataCSV(t *testing.T) {
|
||||
data := newTestingDataFrame()
|
||||
|
||||
if data.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", data.Len())
|
||||
|
449
frame.go
Normal file
449
frame.go
Normal file
@ -0,0 +1,449 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
df "github.com/rocketlaunchr/dataframe-go"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type Frame interface {
|
||||
// Reading data.
|
||||
|
||||
// Copy returns a new Frame with a copy of the original series. start is an EasyIndex and len is the number of rows to copy from start onward. If len is negative then all rows from start to the end of the frame are copied. If there are not enough rows to copy then the maximum amount is returned. If there are no items to copy then an empty frame will be returned with a length of zero.
|
||||
//
|
||||
// If start is out of bounds then nil is returned.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// Copy(0, 10) - copy the first 10 items
|
||||
// Copy(-1, 1) - copy the last item
|
||||
// Copy(-10, -1) - copy the last 10 items
|
||||
Copy(start, len int) Frame
|
||||
Contains(names ...string) bool // Contains returns true if the frame contains all the columns specified.
|
||||
Len() int
|
||||
Names() []string
|
||||
Select(names ...string) Frame // Select returns a new Frame with only the specified columns.
|
||||
Series(name string) Series
|
||||
String() string
|
||||
Value(column string, i int) interface{}
|
||||
Float(column string, i int) float64
|
||||
Int(column string, i int) int64
|
||||
Str(column string, i int) string
|
||||
Time(column string, i int) time.Time
|
||||
|
||||
// Writing data.
|
||||
PushSeries(s ...Series) error
|
||||
PushValues(values map[string]interface{}) error
|
||||
RemoveSeries(name string)
|
||||
|
||||
// Easy access functions for common columns.
|
||||
ContainsDOHLCV() bool // ContainsDOHLCV returns true if the frame contains all the columns: Date, Open, High, Low, Close, and Volume.
|
||||
Date(i int) time.Time
|
||||
Open(i int) float64
|
||||
High(i int) float64
|
||||
Low(i int) float64
|
||||
Close(i int) float64
|
||||
Volume(i int) float64
|
||||
Dates() Series
|
||||
Opens() Series
|
||||
Highs() Series
|
||||
Lows() Series
|
||||
Closes() Series
|
||||
Volumes() Series
|
||||
PushCandle(date time.Time, open, high, low, close float64, volume int64) error
|
||||
}
|
||||
|
||||
type DataFrame struct {
|
||||
series map[string]Series
|
||||
rowCounts map[string]int
|
||||
// data *df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
|
||||
}
|
||||
|
||||
func NewDataFrame(series ...Series) *DataFrame {
|
||||
d := &DataFrame{}
|
||||
d.PushSeries(series...)
|
||||
return d
|
||||
}
|
||||
|
||||
// NewDOHLCVDataFrame returns a DataFrame with empty Date, Open, High, Low, Close, and Volume columns.
|
||||
// Use the PushCandle method to add candlesticks in an easy and type-safe way.
|
||||
func NewDOHLCVDataFrame() *DataFrame {
|
||||
return NewDataFrame(
|
||||
NewDataSeries(df.NewSeriesTime("Date", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Open", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("High", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Low", nil)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Close", nil)),
|
||||
NewDataSeries(df.NewSeriesInt64("Volume", nil)),
|
||||
)
|
||||
}
|
||||
|
||||
// Copy returns a new DataFrame with a copy of the original series. start is an EasyIndex and len is the number of rows to copy from start onward. If len is negative then all rows from start to the end of the frame are copied. If there are not enough rows to copy then the maximum amount is returned. If there are no items to copy then an empty frame will be returned with a length of zero.
|
||||
//
|
||||
// If start is out of bounds then nil is returned.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// Copy(0, 10) - copy the first 10 items
|
||||
// Copy(-1, 1) - copy the last item
|
||||
// Copy(-10, -1) - copy the last 10 items
|
||||
func (d *DataFrame) Copy(start, end int) Frame {
|
||||
out := &DataFrame{}
|
||||
for _, v := range d.series {
|
||||
newSeries := v.Copy(start, end)
|
||||
out.PushSeries(newSeries)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of rows in the DataFrame or 0 if the DataFrame is nil. A value less than zero means the
|
||||
// DataFrame has Series of varying lengths.
|
||||
func (d *DataFrame) Len() int {
|
||||
if len(d.series) == 0 {
|
||||
return 0
|
||||
}
|
||||
// Check if all the Series have the same length.
|
||||
var length int
|
||||
for _, v := range d.rowCounts {
|
||||
if length == 0 {
|
||||
length = v
|
||||
} else if length != v {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
// Select returns a new *DataFrame with the selected Series. The series are not copied so the returned Frame will be a reference to the current frame. If a Series name is not found, it is ignored.
|
||||
func (d *DataFrame) Select(names ...string) Frame {
|
||||
out := &DataFrame{}
|
||||
for _, name := range names {
|
||||
out.PushSeries(d.Series(name))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// String returns a string representation of the DataFrame. If the DataFrame is nil, it will return the string "*autotrader.DataFrame[nil]". Otherwise, it will return a string like:
|
||||
//
|
||||
// *autotrader.DataFrame[2x6]
|
||||
// Date Open High Low Close Volume
|
||||
// 1 2019-01-01 1 2 3 4 5
|
||||
// 2 2019-01-02 4 5 6 7 8
|
||||
//
|
||||
// The order of the columns is not defined.
|
||||
//
|
||||
// If the dataframe has more than 20 rows, the output will include the first ten rows and the last ten rows.
|
||||
func (d *DataFrame) String() string {
|
||||
if d == nil {
|
||||
return fmt.Sprintf("%T[nil]", d)
|
||||
}
|
||||
names := d.Names() // Defines the order of the columns.
|
||||
series := make([]Series, len(names))
|
||||
for i, name := range names {
|
||||
series[i] = d.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(d.series))
|
||||
fmt.Fprintln(t, "\t", strings.Join(names, "\t"), "\t")
|
||||
|
||||
printRow := func(i int) {
|
||||
row := make([]string, len(series))
|
||||
for j, s := range series {
|
||||
switch typ := s.Value(i).(type) {
|
||||
case time.Time:
|
||||
row[j] = typ.Format("2006-01-02 15:04:05")
|
||||
case string:
|
||||
row[j] = fmt.Sprintf("%q", typ)
|
||||
default:
|
||||
row[j] = fmt.Sprintf("%v", typ)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(t, strconv.Itoa(i), "\t", strings.Join(row, "\t"), "\t")
|
||||
}
|
||||
|
||||
// Print the first ten rows and the last ten rows if the DataFrame has more than 20 rows.
|
||||
if d.Len() > 20 {
|
||||
for i := 0; i < 10; i++ {
|
||||
printRow(i)
|
||||
}
|
||||
fmt.Fprintf(t, "...\t")
|
||||
for range names {
|
||||
fmt.Fprint(t, "\t") // Keeps alignment.
|
||||
}
|
||||
fmt.Fprintln(t) // Print new line character.
|
||||
for i := 10; i > 0; i-- {
|
||||
printRow(d.Len() - i)
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < d.Len(); i++ {
|
||||
printRow(i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Flush()
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// Date returns the value of the Date column at index i. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Time("Date", i).
|
||||
func (d *DataFrame) Date(i int) time.Time {
|
||||
return d.Time("Date", i)
|
||||
}
|
||||
|
||||
// Open returns the open price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Open", i).
|
||||
func (d *DataFrame) Open(i int) float64 {
|
||||
return d.Float("Open", i)
|
||||
}
|
||||
|
||||
// High returns the high price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("High", i).
|
||||
func (d *DataFrame) High(i int) float64 {
|
||||
return d.Float("High", i)
|
||||
}
|
||||
|
||||
// Low returns the low price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Low", i).
|
||||
func (d *DataFrame) Low(i int) float64 {
|
||||
return d.Float("Low", i)
|
||||
}
|
||||
|
||||
// Close returns the close price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Close", i).
|
||||
func (d *DataFrame) Close(i int) float64 {
|
||||
return d.Float("Close", i)
|
||||
}
|
||||
|
||||
// Volume returns the volume of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
// This is the equivalent to calling Float("Volume", i).
|
||||
func (d *DataFrame) Volume(i int) float64 {
|
||||
return d.Float("Volume", i)
|
||||
}
|
||||
|
||||
// Dates returns a Series of all the dates in the DataFrame.
|
||||
func (d *DataFrame) Dates() Series {
|
||||
return d.Series("Date")
|
||||
}
|
||||
|
||||
// Opens returns a Series of all the open prices in the DataFrame.
|
||||
func (d *DataFrame) Opens() Series {
|
||||
return d.Series("Open")
|
||||
}
|
||||
|
||||
// Highs returns a Series of all the high prices in the DataFrame.
|
||||
func (d *DataFrame) Highs() Series {
|
||||
return d.Series("High")
|
||||
}
|
||||
|
||||
// Lows returns a Series of all the low prices in the DataFrame.
|
||||
func (d *DataFrame) Lows() Series {
|
||||
return d.Series("Low")
|
||||
}
|
||||
|
||||
// Closes returns a Series of all the close prices in the DataFrame.
|
||||
func (d *DataFrame) Closes() Series {
|
||||
return d.Series("Close")
|
||||
}
|
||||
|
||||
// Volumes returns a Series of all the volumes in the DataFrame.
|
||||
func (d *DataFrame) Volumes() Series {
|
||||
return d.Series("Volume")
|
||||
}
|
||||
|
||||
func (d *DataFrame) Contains(names ...string) bool {
|
||||
for _, name := range names {
|
||||
if _, ok := d.series[name]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *DataFrame) ContainsDOHLCV() bool {
|
||||
return d.Contains("Date", "Open", "High", "Low", "Close", "Volume")
|
||||
}
|
||||
|
||||
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close float64, volume int64) error {
|
||||
if len(d.series) == 0 {
|
||||
d.PushSeries(
|
||||
NewDataSeries(df.NewSeriesTime("Date", nil, date)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Open", nil, open)),
|
||||
NewDataSeries(df.NewSeriesFloat64("High", nil, high)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Low", nil, low)),
|
||||
NewDataSeries(df.NewSeriesFloat64("Close", nil, close)),
|
||||
NewDataSeries(df.NewSeriesInt64("Volume", nil, volume)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if !d.ContainsDOHLCV() {
|
||||
return fmt.Errorf("DataFrame does not contain Date, Open, High, Low, Close, Volume columns")
|
||||
}
|
||||
d.series["Date"].Push(date)
|
||||
d.series["Open"].Push(open)
|
||||
d.series["High"].Push(high)
|
||||
d.series["Low"].Push(low)
|
||||
d.series["Close"].Push(close)
|
||||
d.series["Volume"].Push(volume)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFrame) PushValues(values map[string]interface{}) error {
|
||||
if len(d.series) == 0 {
|
||||
return fmt.Errorf("DataFrame has no columns") // TODO: could create the columns here.
|
||||
}
|
||||
for name, value := range values {
|
||||
if _, ok := d.series[name]; !ok {
|
||||
return fmt.Errorf("DataFrame does not contain column %q", name)
|
||||
}
|
||||
d.series[name].Push(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFrame) PushSeries(series ...Series) error {
|
||||
if d.series == nil {
|
||||
d.series = make(map[string]Series, len(series))
|
||||
d.rowCounts = make(map[string]int, len(series))
|
||||
}
|
||||
|
||||
for _, s := range series {
|
||||
name := s.Name()
|
||||
s.SignalConnect("LengthChanged", d.onSeriesLengthChanged, name)
|
||||
s.SignalConnect("NameChanged", d.onSeriesNameChanged, name)
|
||||
d.series[name] = s
|
||||
d.rowCounts[name] = s.Len()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataFrame) RemoveSeries(name string) {
|
||||
s, ok := d.series[name]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
|
||||
s.SignalDisconnect("NameChanged", d.onSeriesNameChanged)
|
||||
delete(d.series, name)
|
||||
delete(d.rowCounts, name)
|
||||
}
|
||||
|
||||
func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) {
|
||||
if len(args) != 2 {
|
||||
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
|
||||
}
|
||||
newLen := args[0].(int)
|
||||
name := args[1].(string)
|
||||
d.rowCounts[name] = newLen
|
||||
}
|
||||
|
||||
func (d *DataFrame) onSeriesNameChanged(args ...interface{}) {
|
||||
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]
|
||||
d.rowCounts[newName] = d.rowCounts[oldName]
|
||||
delete(d.series, oldName)
|
||||
delete(d.rowCounts, oldName)
|
||||
|
||||
// Reconnect our signal handlers to update the name we use in the handlers.
|
||||
d.series[newName].SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
|
||||
d.series[newName].SignalDisconnect("NameChanged", d.onSeriesNameChanged)
|
||||
d.series[newName].SignalConnect("LengthChanged", d.onSeriesLengthChanged, newName)
|
||||
d.series[newName].SignalConnect("NameChanged", d.onSeriesNameChanged, newName)
|
||||
}
|
||||
|
||||
func (d *DataFrame) Names() []string {
|
||||
return maps.Keys(d.series)
|
||||
}
|
||||
|
||||
// Series returns a Series of the column with the given name. If the column does not exist, nil is returned.
|
||||
func (d *DataFrame) Series(name string) Series {
|
||||
if len(d.series) == 0 {
|
||||
return nil
|
||||
}
|
||||
v, ok := d.series[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Value returns the value of the column at index i. The first value is at index 0. A negative value for i can be used to get i values from the latest, like Python's negative indexing. If i is out of bounds, nil is returned.
|
||||
func (d *DataFrame) Value(column string, i int) interface{} {
|
||||
if len(d.series) == 0 {
|
||||
return nil
|
||||
}
|
||||
i = EasyIndex(i, d.Len()) // Allow for negative indexing.
|
||||
if i < 0 || i >= d.Len() { // Prevent out of bounds access.
|
||||
return nil
|
||||
}
|
||||
return d.series[column].Value(i)
|
||||
}
|
||||
|
||||
// Float returns the value of the column at index i casted to float64. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
func (d *DataFrame) Float(column string, i int) float64 {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case float64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Int returns the value of the column at index i casted to int. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||
func (d *DataFrame) Int(column string, i int) int64 {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case int64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the value of the column at index i casted to string. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, "" is returned.
|
||||
func (d *DataFrame) Str(column string, i int) string {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
return val
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Time returns the value of the column at index i casted to time.Time. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, time.Time{} is returned.
|
||||
func (d *DataFrame) Time(column string, i int) time.Time {
|
||||
val := d.Value(column, i)
|
||||
if val == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case time.Time:
|
||||
return val
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
31
frame_test.go
Normal file
31
frame_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDataFrame(t *testing.T) {
|
||||
data := NewDOHLCVDataFrame()
|
||||
if !data.ContainsDOHLCV() {
|
||||
t.Fatalf("Expected data to contain DOHLCV columns")
|
||||
}
|
||||
if data.Len() != 0 {
|
||||
t.Fatalf("Expected 0 rows, got %d", data.Len())
|
||||
}
|
||||
|
||||
err := data.PushCandle(time.Date(2021, 5, 13, 0, 0, 0, 0, time.UTC), 0.8, 1.2, 0.6, 1.0, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
err = data.PushCandle(time.Date(2023, 5, 14, 0, 0, 0, 0, time.UTC), 1.0, 1.4, 0.8, 1.2, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
if data.Len() != 2 {
|
||||
t.Fatalf("Expected 2 row, got %d", data.Len())
|
||||
}
|
||||
if data.Close(-1) != 1.2 {
|
||||
t.Fatalf("Expected latest close to be 1.2, got %f", data.Close(-1))
|
||||
}
|
||||
}
|
24
indicators.go
Normal file
24
indicators.go
Normal file
@ -0,0 +1,24 @@
|
||||
package autotrader
|
||||
|
||||
import "math"
|
||||
|
||||
// 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.
|
||||
func RSI(series Series, periods int) Series {
|
||||
// Calculate the difference between each day's close and the previous day's close.
|
||||
delta := series.MapReverse(func(i int, v interface{}) interface{} {
|
||||
if i == 0 {
|
||||
return float64(0)
|
||||
}
|
||||
return v.(float64) - series.Value(i-1).(float64)
|
||||
})
|
||||
// Make two Series of gains and losses.
|
||||
gains := delta.Map(func(i int, val interface{}) interface{} { return math.Max(val.(float64), 0) })
|
||||
losses := delta.Map(func(i int, val interface{}) interface{} { return math.Abs(math.Min(val.(float64), 0)) })
|
||||
// Calculate the average gain and average loss.
|
||||
avgGain := gains.Rolling(periods).Mean()
|
||||
avgLoss := losses.Rolling(periods).Mean()
|
||||
// Calculate the RSI.
|
||||
return avgGain.Map(func(i int, val interface{}) interface{} {
|
||||
return 100 - (100 / (1 + (val.(float64) / avgLoss.Value(i).(float64))))
|
||||
})
|
||||
}
|
545
series.go
Normal file
545
series.go
Normal file
@ -0,0 +1,545 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
df "github.com/rocketlaunchr/dataframe-go"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type Series interface {
|
||||
Signaler
|
||||
|
||||
// Reading data.
|
||||
|
||||
// Copy returns a new Series with a copy of the original data and Series name. start is an EasyIndex and len is the number of items to copy from start onward. If len is negative then all items from start to the end of the series are copied. If there are not enough items to copy then the maximum amount is returned. If there are no items to copy then an empty DataSeries is returned.
|
||||
//
|
||||
// If start is out of bounds then nil is returned.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// Copy(0, 10) - copy the first 10 items
|
||||
// Copy(-1, 1) - copy the last item
|
||||
// Copy(-10, -1) - copy the last 10 items
|
||||
//
|
||||
// All signals are disconnected from the copy. The copy has its value function reset to its own Value.
|
||||
Copy(start, len int) Series
|
||||
Len() int
|
||||
Name() string // Name returns the immutable name of the Series.
|
||||
Float(i int) float64
|
||||
Int(i int) int64
|
||||
Str(i int) string
|
||||
Time(i int) time.Time
|
||||
Value(i int) interface{}
|
||||
ValueRange(start, end int) []interface{}
|
||||
Values() []interface{} // Values is the same as ValueRange(0, -1).
|
||||
|
||||
// Writing data.
|
||||
|
||||
SetName(name string) Series
|
||||
SetValue(i int, val interface{}) Series
|
||||
Push(val interface{}) Series
|
||||
|
||||
// Functional.
|
||||
|
||||
Filter(f func(i int, val interface{}) bool) Series // Where returns a new Series with only the values that return true for the given function.
|
||||
Map(f func(i int, val interface{}) interface{}) Series // Map returns a new Series with the values modified by the given function.
|
||||
MapReverse(f func(i int, val interface{}) interface{}) Series // MapReverse is the same as Map but it starts from the last item and works backwards.
|
||||
|
||||
// Statistical functions.
|
||||
|
||||
Rolling(period int) *RollingSeries
|
||||
|
||||
// WithValueFunc is used to implement other types of Series that may modify the values by applying a function before returning them, for example. This returns a Series that is a copy of the original with the new value function used whenever a value is requested outside of the Value() method, which will still return the original value.
|
||||
WithValueFunc(value func(i int) interface{}) Series
|
||||
}
|
||||
|
||||
var _ Series = (*AppliedSeries)(nil) // Compile-time interface check.
|
||||
|
||||
// AppliedSeries is like Series, but it applies a function to each row of data before returning it.
|
||||
type AppliedSeries struct {
|
||||
Series
|
||||
apply func(s *AppliedSeries, i int, val interface{}) interface{}
|
||||
}
|
||||
|
||||
func NewAppliedSeries(s Series, apply func(s *AppliedSeries, i int, val interface{}) interface{}) *AppliedSeries {
|
||||
appliedSeries := &AppliedSeries{apply: apply}
|
||||
appliedSeries.Series = s.WithValueFunc(appliedSeries.Value)
|
||||
return appliedSeries
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) Copy(start, len int) Series {
|
||||
return NewAppliedSeries(s.Series.Copy(start, len), s.apply)
|
||||
}
|
||||
|
||||
// Value returns the value of the underlying Series item after applying the function.
|
||||
//
|
||||
// See also: ValueUnapplied()
|
||||
func (s *AppliedSeries) Value(i int) interface{} {
|
||||
return s.apply(s, EasyIndex(i, s.Series.Len()), s.Series.Value(i))
|
||||
}
|
||||
|
||||
// ValueUnapplied returns the value of the underlying Series item without applying the function.
|
||||
//
|
||||
// This is equivalent to:
|
||||
//
|
||||
// s.Series.Value(i)
|
||||
func (s *AppliedSeries) ValueUnapplied(i int) interface{} {
|
||||
return s.Series.Value(i)
|
||||
}
|
||||
|
||||
// SetValue sets the value of the underlying Series item without applying the function.
|
||||
//
|
||||
// This may give unexpected results, as the function will still be applied when the value is requested.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// series := NewSeries(1, 2, 3) // Pseudo-code.
|
||||
// applied := NewAppliedSeries(series, func(_ *AppliedSeries, _ int, val interface{}) interface{} {
|
||||
// return val.(int) * 2
|
||||
// })
|
||||
// applied.SetValue(0, 10)
|
||||
// applied.Value(0) // 20
|
||||
// series.Value(0) // 1
|
||||
func (s *AppliedSeries) SetValue(i int, val interface{}) Series {
|
||||
_ = s.Series.SetValue(i, val)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) Push(val interface{}) Series {
|
||||
_ = s.Series.Push(val)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) Filter(f func(i int, val interface{}) bool) Series {
|
||||
return NewAppliedSeries(s.Series.Filter(f), s.apply)
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) Map(f func(i int, val interface{}) interface{}) Series {
|
||||
return NewAppliedSeries(s.Series.Map(f), s.apply)
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) MapReverse(f func(i int, val interface{}) interface{}) Series {
|
||||
return NewAppliedSeries(s.Series.MapReverse(f), s.apply)
|
||||
}
|
||||
|
||||
func (s *AppliedSeries) WithValueFunc(value func(i int) interface{}) Series {
|
||||
return &AppliedSeries{Series: s.Series.WithValueFunc(value), apply: s.apply}
|
||||
}
|
||||
|
||||
var _ Series = (*RollingSeries)(nil) // Compile-time interface check.
|
||||
|
||||
type RollingSeries struct {
|
||||
Series
|
||||
period int
|
||||
}
|
||||
|
||||
func NewRollingSeries(s Series, period int) *RollingSeries {
|
||||
series := &RollingSeries{period: period}
|
||||
series.Series = s.WithValueFunc(series.Value)
|
||||
return series
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Copy(start, len int) Series {
|
||||
return NewRollingSeries(s.Series.Copy(start, len), s.period)
|
||||
}
|
||||
|
||||
// Value returns []interface{} up to `period` long. The last item in the slice is the item at i. If i is out of bounds, nil is returned.
|
||||
func (s *RollingSeries) Value(i int) interface{} {
|
||||
items := make([]interface{}, 0, s.period)
|
||||
i = EasyIndex(i, s.Len())
|
||||
if i < 0 || i >= s.Len() {
|
||||
return items
|
||||
}
|
||||
for j := i; j > i-s.period && j >= 0; j-- {
|
||||
// items = append(items, s.Series.Value(j))
|
||||
items = slices.Insert(items, 0, s.Series.Value(j))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *RollingSeries) SetValue(i int, val interface{}) Series {
|
||||
_ = s.Series.SetValue(i, val)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Push(val interface{}) Series {
|
||||
_ = s.Series.Push(val)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Filter(f func(i int, val interface{}) bool) Series {
|
||||
return NewRollingSeries(s.Series.Filter(f), s.period)
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Map(f func(i int, val interface{}) interface{}) Series {
|
||||
return NewRollingSeries(s.Series.Map(f), s.period)
|
||||
}
|
||||
|
||||
func (s *RollingSeries) MapReverse(f func(i int, val interface{}) interface{}) Series {
|
||||
return NewRollingSeries(s.Series.MapReverse(f), s.period)
|
||||
}
|
||||
|
||||
// Average is an alias for Mean.
|
||||
func (s *RollingSeries) Average() *AppliedSeries {
|
||||
return s.Mean()
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Mean() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch v[0].(type) {
|
||||
case float64:
|
||||
var sum float64
|
||||
for _, v := range v {
|
||||
sum += v.(float64)
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) EMA() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch v[0].(type) {
|
||||
case float64:
|
||||
ema := v[0].(float64)
|
||||
for _, v := range v[1:] {
|
||||
ema += (v.(float64) - ema) * 2 / (float64(s.period) + 1)
|
||||
}
|
||||
return ema
|
||||
case int64:
|
||||
ema := v[0].(int64)
|
||||
for _, v := range v[1:] {
|
||||
ema += (v.(int64) - ema) * 2 / (int64(s.period) + 1)
|
||||
}
|
||||
return ema
|
||||
default: // string, time.Time
|
||||
return v[len(v)-1] // Do nothing
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("expected a slice of values, got %t", v))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) Median() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch v[0].(type) {
|
||||
case float64:
|
||||
if len(v) == 0 {
|
||||
return float64(0)
|
||||
}
|
||||
slices.SortFunc(v, func(a, b interface{}) 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 interface{}) 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
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("expected a slice of values, got %t", v))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) StdDev() *AppliedSeries {
|
||||
return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RollingSeries) WithValueFunc(value func(i int) interface{}) Series {
|
||||
return &RollingSeries{Series: s.Series.WithValueFunc(value), period: s.period}
|
||||
}
|
||||
|
||||
// DataSeries is a Series that wraps a column of data. The data can be of the following types: float64, int64, string, or time.Time.
|
||||
//
|
||||
// Signals:
|
||||
// - LengthChanged(int) - when the data is appended or an item is removed.
|
||||
// - NameChanged(string) - when the name is changed.
|
||||
type DataSeries struct {
|
||||
SignalManager
|
||||
data df.Series
|
||||
value func(i int) interface{}
|
||||
}
|
||||
|
||||
func NewDataSeries(data df.Series) *DataSeries {
|
||||
dataSeries := &DataSeries{
|
||||
SignalManager: SignalManager{},
|
||||
data: data,
|
||||
}
|
||||
dataSeries.value = dataSeries.Value
|
||||
return dataSeries
|
||||
}
|
||||
|
||||
// Copy returns a new DataSeries with a copy of the original data and Series name. start is an EasyIndex and len is the number of items to copy from start onward. If len is negative then all items from start to the end of the series are copied. If there are not enough items to copy then the maximum amount is returned. If there are no items to copy then an empty DataSeries is returned.
|
||||
//
|
||||
// If start is out of bounds then nil is returned.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// Copy(0, 10) - copy the first 10 items
|
||||
// Copy(-1, 1) - copy the last item
|
||||
// Copy(-10, -1) - copy the last 10 items
|
||||
//
|
||||
// All signals are disconnected from the copy. The copy has its value function reset to its own Value.
|
||||
func (s *DataSeries) Copy(start, len int) Series {
|
||||
start = EasyIndex(start, s.Len())
|
||||
var _end *int
|
||||
if start < 0 || start >= s.Len() {
|
||||
return nil
|
||||
} else if len >= 0 {
|
||||
end := start + len
|
||||
if end < s.Len() {
|
||||
if end < start {
|
||||
copy := s.data.Copy()
|
||||
copy.Reset()
|
||||
series := &DataSeries{SignalManager{}, copy, nil}
|
||||
series.value = series.Value
|
||||
return series
|
||||
}
|
||||
_end = &end
|
||||
}
|
||||
}
|
||||
return &DataSeries{
|
||||
SignalManager: SignalManager{},
|
||||
data: s.data.Copy(df.Range{Start: &start, End: _end}),
|
||||
value: s.value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Name() string {
|
||||
return s.data.Name()
|
||||
}
|
||||
|
||||
func (s *DataSeries) SetName(name string) Series {
|
||||
if name == s.Name() {
|
||||
return s
|
||||
}
|
||||
s.data.Rename(name)
|
||||
s.SignalEmit("NameChanged", name)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DataSeries) Len() int {
|
||||
if s.data == nil {
|
||||
return 0
|
||||
}
|
||||
return s.data.NRows()
|
||||
}
|
||||
|
||||
func (s *DataSeries) Push(value interface{}) Series {
|
||||
if s.data != nil {
|
||||
s.data.Append(value)
|
||||
s.SignalEmit("LengthChanged", s.Len())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DataSeries) SetValue(i int, val interface{}) Series {
|
||||
if s.data != nil {
|
||||
s.data.Update(EasyIndex(i, s.Len()), val)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DataSeries) Value(i int) interface{} {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
i = EasyIndex(i, s.Len()) // Allow for negative indexing.
|
||||
return s.data.Value(i)
|
||||
}
|
||||
|
||||
// ValueRange returns a slice of values from start to end, including start and end. The first value is at index 0. A negative value for start or end can be used to get values from the latest, like Python's negative indexing. If end is less than zero, it will be sliced from start to the last item. If start or end is out of bounds, nil is returned. If start is greater than end, nil is returned.
|
||||
func (s *DataSeries) ValueRange(start, end int) []interface{} {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
start = EasyIndex(start, s.Len())
|
||||
if end < 0 {
|
||||
end = s.Len() - 1
|
||||
}
|
||||
if start < 0 || start >= s.Len() || end >= s.Len() || start > end {
|
||||
return nil
|
||||
}
|
||||
|
||||
items := make([]interface{}, end-start+1)
|
||||
for i := start; i <= end; i++ {
|
||||
items[i-start] = s.value(i)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *DataSeries) Values() []interface{} {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
return s.ValueRange(0, -1)
|
||||
}
|
||||
|
||||
func (s *DataSeries) Float(i int) float64 {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case float64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Int(i int) int64 {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case int64:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Str(i int) string {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
return val
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Time(i int) time.Time {
|
||||
val := s.value(i)
|
||||
if val == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case time.Time:
|
||||
return val
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DataSeries) Filter(f func(i int, val interface{}) bool) Series {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
series := &DataSeries{SignalManager{}, df.NewSeriesGeneric(s.data.Name(), (interface{})(nil), nil), s.value}
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
if val := series.value(i); f(i, val) {
|
||||
series.Push(val)
|
||||
}
|
||||
}
|
||||
return series
|
||||
}
|
||||
|
||||
func (s *DataSeries) Map(f func(i int, val interface{}) interface{}) Series {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
series := &DataSeries{SignalManager{}, s.data.Copy(), s.value}
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
series.SetValue(i, f(i, series.value(i)))
|
||||
}
|
||||
return series
|
||||
}
|
||||
|
||||
func (s *DataSeries) MapReverse(f func(i int, val interface{}) interface{}) Series {
|
||||
if s.data == nil {
|
||||
return nil
|
||||
}
|
||||
series := &DataSeries{SignalManager{}, s.data.Copy(), s.value}
|
||||
for i := s.Len() - 1; i >= 0; i-- {
|
||||
series.SetValue(i, f(i, series.value(i)))
|
||||
}
|
||||
return series
|
||||
}
|
||||
|
||||
func (s *DataSeries) Rolling(period int) *RollingSeries {
|
||||
return NewRollingSeries(s, period)
|
||||
}
|
||||
|
||||
func (s *DataSeries) WithValueFunc(value func(i int) interface{}) Series {
|
||||
copy := s.Copy(0, -1).(*DataSeries)
|
||||
copy.value = value
|
||||
return copy
|
||||
}
|
96
series_test.go
Normal file
96
series_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rocketlaunchr/dataframe-go"
|
||||
)
|
||||
|
||||
func TestAppliedSeries(t *testing.T) {
|
||||
underlying := NewDataSeries(dataframe.NewSeriesFloat64("test", nil, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
applied := NewAppliedSeries(underlying, func(_ *AppliedSeries, _ int, val interface{}) interface{} {
|
||||
return val.(float64) * 2
|
||||
})
|
||||
|
||||
if applied.Len() != 10 {
|
||||
t.Fatalf("Expected 10 rows, got %d", applied.Len())
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if val := applied.Float(i); val != float64(i+1)*2 {
|
||||
t.Errorf("(%d)\tExpected %f, got %v", i, float64(i+1)*2, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the underlying series is not modified.
|
||||
if underlying.Len() != 10 {
|
||||
t.Fatalf("Expected 10 rows, got %d", underlying.Len())
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if val := underlying.Float(i); val != float64(i+1) {
|
||||
t.Errorf("(%d)\tExpected %f, got %v", i, float64(i+1), val)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the underlying series is not modified when the applied series is modified.
|
||||
applied.SetValue(0, 100)
|
||||
if underlying.Float(0) != 1 {
|
||||
t.Errorf("Expected 1, got %v", underlying.Float(0))
|
||||
}
|
||||
if applied.Float(0) != 200 {
|
||||
t.Errorf("Expected 200, got %v", applied.Float(0))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollingAppliedSeries(t *testing.T) {
|
||||
// Test rolling average.
|
||||
series := NewDataSeries(dataframe.NewSeriesFloat64("test", nil, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
|
||||
sma5Expected := []float64{1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8}
|
||||
sma5 := (Series)(series.Rolling(5).Average()) // Take the 5 period moving average and cast it to Series.
|
||||
if sma5.Len() != 10 {
|
||||
t.Fatalf("Expected 10 rows, got %d", sma5.Len())
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
// Calling Float instead of Value is very important. Value will call the AppliedSeries.Value method
|
||||
// while Float calls Series.Float which is what most people will use and is the most likely to be
|
||||
// problematic as it is supposed to route through the DataSeries.value method.
|
||||
if val := sma5.Float(i); !EqualApprox(val, sma5Expected[i]) {
|
||||
t.Errorf("(%d)\tExpected %f, got %v", i, sma5Expected[i], val)
|
||||
}
|
||||
}
|
||||
|
||||
ema5Expected := []float64{1, 1.3333333333333333, 1.8888888888888888, 2.5925925925925926, 3.3950617283950617, 4.395061728395062, 5.395061728395062, 6.395061728395062, 7.395061728395062, 8.395061728395062}
|
||||
ema5 := (Series)(series.Rolling(5).EMA()) // Take the 5 period exponential moving average.
|
||||
if ema5.Len() != 10 {
|
||||
t.Fatalf("Expected 10 rows, got %d", ema5.Len())
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if val := ema5.Float(i); !EqualApprox(val, ema5Expected[i]) {
|
||||
t.Errorf("(%d)\tExpected %f, got %v", i, ema5Expected[i], val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataSeries(t *testing.T) {
|
||||
data, err := EURUSD()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
dates, closes := data.Dates(), data.Closes()
|
||||
|
||||
if dates.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", dates.Len())
|
||||
}
|
||||
if closes.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", closes.Len())
|
||||
}
|
||||
|
||||
sma10 := closes.Rolling(10).Mean()
|
||||
if sma10.Len() != 2610 {
|
||||
t.Fatalf("Expected 2610 rows, got %d", sma10.Len())
|
||||
}
|
||||
if sma10.Value(-1) != 1.10039 { // Latest closing price averaged over 10 periods.
|
||||
t.Fatalf("Expected 1.10039, got %f", sma10.Value(-1))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user