Gave the Series, Frame, and Signals a lot of needed love

This commit is contained in:
Luke I. Wilson 2023-05-18 11:25:36 -05:00
parent a68542d34b
commit 52260551e8
8 changed files with 491 additions and 364 deletions

View File

@ -228,19 +228,17 @@ func (b *TestBroker) Advance() {
} }
} }
// Candles returns the last count candles for the given symbol and frequency. If count is greater than the number of candles, then a dataframe with zero rows is returned.
//
// If the TestBroker has a data broker set, then it will use that to get candles. Otherwise, it will return the candles from the data that was set. The first call to Candles will fetch candles from the data broker if it is set, so it is recommended to set the data broker before the first call to Candles and to call Candles the first time with the number of candles you want to fetch.
func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataFrame, error) { func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataFrame, error) {
start := Max(Max(b.candleCount, 1)-count, 0) start := Max(Max(b.candleCount, 1)-count, 0)
end := b.candleCount - 1 adjCount := b.candleCount - start
if b.Data != nil && b.candleCount >= b.Data.Len() { // We have data and we are at the end of it. if b.Data != nil && b.candleCount >= b.Data.Len() { // We have data and we are at the end of it.
if count >= b.Data.Len() { // We are asking for more data than we have. return b.Data.Copy(-count, -1).(*DataFrame), ErrEOF // Return the last count candles.
return b.Data.Copy(0, -1).(*DataFrame), ErrEOF
} else {
return b.Data.Copy(start, -1).(*DataFrame), ErrEOF
}
} else if b.DataBroker != nil && b.Data == nil { // We have a data broker but no data. } else if b.DataBroker != nil && b.Data == nil { // We have a data broker but no data.
// Fetch a lot of candles from the broker so we don't keep asking. candles, err := b.DataBroker.Candles(symbol, frequency, count)
candles, err := b.DataBroker.Candles(symbol, frequency, Max(count, 1000))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -248,10 +246,7 @@ func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataF
} else if b.Data == nil { // Both b.DataBroker and b.Data are nil. } else if b.Data == nil { // Both b.DataBroker and b.Data are nil.
return nil, ErrNoData return nil, ErrNoData
} }
return b.Data.Copy(start, adjCount).(*DataFrame), nil
// TODO: check if count > our rows if we are using a data broker and then fetch more data if so.
return b.Data.Copy(start, end).(*DataFrame), nil
} }
func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) { func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) {

106
data.go
View File

@ -2,13 +2,10 @@ package autotrader
import ( import (
"encoding/csv" "encoding/csv"
"errors"
"io" "io"
"os" "os"
"strconv" "strconv"
"time" "time"
df "github.com/rocketlaunchr/dataframe-go"
) )
type DataCSVLayout struct { type DataCSVLayout struct {
@ -73,102 +70,51 @@ func DataFrameFromCSVReaderLayout(r io.Reader, layout DataCSVLayout) (*DataFrame
data.Series(name).SetName(newName) data.Series(name).SetName(newName)
} }
// err = data.ReorderColumns([]string{"Date", "Open", "High", "Low", "Close", "Volume"})
// if err != nil {
// return data, err
// }
// TODO: Reverse the dataframe if the latest data is first.
return data, nil return data, nil
} }
func DataFrameFromCSVReader(r io.Reader, dateLayout string, readReversed bool) (*DataFrame, error) { func DataFrameFromCSVReader(r io.Reader, dateLayout string, readReversed bool) (*DataFrame, error) {
csv := csv.NewReader(r) csv := csv.NewReader(r)
csv.LazyQuotes = true csv.LazyQuotes = true
records, err := csv.ReadAll()
if err != nil { seriesSlice := make([]Series, 0, 12)
// Read the CSV file.
for {
rec, err := csv.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err return nil, err
} }
if len(records) < 2 { // Create the columns needed.
return nil, errors.New("csv file must have at least 2 rows") if len(seriesSlice) == 0 {
for _, val := range rec {
seriesSlice = append(seriesSlice, NewDataSeries(val))
} }
continue
dfSeriesSlice := make([]df.Series, 0, 12)
// TODO: change Capacity to Size.
initOptions := &df.SeriesInit{Capacity: len(records) - 1}
// Replace column names with standard ones.
for j, val := range records[0] {
// Check what type the next row is to determine the type of the series.
nextRow := records[1][j]
var series df.Series
if _, err := strconv.ParseFloat(nextRow, 64); err == nil {
series = df.NewSeriesFloat64(val, initOptions)
} else if _, err := strconv.ParseInt(nextRow, 10, 64); err == nil {
series = df.NewSeriesInt64(val, initOptions)
} else if _, err := time.Parse(dateLayout, nextRow); err == nil {
series = df.NewSeriesTime(val, initOptions)
} else {
series = df.NewSeriesString(val, initOptions)
} }
// Create the series columns and label them.
dfSeriesSlice = append(dfSeriesSlice, series)
}
// Set the direction to iterate the records.
var startIdx, stopIdx, inc int
if readReversed {
startIdx = len(records) - 1
stopIdx = 0 // Stop before the first row because it contains the column names.
inc = -1
} else {
startIdx = 1 // Skip first row because it contains the column names.
stopIdx = len(records)
inc = 1
}
for i := startIdx; i != stopIdx; i += inc {
rec := records[i]
// Add rows to the series. // Add rows to the series.
for j, val := range rec { for j, val := range rec {
series := dfSeriesSlice[j] series := seriesSlice[j]
switch series.Type() { if f, err := strconv.ParseFloat(val, 64); err == nil {
case "float64": series.Push(f)
val, err := strconv.ParseFloat(val, 64) } else if t, err := time.Parse(dateLayout, val); err == nil {
if err != nil { series.Push(t)
series.Append(nil)
} else { } else {
series.Append(val) series.Push(val)
} }
case "int64":
val, err := strconv.ParseInt(val, 10, 64)
if err != nil {
series.Append(nil)
} else {
series.Append(val)
}
case "time":
val, err := time.Parse(dateLayout, val)
if err != nil {
series.Append(nil)
} else {
series.Append(val)
}
case "string":
series.Append(val)
}
dfSeriesSlice[j] = series
} }
} }
// NOTE: we specifically construct the DataFrame at the end of the function because it likes to set // Reverse the series if needed.
// state like number of rows and columns at initialization and won't let you change it later. if readReversed {
seriesSlice := make([]Series, len(dfSeriesSlice)) for _, series := range seriesSlice {
for i, series := range dfSeriesSlice { series.Reverse()
seriesSlice[i] = NewDataSeries(series)
} }
}
return NewDataFrame(seriesSlice...), nil return NewDataFrame(seriesSlice...), nil
} }

155
frame.go
View File

@ -3,44 +3,42 @@ package autotrader
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"time" "time"
df "github.com/rocketlaunchr/dataframe-go"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
type Frame interface { type Frame interface {
// Reading data. // 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. // Copy returns a new Frame 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 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 aframe will be returned with a length of zero but with the same column names as the original.
//
// If start is out of bounds then nil is returned.
// //
// Examples: // Examples:
// //
// Copy(0, 10) - copy the first 10 items // Copy(0, 10) - copy the first 10 items
// Copy(-1, 1) - copy the last item // Copy(-1, 1) - copy the last item
// Copy(-10, -1) - copy the last 10 items // Copy(-10, -1) - copy the last 10 items
Copy(start, len int) Frame Copy(start, count int) Frame
Contains(names ...string) bool // Contains returns true if the frame contains all the columns specified. Contains(names ...string) bool // Contains returns true if the frame contains all the columns specified.
Len() int Len() int
Names() []string Names() []string
Select(names ...string) Frame // Select returns a new Frame with only the specified columns. Select(names ...string) Frame // Select returns a new Frame with only the specified columns.
Series(name string) Series Series(name string) Series
String() string String() string
Value(column string, i int) interface{} Value(column string, i int) any
Float(column string, i int) float64 Float(column string, i int) float64
Int(column string, i int) int64 Int(column string, i int) int
Str(column string, i int) string Str(column string, i int) string
Time(column string, i int) time.Time Time(column string, i int) time.Time
// Writing data. // Writing data.
PushSeries(s ...Series) error PushSeries(s ...Series) error
PushValues(values map[string]interface{}) error PushValues(values map[string]any) error
RemoveSeries(name string) RemoveSeries(names ...string)
// Easy access functions for common columns. // 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. ContainsDOHLCV() bool // ContainsDOHLCV returns true if the frame contains all the columns: Date, Open, High, Low, Close, and Volume.
@ -49,7 +47,7 @@ type Frame interface {
High(i int) float64 High(i int) float64
Low(i int) float64 Low(i int) float64
Close(i int) float64 Close(i int) float64
Volume(i int) float64 Volume(i int) int
Dates() Series Dates() Series
Opens() Series Opens() Series
Highs() Series Highs() Series
@ -75,56 +73,51 @@ func NewDataFrame(series ...Series) *DataFrame {
// Use the PushCandle method to add candlesticks in an easy and type-safe way. // Use the PushCandle method to add candlesticks in an easy and type-safe way.
func NewDOHLCVDataFrame() *DataFrame { func NewDOHLCVDataFrame() *DataFrame {
return NewDataFrame( return NewDataFrame(
NewDataSeries(df.NewSeriesTime("Date", nil)), NewDataSeries("Date"),
NewDataSeries(df.NewSeriesFloat64("Open", nil)), NewDataSeries("Open"),
NewDataSeries(df.NewSeriesFloat64("High", nil)), NewDataSeries("High"),
NewDataSeries(df.NewSeriesFloat64("Low", nil)), NewDataSeries("Low"),
NewDataSeries(df.NewSeriesFloat64("Close", nil)), NewDataSeries("Close"),
NewDataSeries(df.NewSeriesInt64("Volume", nil)), NewDataSeries("Volume"),
) )
} }
// 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. // Copy returns a new DataFrame 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 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 aframe will be returned with a length of zero but with the same column names as the original.
//
// If start is out of bounds then nil is returned.
// //
// Examples: // Examples:
// //
// Copy(0, 10) - copy the first 10 items // Copy(0, 10) - copy the first 10 items
// Copy(-1, 1) - copy the last item // Copy(-1, 1) - copy the last item
// Copy(-10, -1) - copy the last 10 items // Copy(-10, -1) - copy the last 10 items
func (d *DataFrame) Copy(start, end int) Frame { func (d *DataFrame) Copy(start, count int) Frame {
out := &DataFrame{} out := &DataFrame{}
for _, v := range d.series { for _, s := range d.series {
newSeries := v.Copy(start, end) out.PushSeries(s.Copy(start, count))
out.PushSeries(newSeries)
} }
return out 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 // Len returns the number of rows in the dataframe or 0 if the dataframe has no rows. If the dataframe has series of different lengths, then the longest length series is returned.
// DataFrame has Series of varying lengths.
func (d *DataFrame) Len() int { func (d *DataFrame) Len() int {
if len(d.series) == 0 { if len(d.series) == 0 {
return 0 return 0
} }
// Check if all the Series have the same length.
var length int var length int
for _, v := range d.rowCounts { for _, v := range d.rowCounts {
if length == 0 { if v > length {
length = v length = v
} else if length != v {
return -1
} }
} }
return length 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. // 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 { func (d *DataFrame) Select(names ...string) Frame {
out := &DataFrame{} out := &DataFrame{}
for _, name := range names { for _, name := range names {
out.PushSeries(d.Series(name)) if s := d.Series(name); s != nil {
out.PushSeries(s)
}
} }
return out return out
} }
@ -192,40 +185,40 @@ func (d *DataFrame) String() string {
return buffer.String() 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. // Date returns the value of the Date column at index i. The first value is at index 0. A negative value for i 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.
// This is the equivalent to calling Time("Date", i). // This is the equivalent to calling Time("Date", i).
func (d *DataFrame) Date(i int) time.Time { func (d *DataFrame) Date(i int) time.Time {
return d.Time("Date", i) 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. // Open returns the open price of the candle at index i. The first candle is at index 0. A negative value for i can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, NaN is returned.
// This is the equivalent to calling Float("Open", i). // This is the equivalent to calling Float("Open", i).
func (d *DataFrame) Open(i int) float64 { func (d *DataFrame) Open(i int) float64 {
return d.Float("Open", i) 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. // High returns the high price of the candle at index i. The first candle is at index 0. A negative value for i can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, NaN is returned.
// This is the equivalent to calling Float("High", i). // This is the equivalent to calling Float("High", i).
func (d *DataFrame) High(i int) float64 { func (d *DataFrame) High(i int) float64 {
return d.Float("High", i) 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. // Low returns the low price of the candle at index i. The first candle is at index 0. A negative value for i can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, NaN is returned.
// This is the equivalent to calling Float("Low", i). // This is the equivalent to calling Float("Low", i).
func (d *DataFrame) Low(i int) float64 { func (d *DataFrame) Low(i int) float64 {
return d.Float("Low", i) 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. // Close returns the close price of the candle at index i. The first candle is at index 0. A negative value for i can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, NaN is returned.
// This is the equivalent to calling Float("Close", i). // This is the equivalent to calling Float("Close", i).
func (d *DataFrame) Close(i int) float64 { func (d *DataFrame) Close(i int) float64 {
return d.Float("Close", i) 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. // Volume returns the volume of the candle at index i. The first candle is at index 0. A negative value for i 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). // This is the equivalent to calling Float("Volume", i).
func (d *DataFrame) Volume(i int) float64 { func (d *DataFrame) Volume(i int) int {
return d.Float("Volume", i) return d.Int("Volume", i)
} }
// Dates returns a Series of all the dates in the DataFrame. // Dates returns a Series of all the dates in the DataFrame.
@ -258,6 +251,7 @@ func (d *DataFrame) Volumes() Series {
return d.Series("Volume") return d.Series("Volume")
} }
// Contains returns true if the DataFrame contains all the given series names.
func (d *DataFrame) Contains(names ...string) bool { func (d *DataFrame) Contains(names ...string) bool {
for _, name := range names { for _, name := range names {
if _, ok := d.series[name]; !ok { if _, ok := d.series[name]; !ok {
@ -267,22 +261,13 @@ func (d *DataFrame) Contains(names ...string) bool {
return true return true
} }
// ContainsDOHLCV returns true if the DataFrame contains the series "Date", "Open", "High", "Low", "Close", and "Volume".
func (d *DataFrame) ContainsDOHLCV() bool { func (d *DataFrame) ContainsDOHLCV() bool {
return d.Contains("Date", "Open", "High", "Low", "Close", "Volume") return d.Contains("Date", "Open", "High", "Low", "Close", "Volume")
} }
// PushCandle pushes a candlestick to the dataframe. If the dataframe does not contain the series "Date", "Open", "High", "Low", "Close", and "Volume", an error is returned.
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close float64, volume int64) error { 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() { if !d.ContainsDOHLCV() {
return fmt.Errorf("DataFrame does not contain Date, Open, High, Low, Close, Volume columns") return fmt.Errorf("DataFrame does not contain Date, Open, High, Low, Close, Volume columns")
} }
@ -295,9 +280,10 @@ func (d *DataFrame) PushCandle(date time.Time, open, high, low, close float64, v
return nil return nil
} }
func (d *DataFrame) PushValues(values map[string]interface{}) error { // PushValues uses the keys of the values map as the names of the series to push the values to. If the dataframe does not contain a series with a given name, an error is returned.
func (d *DataFrame) PushValues(values map[string]any) error {
if len(d.series) == 0 { if len(d.series) == 0 {
return fmt.Errorf("DataFrame has no columns") // TODO: could create the columns here. return fmt.Errorf("DataFrame has no columns")
} }
for name, value := range values { for name, value := range values {
if _, ok := d.series[name]; !ok { if _, ok := d.series[name]; !ok {
@ -308,6 +294,7 @@ func (d *DataFrame) PushValues(values map[string]interface{}) error {
return nil return nil
} }
// PushSeries adds the given series to the dataframe. If the dataframe already contains a series with the same name, an error is returned.
func (d *DataFrame) PushSeries(series ...Series) error { func (d *DataFrame) PushSeries(series ...Series) error {
if d.series == nil { if d.series == nil {
d.series = make(map[string]Series, len(series)) d.series = make(map[string]Series, len(series))
@ -316,8 +303,11 @@ func (d *DataFrame) PushSeries(series ...Series) error {
for _, s := range series { for _, s := range series {
name := s.Name() name := s.Name()
s.SignalConnect("LengthChanged", d.onSeriesLengthChanged, name) if _, ok := d.series[name]; ok {
s.SignalConnect("NameChanged", d.onSeriesNameChanged, name) return fmt.Errorf("DataFrame already contains column %q", name)
}
s.SignalConnect("LengthChanged", d, d.onSeriesLengthChanged, name)
s.SignalConnect("NameChanged", d, d.onSeriesNameChanged, name)
d.series[name] = s d.series[name] = s
d.rowCounts[name] = s.Len() d.rowCounts[name] = s.Len()
} }
@ -325,18 +315,21 @@ func (d *DataFrame) PushSeries(series ...Series) error {
return nil return nil
} }
func (d *DataFrame) RemoveSeries(name string) { // RemoveSeries removes the given series from the dataframe. If the dataframe does not contain a series with a given name, nothing happens.
func (d *DataFrame) RemoveSeries(names ...string) {
for _, name := range names {
s, ok := d.series[name] s, ok := d.series[name]
if !ok { if !ok {
return return
} }
s.SignalDisconnect("LengthChanged", d.onSeriesLengthChanged) s.SignalDisconnect("LengthChanged", d, d.onSeriesLengthChanged)
s.SignalDisconnect("NameChanged", d.onSeriesNameChanged) s.SignalDisconnect("NameChanged", d, d.onSeriesNameChanged)
delete(d.series, name) delete(d.series, name)
delete(d.rowCounts, name) delete(d.rowCounts, name)
} }
}
func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) { func (d *DataFrame) onSeriesLengthChanged(args ...any) {
if len(args) != 2 { if len(args) != 2 {
panic(fmt.Sprintf("expected two arguments, got %d", len(args))) panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
} }
@ -345,7 +338,7 @@ func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) {
d.rowCounts[name] = newLen d.rowCounts[name] = newLen
} }
func (d *DataFrame) onSeriesNameChanged(args ...interface{}) { func (d *DataFrame) onSeriesNameChanged(args ...any) {
if len(args) != 2 { if len(args) != 2 {
panic(fmt.Sprintf("expected two arguments, got %d", len(args))) panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
} }
@ -358,12 +351,13 @@ func (d *DataFrame) onSeriesNameChanged(args ...interface{}) {
delete(d.rowCounts, oldName) delete(d.rowCounts, oldName)
// Reconnect our signal handlers to update the name we use in the handlers. // Reconnect our signal handlers to update the name we use in the handlers.
d.series[newName].SignalDisconnect("LengthChanged", d.onSeriesLengthChanged) d.series[newName].SignalDisconnect("LengthChanged", d, d.onSeriesLengthChanged)
d.series[newName].SignalDisconnect("NameChanged", d.onSeriesNameChanged) d.series[newName].SignalDisconnect("NameChanged", d, d.onSeriesNameChanged)
d.series[newName].SignalConnect("LengthChanged", d.onSeriesLengthChanged, newName) d.series[newName].SignalConnect("LengthChanged", d, d.onSeriesLengthChanged, newName)
d.series[newName].SignalConnect("NameChanged", d.onSeriesNameChanged, newName) d.series[newName].SignalConnect("NameChanged", d, d.onSeriesNameChanged, newName)
} }
// Names returns a slice of the names of the series in the dataframe.
func (d *DataFrame) Names() []string { func (d *DataFrame) Names() []string {
return maps.Keys(d.series) return maps.Keys(d.series)
} }
@ -381,51 +375,41 @@ func (d *DataFrame) Series(name string) Series {
} }
// 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. // 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{} { func (d *DataFrame) Value(column string, i int) any {
if len(d.series) == 0 { if len(d.series) == 0 {
return nil return nil
} }
i = EasyIndex(i, d.Len()) // Allow for negative indexing. if s, ok := d.series[column]; ok {
if i < 0 || i >= d.Len() { // Prevent out of bounds access. return s.Value(i)
}
return nil 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. // 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 can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, NaN is returned.
func (d *DataFrame) Float(column string, i int) float64 { func (d *DataFrame) Float(column string, i int) float64 {
val := d.Value(column, i) val := d.Value(column, i)
if val == nil {
return 0
}
switch val := val.(type) { switch val := val.(type) {
case float64: case float64:
return val return val
default: default:
return 0 return math.NaN()
} }
} }
// 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. // 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 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 { func (d *DataFrame) Int(column string, i int) int {
val := d.Value(column, i) val := d.Value(column, i)
if val == nil {
return 0
}
switch val := val.(type) { switch val := val.(type) {
case int64: case int:
return val return val
default: default:
return 0 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. // 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 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 { func (d *DataFrame) Str(column string, i int) string {
val := d.Value(column, i) val := d.Value(column, i)
if val == nil {
return ""
}
switch val := val.(type) { switch val := val.(type) {
case string: case string:
return val return val
@ -434,12 +418,9 @@ func (d *DataFrame) Str(column string, i int) string {
} }
} }
// 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. // 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 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 { func (d *DataFrame) Time(column string, i int) time.Time {
val := d.Value(column, i) val := d.Value(column, i)
if val == nil {
return time.Time{}
}
switch val := val.(type) { switch val := val.(type) {
case time.Time: case time.Time:
return val return val

View File

@ -5,7 +5,70 @@ import (
"time" "time"
) )
func TestDataFrame(t *testing.T) { func TestDataFrameSeriesManagement(t *testing.T) {
data := NewDataFrame(NewDataSeries("A"), NewDataSeries("B"))
if data.Len() != 0 {
t.Fatalf("Expected 0 rows, got %d", data.Len())
}
if data.Contains("A", "B") != true {
t.Fatalf("Expected data to contain A and B columns")
}
err := data.PushSeries(NewDataSeries("C"))
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if len(data.Names()) != 3 {
t.Fatalf("Expected 3 columns, got %d", len(data.Names()))
}
if data.Contains("C") != true {
t.Fatalf("Expected data to contain C column")
}
err = data.PushValues(map[string]any{"A": 1, "B": 2, "C": 3})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if data.Len() != 1 {
t.Fatalf("Expected 1 row, got %d", data.Len())
}
if data.Int("B", -1) != 2 {
t.Fatalf("Expected latest B to be 2, got %d", data.Int("B", -1))
}
err = data.PushValues(map[string]any{"A": 4, "B": 5, "C": 6})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if data.Len() != 2 {
t.Fatalf("Expected 2 rows, got %d", data.Len())
}
if data.Int("B", -1) != 5 {
t.Fatalf("Expected latest B to be 5, got %d", data.Int("B", -1))
}
selected := data.Select("A", "C")
if len(selected.Names()) != 2 {
t.Fatalf("Expected 2 selected columns, got %d", len(selected.Names()))
}
if selected.Int("A", -1) != 4 {
t.Fatalf("Expected latest A to be 4, got %d", selected.Int("A", -1))
}
data.RemoveSeries("B")
if data.Contains("B") != false {
t.Fatalf("Expected data to not contain B column")
}
data.RemoveSeries("A", "C")
if len(data.Names()) != 0 {
t.Fatalf("Expected 0 columns, got %d", len(data.Names()))
}
if data.Len() != 0 {
t.Fatalf("Expected 0 rows, got %d", data.Len())
}
}
func TestDOHLCVDataFrame(t *testing.T) {
data := NewDOHLCVDataFrame() data := NewDOHLCVDataFrame()
if !data.ContainsDOHLCV() { if !data.ContainsDOHLCV() {
t.Fatalf("Expected data to contain DOHLCV columns") t.Fatalf("Expected data to contain DOHLCV columns")

293
series.go
View File

@ -3,9 +3,9 @@ package autotrader
import ( import (
"fmt" "fmt"
"math" "math"
"sort"
"time" "time"
df "github.com/rocketlaunchr/dataframe-go"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -14,9 +14,7 @@ type Series interface {
// Reading data. // 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. // Copy returns a new Series with a copy of the original data and Series name. start is an EasyIndex and count is the number of items to copy from start onward. If count 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: // Examples:
// //
@ -25,35 +23,36 @@ type Series interface {
// Copy(-10, -1) - copy the last 10 items // 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. // All signals are disconnected from the copy. The copy has its value function reset to its own Value.
Copy(start, len int) Series Copy(start, count int) Series
Len() int Len() int
Name() string // Name returns the immutable name of the Series. Name() string // Name returns the immutable name of the Series.
Float(i int) float64 Float(i int) float64
Int(i int) int64 Int(i int) int
Str(i int) string Str(i int) string
Time(i int) time.Time Time(i int) time.Time
Value(i int) interface{} Value(i int) any
ValueRange(start, end int) []interface{} ValueRange(start, end int) []any
Values() []interface{} // Values is the same as ValueRange(0, -1). Values() []any // Values is the same as ValueRange(0, -1).
// Writing data. // Writing data.
Reverse() Series
SetName(name string) Series SetName(name string) Series
SetValue(i int, val interface{}) Series SetValue(i int, val any) Series
Push(val interface{}) Series Push(val any) Series
// Functional. // 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. Filter(f func(i int, val any) bool) Series // Where returns a new Series with only the values that return true for the given function.
Map(f func(i int, val interface{}) interface{}) Series // Map returns a new Series with the values modified by the given function. Map(f func(i int, val any) any) Series // Map returns a new Series with the values modified by the given function.
MapReverse(f func(i int, val interface{}) interface{}) Series // MapReverse is the same as Map but it starts from the last item and works backwards. MapReverse(f func(i int, val any) any) Series // MapReverse is the same as Map but it starts from the last item and works backwards.
// Statistical functions. // Statistical functions.
Rolling(period int) *RollingSeries 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 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 WithValueFunc(value func(i int) any) Series
} }
var _ Series = (*AppliedSeries)(nil) // Compile-time interface check. var _ Series = (*AppliedSeries)(nil) // Compile-time interface check.
@ -61,23 +60,23 @@ 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. // AppliedSeries is like Series, but it applies a function to each row of data before returning it.
type AppliedSeries struct { type AppliedSeries struct {
Series Series
apply func(s *AppliedSeries, i int, val interface{}) interface{} apply func(s *AppliedSeries, i int, val any) any
} }
func NewAppliedSeries(s Series, apply func(s *AppliedSeries, i int, val interface{}) interface{}) *AppliedSeries { func NewAppliedSeries(s Series, apply func(s *AppliedSeries, i int, val any) any) *AppliedSeries {
appliedSeries := &AppliedSeries{apply: apply} appliedSeries := &AppliedSeries{apply: apply}
appliedSeries.Series = s.WithValueFunc(appliedSeries.Value) appliedSeries.Series = s.WithValueFunc(appliedSeries.Value)
return appliedSeries return appliedSeries
} }
func (s *AppliedSeries) Copy(start, len int) Series { func (s *AppliedSeries) Copy(start, count int) Series {
return NewAppliedSeries(s.Series.Copy(start, len), s.apply) return NewAppliedSeries(s.Series.Copy(start, count), s.apply)
} }
// Value returns the value of the underlying Series item after applying the function. // Value returns the value of the underlying Series item after applying the function.
// //
// See also: ValueUnapplied() // See also: ValueUnapplied()
func (s *AppliedSeries) Value(i int) interface{} { func (s *AppliedSeries) Value(i int) any {
return s.apply(s, EasyIndex(i, s.Series.Len()), s.Series.Value(i)) return s.apply(s, EasyIndex(i, s.Series.Len()), s.Series.Value(i))
} }
@ -86,10 +85,14 @@ func (s *AppliedSeries) Value(i int) interface{} {
// This is equivalent to: // This is equivalent to:
// //
// s.Series.Value(i) // s.Series.Value(i)
func (s *AppliedSeries) ValueUnapplied(i int) interface{} { func (s *AppliedSeries) ValueUnapplied(i int) any {
return s.Series.Value(i) return s.Series.Value(i)
} }
func (s *AppliedSeries) Reverse() Series {
return NewAppliedSeries(s.Series.Reverse(), s.apply)
}
// SetValue sets the value of the underlying Series item without applying the function. // 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. // This may give unexpected results, as the function will still be applied when the value is requested.
@ -97,35 +100,35 @@ func (s *AppliedSeries) ValueUnapplied(i int) interface{} {
// For example: // For example:
// //
// series := NewSeries(1, 2, 3) // Pseudo-code. // series := NewSeries(1, 2, 3) // Pseudo-code.
// applied := NewAppliedSeries(series, func(_ *AppliedSeries, _ int, val interface{}) interface{} { // applied := NewAppliedSeries(series, func(_ *AppliedSeries, _ int, val any) any {
// return val.(int) * 2 // return val.(int) * 2
// }) // })
// applied.SetValue(0, 10) // applied.SetValue(0, 10)
// applied.Value(0) // 20 // applied.Value(0) // 20
// series.Value(0) // 1 // series.Value(0) // 1
func (s *AppliedSeries) SetValue(i int, val interface{}) Series { func (s *AppliedSeries) SetValue(i int, val any) Series {
_ = s.Series.SetValue(i, val) _ = s.Series.SetValue(i, val)
return s return s
} }
func (s *AppliedSeries) Push(val interface{}) Series { func (s *AppliedSeries) Push(val any) Series {
_ = s.Series.Push(val) _ = s.Series.Push(val)
return s return s
} }
func (s *AppliedSeries) Filter(f func(i int, val interface{}) bool) Series { func (s *AppliedSeries) Filter(f func(i int, val any) bool) Series {
return NewAppliedSeries(s.Series.Filter(f), s.apply) return NewAppliedSeries(s.Series.Filter(f), s.apply)
} }
func (s *AppliedSeries) Map(f func(i int, val interface{}) interface{}) Series { func (s *AppliedSeries) Map(f func(i int, val any) any) Series {
return NewAppliedSeries(s.Series.Map(f), s.apply) return NewAppliedSeries(s.Series.Map(f), s.apply)
} }
func (s *AppliedSeries) MapReverse(f func(i int, val interface{}) interface{}) Series { func (s *AppliedSeries) MapReverse(f func(i int, val any) any) Series {
return NewAppliedSeries(s.Series.MapReverse(f), s.apply) return NewAppliedSeries(s.Series.MapReverse(f), s.apply)
} }
func (s *AppliedSeries) WithValueFunc(value func(i int) interface{}) Series { func (s *AppliedSeries) WithValueFunc(value func(i int) any) Series {
return &AppliedSeries{Series: s.Series.WithValueFunc(value), apply: s.apply} return &AppliedSeries{Series: s.Series.WithValueFunc(value), apply: s.apply}
} }
@ -142,13 +145,13 @@ func NewRollingSeries(s Series, period int) *RollingSeries {
return series return series
} }
func (s *RollingSeries) Copy(start, len int) Series { func (s *RollingSeries) Copy(start, count int) Series {
return NewRollingSeries(s.Series.Copy(start, len), s.period) return NewRollingSeries(s.Series.Copy(start, count), 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. // Value returns []any 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{} { func (s *RollingSeries) Value(i int) any {
items := make([]interface{}, 0, s.period) items := make([]any, 0, s.period)
i = EasyIndex(i, s.Len()) i = EasyIndex(i, s.Len())
if i < 0 || i >= s.Len() { if i < 0 || i >= s.Len() {
return items return items
@ -160,25 +163,29 @@ func (s *RollingSeries) Value(i int) interface{} {
return items return items
} }
func (s *RollingSeries) SetValue(i int, val interface{}) Series { func (s *RollingSeries) Reverse() Series {
return NewRollingSeries(s.Series.Reverse(), s.period)
}
func (s *RollingSeries) SetValue(i int, val any) Series {
_ = s.Series.SetValue(i, val) _ = s.Series.SetValue(i, val)
return s return s
} }
func (s *RollingSeries) Push(val interface{}) Series { func (s *RollingSeries) Push(val any) Series {
_ = s.Series.Push(val) _ = s.Series.Push(val)
return s return s
} }
func (s *RollingSeries) Filter(f func(i int, val interface{}) bool) Series { func (s *RollingSeries) Filter(f func(i int, val any) bool) Series {
return NewRollingSeries(s.Series.Filter(f), s.period) return NewRollingSeries(s.Series.Filter(f), s.period)
} }
func (s *RollingSeries) Map(f func(i int, val interface{}) interface{}) Series { func (s *RollingSeries) Map(f func(i int, val any) any) Series {
return NewRollingSeries(s.Series.Map(f), s.period) return NewRollingSeries(s.Series.Map(f), s.period)
} }
func (s *RollingSeries) MapReverse(f func(i int, val interface{}) interface{}) Series { func (s *RollingSeries) MapReverse(f func(i int, val any) any) Series {
return NewRollingSeries(s.Series.MapReverse(f), s.period) return NewRollingSeries(s.Series.MapReverse(f), s.period)
} }
@ -188,9 +195,9 @@ func (s *RollingSeries) Average() *AppliedSeries {
} }
func (s *RollingSeries) Mean() *AppliedSeries { func (s *RollingSeries) Mean() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v interface{}) interface{} { return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v any) any {
switch v := v.(type) { switch v := v.(type) {
case []interface{}: case []any:
if len(v) == 0 { if len(v) == 0 {
return nil return nil
} }
@ -217,9 +224,9 @@ func (s *RollingSeries) Mean() *AppliedSeries {
} }
func (s *RollingSeries) EMA() *AppliedSeries { func (s *RollingSeries) EMA() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v interface{}) interface{} { return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v any) any {
switch v := v.(type) { switch v := v.(type) {
case []interface{}: case []any:
if len(v) == 0 { if len(v) == 0 {
return nil return nil
} }
@ -246,9 +253,9 @@ func (s *RollingSeries) EMA() *AppliedSeries {
} }
func (s *RollingSeries) Median() *AppliedSeries { func (s *RollingSeries) Median() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v interface{}) interface{} { return NewAppliedSeries(s, func(_ *AppliedSeries, _ int, v any) any {
switch v := v.(type) { switch v := v.(type) {
case []interface{}: case []any:
if len(v) == 0 { if len(v) == 0 {
return nil return nil
} }
@ -257,7 +264,7 @@ func (s *RollingSeries) Median() *AppliedSeries {
if len(v) == 0 { if len(v) == 0 {
return float64(0) return float64(0)
} }
slices.SortFunc(v, func(a, b interface{}) bool { slices.SortFunc(v, func(a, b any) bool {
x, y := a.(float64), b.(float64) x, y := a.(float64), b.(float64)
return x < y || (math.IsNaN(x) && !math.IsNaN(y)) return x < y || (math.IsNaN(x) && !math.IsNaN(y))
}) })
@ -269,7 +276,7 @@ func (s *RollingSeries) Median() *AppliedSeries {
if len(v) == 0 { if len(v) == 0 {
return int64(0) return int64(0)
} }
slices.SortFunc(v, func(a, b interface{}) bool { slices.SortFunc(v, func(a, b any) bool {
x, y := a.(int64), b.(int64) x, y := a.(int64), b.(int64)
return x < y return x < y
}) })
@ -287,9 +294,9 @@ func (s *RollingSeries) Median() *AppliedSeries {
} }
func (s *RollingSeries) StdDev() *AppliedSeries { func (s *RollingSeries) StdDev() *AppliedSeries {
return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v interface{}) interface{} { return NewAppliedSeries(s, func(_ *AppliedSeries, i int, v any) any {
switch v := v.(type) { switch v := v.(type) {
case []interface{}: case []any:
if len(v) == 0 { if len(v) == 0 {
return nil return nil
} }
@ -317,7 +324,7 @@ func (s *RollingSeries) StdDev() *AppliedSeries {
}) })
} }
func (s *RollingSeries) WithValueFunc(value func(i int) interface{}) Series { func (s *RollingSeries) WithValueFunc(value func(i int) any) Series {
return &RollingSeries{Series: s.Series.WithValueFunc(value), period: s.period} return &RollingSeries{Series: s.Series.WithValueFunc(value), period: s.period}
} }
@ -326,24 +333,25 @@ func (s *RollingSeries) WithValueFunc(value func(i int) interface{}) Series {
// Signals: // Signals:
// - LengthChanged(int) - when the data is appended or an item is removed. // - LengthChanged(int) - when the data is appended or an item is removed.
// - NameChanged(string) - when the name is changed. // - NameChanged(string) - when the name is changed.
// - ValueChanged(int, any) - when a value is changed.
type DataSeries struct { type DataSeries struct {
SignalManager SignalManager
data df.Series name string
value func(i int) interface{} data []any
value func(i int) any
} }
func NewDataSeries(data df.Series) *DataSeries { func NewDataSeries(name string, vals ...any) *DataSeries {
dataSeries := &DataSeries{ dataSeries := &DataSeries{
SignalManager: SignalManager{}, SignalManager: SignalManager{},
data: data, name: name,
data: vals,
} }
dataSeries.value = dataSeries.Value dataSeries.value = dataSeries.Value
return dataSeries 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. // Copy returns a new DataSeries with a copy of the original data and Series name. start is an EasyIndex and count is the number of items to copy from start onward. If count 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: // Examples:
// //
@ -352,132 +360,130 @@ func NewDataSeries(data df.Series) *DataSeries {
// Copy(-10, -1) - copy the last 10 items // 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. // 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 { func (s *DataSeries) Copy(start, count int) Series {
if s.Len() == 0 {
return NewDataSeries(s.name)
}
start = EasyIndex(start, s.Len()) start = EasyIndex(start, s.Len())
var _end *int var end int
if start < 0 || start >= s.Len() { start = Max(Min(start, s.Len()), 0)
return nil if count < 0 {
} else if len >= 0 { end = s.Len()
end := start + len } else {
if end < s.Len() { end = Min(start+count, s.Len())
if end < start {
copy := s.data.Copy()
copy.Reset()
series := &DataSeries{SignalManager{}, copy, nil}
series.value = series.Value
return series
} }
_end = &end if end <= start {
} return NewDataSeries(s.name) // Return an empty series.
}
return &DataSeries{
SignalManager: SignalManager{},
data: s.data.Copy(df.Range{Start: &start, End: _end}),
value: s.value,
} }
data := make([]any, end-start)
copy(data, s.data[start:end])
return NewDataSeries(s.name, data...)
} }
func (s *DataSeries) Name() string { func (s *DataSeries) Name() string {
return s.data.Name() return s.name
} }
func (s *DataSeries) SetName(name string) Series { func (s *DataSeries) SetName(name string) Series {
if name == s.Name() { if name == s.name {
return s return s
} }
s.data.Rename(name) s.name = name
s.SignalEmit("NameChanged", name) s.SignalEmit("NameChanged", name)
return s return s
} }
func (s *DataSeries) Len() int { func (s *DataSeries) Len() int {
if s.data == nil { return len(s.data)
return 0
}
return s.data.NRows()
} }
func (s *DataSeries) Push(value interface{}) Series { func (s *DataSeries) Reverse() Series {
if s.data != nil { if len(s.data) != 0 {
s.data.Append(value) sort.Slice(s.data, func(i, j int) bool {
return i > j
})
for i, v := range s.data {
s.SignalEmit("ValueChanged", i, v)
}
}
return s
}
func (s *DataSeries) Push(value any) Series {
s.data = append(s.data, value)
s.SignalEmit("LengthChanged", s.Len()) s.SignalEmit("LengthChanged", s.Len())
return s
}
func (s *DataSeries) SetValue(i int, val any) Series {
if i = EasyIndex(i, s.Len()); i < s.Len() && i >= 0 {
s.data[i] = val
s.SignalEmit("ValueChanged", i, val)
} }
return s return s
} }
func (s *DataSeries) SetValue(i int, val interface{}) Series { func (s *DataSeries) Value(i int) any {
if s.data != nil { i = EasyIndex(i, s.Len())
s.data.Update(EasyIndex(i, s.Len()), val) if i >= s.Len() || i < 0 {
}
return s
}
func (s *DataSeries) Value(i int) interface{} {
if s.data == nil {
return nil return nil
} }
i = EasyIndex(i, s.Len()) // Allow for negative indexing. return s.data[i]
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. // ValueRange returns a copy of values from start to start+count. If count is negative then all items from start to the end of the series are returned. If there are not enough items to return then the maximum amount is returned. If there are no items to return then an empty slice is returned.
func (s *DataSeries) ValueRange(start, end int) []interface{} { func (s *DataSeries) ValueRange(start, count int) []any {
if s.data == nil {
return nil
}
start = EasyIndex(start, s.Len()) start = EasyIndex(start, s.Len())
if end < 0 { start = Max(Min(start, s.Len()), 0)
end = s.Len() - 1 if count < 0 {
count = s.Len() - start
} else {
count = Min(count, s.Len()-start)
} }
if start < 0 || start >= s.Len() || end >= s.Len() || start > end { if count <= 0 {
return nil return []any{}
} }
items := make([]interface{}, end-start+1) end := start + count
for i := start; i <= end; i++ { items := make([]any, count)
items[i-start] = s.value(i) copy(items, s.data[start:end])
}
return items return items
} }
func (s *DataSeries) Values() []interface{} { // Values returns a copy of all values. If there are no values, an empty slice is returned.
if s.data == nil { //
return nil // Same as:
} //
// ValueRange(0, -1)
func (s *DataSeries) Values() []any {
return s.ValueRange(0, -1) return s.ValueRange(0, -1)
} }
// Float returns the value at index i as a float64. If the value is not a float64 then NaN is returned.
func (s *DataSeries) Float(i int) float64 { func (s *DataSeries) Float(i int) float64 {
val := s.value(i) val := s.value(i)
if val == nil {
return 0
}
switch val := val.(type) { switch val := val.(type) {
case float64: case float64:
return val return val
default: default:
return 0 return math.NaN()
} }
} }
func (s *DataSeries) Int(i int) int64 { // Int returns the value at index i as an int64. If the value is not an int64 then 0 is returned.
func (s *DataSeries) Int(i int) int {
val := s.value(i) val := s.value(i)
if val == nil {
return 0
}
switch val := val.(type) { switch val := val.(type) {
case int64: case int:
return val return val
default: default:
return 0 return 0
} }
} }
// Str returns the value at index i as a string. If the value is not a string then "" is returned.
func (s *DataSeries) Str(i int) string { func (s *DataSeries) Str(i int) string {
val := s.value(i) val := s.value(i)
if val == nil {
return ""
}
switch val := val.(type) { switch val := val.(type) {
case string: case string:
return val return val
@ -486,11 +492,9 @@ func (s *DataSeries) Str(i int) string {
} }
} }
// Time returns the value at index i as a time.Time. If the value is not a time.Time then time.Time{} is returned.
func (s *DataSeries) Time(i int) time.Time { func (s *DataSeries) Time(i int) time.Time {
val := s.value(i) val := s.value(i)
if val == nil {
return time.Time{}
}
switch val := val.(type) { switch val := val.(type) {
case time.Time: case time.Time:
return val return val
@ -499,37 +503,30 @@ func (s *DataSeries) Time(i int) time.Time {
} }
} }
func (s *DataSeries) Filter(f func(i int, val interface{}) bool) Series { func (s *DataSeries) Filter(f func(i int, val any) bool) Series {
if s.data == nil { series := NewDataSeries(s.name, make([]any, 0, s.Len())...)
return nil
}
series := &DataSeries{SignalManager{}, df.NewSeriesGeneric(s.data.Name(), (interface{})(nil), nil), s.value}
for i := 0; i < s.Len(); i++ { for i := 0; i < s.Len(); i++ {
if val := series.value(i); f(i, val) { if val := s.value(i); f(i, val) {
series.Push(val) series.Push(val)
} }
} }
return series return series
} }
func (s *DataSeries) Map(f func(i int, val interface{}) interface{}) Series { // Map returns a new series with the same length as the original series. The value at each index is replaced by the value returned by the function f.
if s.data == nil { func (s *DataSeries) Map(f func(i int, val any) any) Series {
return nil series := s.Copy(0, -1)
}
series := &DataSeries{SignalManager{}, s.data.Copy(), s.value}
for i := 0; i < s.Len(); i++ { for i := 0; i < s.Len(); i++ {
series.SetValue(i, f(i, series.value(i))) series.SetValue(i, f(i, series.Value(i)))
} }
return series return series
} }
func (s *DataSeries) MapReverse(f func(i int, val interface{}) interface{}) Series { // MapReverse returns a new series with the same length as the original series. The value at each index is replaced by the value returned by the function f. The values are processed in reverse order.
if s.data == nil { func (s *DataSeries) MapReverse(f func(i int, val any) any) Series {
return nil series := s.Copy(0, -1)
}
series := &DataSeries{SignalManager{}, s.data.Copy(), s.value}
for i := s.Len() - 1; i >= 0; i-- { for i := s.Len() - 1; i >= 0; i-- {
series.SetValue(i, f(i, series.value(i))) series.SetValue(i, f(i, series.Value(i)))
} }
return series return series
} }
@ -538,7 +535,7 @@ func (s *DataSeries) Rolling(period int) *RollingSeries {
return NewRollingSeries(s, period) return NewRollingSeries(s, period)
} }
func (s *DataSeries) WithValueFunc(value func(i int) interface{}) Series { func (s *DataSeries) WithValueFunc(value func(i int) any) Series {
copy := s.Copy(0, -1).(*DataSeries) copy := s.Copy(0, -1).(*DataSeries)
copy.value = value copy.value = value
return copy return copy

View File

@ -1,14 +1,128 @@
package autotrader package autotrader
import ( import (
"math"
"testing" "testing"
"github.com/rocketlaunchr/dataframe-go"
) )
func TestDataSeries(t *testing.T) {
series := NewDataSeries("test", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
if series.Len() != 10 {
t.Fatalf("Expected 10 rows, got %d", series.Len())
}
series.Reverse()
if series.Len() != 10 {
t.Fatalf("Expected 10 rows, got %d", series.Len())
}
for i := 0; i < 10; i++ {
if val := series.Float(i); val != float64(10-i) {
t.Errorf("(%d)\tExpected %f, got %v", i, float64(10-i), val)
}
}
last5 := series.Copy(-5, -1)
if last5.Len() != 5 {
t.Fatalf("Expected 5 rows, got %d", last5.Len())
}
for i := 0; i < 5; i++ {
if val := last5.Float(i); val != float64(5-i) {
t.Errorf("(%d)\tExpected %f, got %v", i, float64(5-i), val)
}
}
last5.SetValue(-1, 0.0)
if series.Float(-1) == 0.0 {
t.Errorf("Expected data to be copied, not referenced")
}
outOfBounds := series.Copy(10, -1)
if outOfBounds == nil {
t.Fatal("Expected non-nil series, got nil")
}
if outOfBounds.Len() != 0 {
t.Fatalf("Expected 0 rows, got %d", outOfBounds.Len())
}
valueRange := series.ValueRange(-1, 0) // Out of bounds should result in an empty slice.
if valueRange == nil || len(valueRange) != 0 {
t.Fatalf("Expected a slice with 0 items, got %d", len(valueRange))
}
valueRange = series.ValueRange(0, 5) // Take the first 5 items.
if len(valueRange) != 5 {
t.Fatalf("Expected a slice with 5 items, got %d", len(valueRange))
}
for i := 0; i < 5; i++ {
if val := valueRange[i]; val != float64(10-i) {
t.Errorf("(%d)\tExpected %f, got %v", i, float64(10-i), val)
}
}
values := series.Values()
if len(values) != 10 {
t.Fatalf("Expected a slice with 10 items, got %d", len(values))
}
for i := 0; i < 10; i++ {
if val := values[i]; val != float64(10-i) {
t.Errorf("(%d)\tExpected %f, got %v", i, float64(10-i), val)
}
}
}
func TestDataSeriesFunctional(t *testing.T) {
series := NewDataSeries("test", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
doubled := series.Map(func(_ int, val any) any {
return val.(float64) * 2
})
if doubled.Len() != 10 {
t.Fatalf("Expected 10 rows, got %d", doubled.Len())
}
for i := 0; i < 10; i++ {
if val := doubled.Float(i); val != float64(i+1)*2 {
t.Errorf("(%d)\tExpected %f, got %v", i, float64(i+1)*2, val)
}
}
series.SetValue(0, 100.0)
if doubled.Float(0) == 100.0 {
t.Error("Expected data to be copied, not referenced")
}
series.SetValue(0, 1.0) // Reset the value.
evens := series.Filter(func(_ int, val any) bool {
return EqualApprox(math.Mod(val.(float64), 2), 0)
})
if evens.Len() != 5 {
t.Fatalf("Expected 5 rows, got %d", evens.Len())
}
for i := 0; i < 5; i++ {
if val := evens.Float(i); val != float64(i+1)*2 {
t.Errorf("(%d)\tExpected %f, got %v", i, float64(i+1)*2, val)
}
}
if series.Len() != 10 {
t.Fatalf("Expected series to still have 10 rows, got %d", series.Len())
}
diffed := series.MapReverse(func(i int, v any) any {
if i == 0 {
return 0.0
}
return v.(float64) - series.Float(i-1)
})
if diffed.Len() != 10 {
t.Fatalf("Expected 10 rows, got %d", diffed.Len())
}
if diffed.Float(0) != 0.0 {
t.Errorf("Expected first value to be 0.0, got %v", diffed.Float(0))
}
for i := 1; i < 10; i++ {
if val := diffed.Float(i); val != 1.0 {
t.Errorf("(%d)\tExpected 1.0, got %v", i, val)
}
}
}
func TestAppliedSeries(t *testing.T) { func TestAppliedSeries(t *testing.T) {
underlying := NewDataSeries(dataframe.NewSeriesFloat64("test", nil, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) underlying := NewDataSeries("test", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
applied := NewAppliedSeries(underlying, func(_ *AppliedSeries, _ int, val interface{}) interface{} { applied := NewAppliedSeries(underlying, func(_ *AppliedSeries, _ int, val any) any {
return val.(float64) * 2 return val.(float64) * 2
}) })
@ -32,7 +146,7 @@ func TestAppliedSeries(t *testing.T) {
} }
// Test that the underlying series is not modified when the applied series is modified. // Test that the underlying series is not modified when the applied series is modified.
applied.SetValue(0, 100) applied.SetValue(0, 100.0)
if underlying.Float(0) != 1 { if underlying.Float(0) != 1 {
t.Errorf("Expected 1, got %v", underlying.Float(0)) t.Errorf("Expected 1, got %v", underlying.Float(0))
} }
@ -43,7 +157,7 @@ func TestAppliedSeries(t *testing.T) {
func TestRollingAppliedSeries(t *testing.T) { func TestRollingAppliedSeries(t *testing.T) {
// Test rolling average. // Test rolling average.
series := NewDataSeries(dataframe.NewSeriesFloat64("test", nil, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) series := NewDataSeries("test", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
sma5Expected := []float64{1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8} 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. sma5 := (Series)(series.Rolling(5).Average()) // Take the 5 period moving average and cast it to Series.
@ -71,7 +185,7 @@ func TestRollingAppliedSeries(t *testing.T) {
} }
} }
func TestDataSeries(t *testing.T) { func TestDataSeriesEURUSD(t *testing.T) {
data, err := EURUSD() data, err := EURUSD()
if err != nil { if err != nil {
t.Fatalf("Expected no error, got %s", err) t.Fatalf("Expected no error, got %s", err)
@ -90,7 +204,7 @@ func TestDataSeries(t *testing.T) {
if sma10.Len() != 2610 { if sma10.Len() != 2610 {
t.Fatalf("Expected 2610 rows, got %d", sma10.Len()) t.Fatalf("Expected 2610 rows, got %d", sma10.Len())
} }
if sma10.Value(-1) != 1.10039 { // Latest closing price averaged over 10 periods. if !EqualApprox(sma10.Value(-1).(float64), 1.15878) { // Latest closing price averaged over 10 periods.
t.Fatalf("Expected 1.10039, got %f", sma10.Value(-1)) t.Fatalf("Expected 1.10039, got %f", sma10.Value(-1))
} }
} }

View File

@ -2,43 +2,71 @@ package autotrader
import "reflect" import "reflect"
// Signaler is an interface for objects that can emit signals which fire event handlers. This is used to implement event-driven programming. Embed a pointer to a SignalManager in your struct to have signals entirely for free.
//
// Example:
//
// type MyStruct struct {
// *SignalManager // Now MyStruct has SignalConnect, SignalEmit, etc.
// }
//
// When your type emits signals, they should be listed somewhere in the documentation. For example:
//
// // Signals:
// // - MySignal() - Emitted when...
// // - ThingChanged(newThing *Thing) - Emitted when a thing changes.
// type MyStruct struct { ... }
type Signaler interface { type Signaler interface {
SignalConnect(signal string, handler func(...interface{}), bindings ...interface{}) error // SignalConnect connects the handler to the signal. SignalConnect(signal string, identity any, handler func(...any), bindings ...any) error // SignalConnect connects the handler to the signal under identity.
SignalConnected(signal string, handler func(...interface{})) bool // SignalConnected returns true if the handler is connected to the signal. SignalConnected(signal string, identity any, handler func(...any)) bool // SignalConnected returns true if the handler under the identity is connected to the signal.
SignalConnections(signal string) []SignalHandler // SignalConnections returns a slice of handlers connected to the signal. SignalConnections(signal string) []SignalHandler // SignalConnections returns a slice of handlers connected to the signal.
SignalDisconnect(signal string, handler func(...interface{})) // SignalDisconnect removes the handler from the signal. SignalDisconnect(signal string, identity any, handler func(...any)) // SignalDisconnect removes the handler under identity from the signal.
SignalEmit(signal string, data ...interface{}) // SignalEmit emits the signal with the data. SignalEmit(signal string, data ...any) // SignalEmit emits the signal with the data.
} }
// SignalHandler wraps a signal handler.
type SignalHandler struct { type SignalHandler struct {
Callback func(...interface{}) Identity any // Identity is used to identify functions implemented on the same type. It is typically a pointer to an object that owns the callback function, but it can be a string or any other type.
Bindings []interface{} Callback func(...any) // Callback is the function that is called when the signal is emitted.
Bindings []any // Bindings are arguments that are passed to the callback function when the signal is emitted. These are typically used to pass context.
} }
// SignalManager is a struct that implements the Signaler interface. Embed this into your struct to have signals entirely for free. Emitting a signal will call all handlers connected to the signal, but if no handlers are connected then it is a no-op. This means signals are very cheap and only come at a cost when they're actually used.
type SignalManager struct { type SignalManager struct {
signalConnections map[string][]SignalHandler signalConnections map[string][]SignalHandler
} }
func (s *SignalManager) SignalConnect(signal string, callback func(...interface{}), bindings ...interface{}) error { // SignalConnect connects a callback function to the signal. The callback function will be called when the signal is emitted. The identity is used to identify functions implemented on the same type. It is typically a pointer to an object that owns the callback function, but it can be a string or any other type. Bindings are arguments that are passed to the callback function when the signal is emitted. These are typically used to pass context.
func (s *SignalManager) SignalConnect(signal string, identity any, callback func(...any), bindings ...any) error {
if s.signalConnections == nil { if s.signalConnections == nil {
s.signalConnections = make(map[string][]SignalHandler) s.signalConnections = make(map[string][]SignalHandler)
} }
s.signalConnections[signal] = append(s.signalConnections[signal], SignalHandler{callback, bindings}) // Check if the callback and identity is already connected to the signal.
if connections, ok := s.signalConnections[signal]; ok {
for _, h := range connections {
if h.Identity == identity && reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() {
return nil
}
}
}
s.signalConnections[signal] = append(s.signalConnections[signal], SignalHandler{identity, callback, bindings})
return nil return nil
} }
func (s *SignalManager) SignalConnected(signal string, callback func(...interface{})) bool { // SignalConnected returns true if the callback function under the identity is connected to the signal.
func (s *SignalManager) SignalConnected(signal string, identity any, callback func(...any)) bool {
if s.signalConnections == nil { if s.signalConnections == nil {
return false return false
} }
for _, h := range s.signalConnections[signal] { for _, h := range s.signalConnections[signal] {
if reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() { if h.Identity == identity && reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() {
return true return true
} }
} }
return false return false
} }
// SignalConnections returns a slice of handlers connected to the signal.
func (s *SignalManager) SignalConnections(signal string) []SignalHandler { func (s *SignalManager) SignalConnections(signal string) []SignalHandler {
if s.signalConnections == nil { if s.signalConnections == nil {
return nil return nil
@ -46,23 +74,27 @@ func (s *SignalManager) SignalConnections(signal string) []SignalHandler {
return s.signalConnections[signal] return s.signalConnections[signal]
} }
func (s *SignalManager) SignalDisconnect(signal string, callback func(...interface{})) { // SignalDisconnect removes the equivalent callback function under the identity from the signal.
func (s *SignalManager) SignalDisconnect(signal string, identity any, callback func(...any)) {
if s.signalConnections == nil { if s.signalConnections == nil {
return return
} }
for i, h := range s.signalConnections[signal] { connections := s.signalConnections[signal]
if reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() { for i, h := range connections {
s.signalConnections[signal] = append(s.signalConnections[signal][:i], s.signalConnections[signal][i+1:]...) if h.Identity == identity && reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() {
s.signalConnections[signal] = append(connections[:i], connections[i+1:]...)
break
} }
} }
} }
func (s *SignalManager) SignalEmit(signal string, data ...interface{}) { // SignalEmit calls all handlers connected to the signal with the data. If no handlers are connected then it is a no-op.
func (s *SignalManager) SignalEmit(signal string, data ...any) {
if s.signalConnections == nil { if s.signalConnections == nil {
return return
} }
for _, handler := range s.signalConnections[signal] { for _, handler := range s.signalConnections[signal] {
args := make([]interface{}, len(data)+len(handler.Bindings)) args := make([]any, len(data)+len(handler.Bindings))
copy(args, data) copy(args, data)
copy(args[len(data):], handler.Bindings) copy(args[len(data):], handler.Bindings)
handler.Callback(args...) handler.Callback(args...)

View File

@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/go-co-op/gocron" "github.com/go-co-op/gocron"
"github.com/rocketlaunchr/dataframe-go"
) )
// Performance (financial) reporting and statistics. // Performance (financial) reporting and statistics.
@ -86,13 +85,13 @@ func (t *Trader) Run() {
func (t *Trader) Init() { func (t *Trader) Init() {
t.Strategy.Init(t) t.Strategy.Init(t)
t.stats.Dated = NewDataFrame( t.stats.Dated = NewDataFrame(
NewDataSeries(dataframe.NewSeriesTime("Date", nil)), NewDataSeries("Date"),
NewDataSeries(dataframe.NewSeriesFloat64("Equity", nil)), NewDataSeries("Equity"),
NewDataSeries(dataframe.NewSeriesFloat64("Profit", nil)), NewDataSeries("Profit"),
NewDataSeries(dataframe.NewSeriesFloat64("Drawdown", nil)), NewDataSeries("Drawdown"),
NewDataSeries(dataframe.NewSeriesFloat64("Returns", nil)), NewDataSeries("Returns"),
) )
t.Broker.SignalConnect("PositionClosed", func(args ...interface{}) { t.Broker.SignalConnect("PositionClosed", t, func(args ...interface{}) {
position := args[0].(Position) position := args[0].(Position)
t.stats.returnsThisCandle += position.PL() t.stats.returnsThisCandle += position.PL()
}) })