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) {
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 count >= b.Data.Len() { // We are asking for more data than we have.
return b.Data.Copy(0, -1).(*DataFrame), ErrEOF
} else {
return b.Data.Copy(start, -1).(*DataFrame), ErrEOF
}
return b.Data.Copy(-count, -1).(*DataFrame), ErrEOF // Return the last count candles.
} 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, Max(count, 1000))
candles, err := b.DataBroker.Candles(symbol, frequency, count)
if err != nil {
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.
return nil, ErrNoData
}
// 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
return b.Data.Copy(start, adjCount).(*DataFrame), nil
}
func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) {

110
data.go
View File

@ -2,13 +2,10 @@ package autotrader
import (
"encoding/csv"
"errors"
"io"
"os"
"strconv"
"time"
df "github.com/rocketlaunchr/dataframe-go"
)
type DataCSVLayout struct {
@ -73,102 +70,51 @@ func DataFrameFromCSVReaderLayout(r io.Reader, layout DataCSVLayout) (*DataFrame
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
}
func DataFrameFromCSVReader(r io.Reader, dateLayout string, readReversed bool) (*DataFrame, error) {
csv := csv.NewReader(r)
csv.LazyQuotes = true
records, err := csv.ReadAll()
if err != nil {
return nil, err
}
if len(records) < 2 {
return nil, errors.New("csv file must have at least 2 rows")
}
seriesSlice := make([]Series, 0, 12)
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)
// Read the CSV file.
for {
rec, err := csv.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
// 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]
// Create the columns needed.
if len(seriesSlice) == 0 {
for _, val := range rec {
seriesSlice = append(seriesSlice, NewDataSeries(val))
}
continue
}
// Add rows to the series.
for j, val := range rec {
series := dfSeriesSlice[j]
switch series.Type() {
case "float64":
val, err := strconv.ParseFloat(val, 64)
if err != nil {
series.Append(nil)
} else {
series.Append(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)
series := seriesSlice[j]
if f, err := strconv.ParseFloat(val, 64); err == nil {
series.Push(f)
} else if t, err := time.Parse(dateLayout, val); err == nil {
series.Push(t)
} else {
series.Push(val)
}
dfSeriesSlice[j] = series
}
}
// NOTE: we specifically construct the DataFrame at the end of the function because it likes to set
// state like number of rows and columns at initialization and won't let you change it later.
seriesSlice := make([]Series, len(dfSeriesSlice))
for i, series := range dfSeriesSlice {
seriesSlice[i] = NewDataSeries(series)
// Reverse the series if needed.
if readReversed {
for _, series := range seriesSlice {
series.Reverse()
}
}
return NewDataFrame(seriesSlice...), nil
}

165
frame.go
View File

@ -3,44 +3,42 @@ package autotrader
import (
"bytes"
"fmt"
"math"
"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.
// 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.
//
// 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
Copy(start, count 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{}
Value(column string, i int) any
Float(column string, i int) float64
Int(column string, i int) int64
Int(column string, i int) int
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)
PushValues(values map[string]any) error
RemoveSeries(names ...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.
@ -49,7 +47,7 @@ type Frame interface {
High(i int) float64
Low(i int) float64
Close(i int) float64
Volume(i int) float64
Volume(i int) int
Dates() Series
Opens() 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.
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)),
NewDataSeries("Date"),
NewDataSeries("Open"),
NewDataSeries("High"),
NewDataSeries("Low"),
NewDataSeries("Close"),
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.
//
// If start is out of bounds then nil is returned.
// 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.
//
// 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 {
func (d *DataFrame) Copy(start, count int) Frame {
out := &DataFrame{}
for _, v := range d.series {
newSeries := v.Copy(start, end)
out.PushSeries(newSeries)
for _, s := range d.series {
out.PushSeries(s.Copy(start, count))
}
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.
// 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.
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 {
if v > length {
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.
// 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))
if s := d.Series(name); s != nil {
out.PushSeries(s)
}
}
return out
}
@ -192,40 +185,40 @@ func (d *DataFrame) String() 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).
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.
// 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).
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.
// 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).
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.
// 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).
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.
// 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).
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.
// 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).
func (d *DataFrame) Volume(i int) float64 {
return d.Float("Volume", i)
func (d *DataFrame) Volume(i int) int {
return d.Int("Volume", i)
}
// Dates returns a Series of all the dates in the DataFrame.
@ -258,6 +251,7 @@ func (d *DataFrame) Volumes() Series {
return d.Series("Volume")
}
// Contains returns true if the DataFrame contains all the given series names.
func (d *DataFrame) Contains(names ...string) bool {
for _, name := range names {
if _, ok := d.series[name]; !ok {
@ -267,22 +261,13 @@ func (d *DataFrame) Contains(names ...string) bool {
return true
}
// ContainsDOHLCV returns true if the DataFrame contains the series "Date", "Open", "High", "Low", "Close", and "Volume".
func (d *DataFrame) ContainsDOHLCV() bool {
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 {
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")
}
@ -295,9 +280,10 @@ func (d *DataFrame) PushCandle(date time.Time, open, high, low, close float64, v
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 {
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 {
if _, ok := d.series[name]; !ok {
@ -308,6 +294,7 @@ func (d *DataFrame) PushValues(values map[string]interface{}) error {
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 {
if d.series == nil {
d.series = make(map[string]Series, len(series))
@ -316,8 +303,11 @@ func (d *DataFrame) PushSeries(series ...Series) error {
for _, s := range series {
name := s.Name()
s.SignalConnect("LengthChanged", d.onSeriesLengthChanged, name)
s.SignalConnect("NameChanged", d.onSeriesNameChanged, name)
if _, ok := d.series[name]; ok {
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.rowCounts[name] = s.Len()
}
@ -325,18 +315,21 @@ func (d *DataFrame) PushSeries(series ...Series) error {
return nil
}
func (d *DataFrame) RemoveSeries(name string) {
s, ok := d.series[name]
if !ok {
return
// 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]
if !ok {
return
}
s.SignalDisconnect("LengthChanged", d, d.onSeriesLengthChanged)
s.SignalDisconnect("NameChanged", d, d.onSeriesNameChanged)
delete(d.series, name)
delete(d.rowCounts, name)
}
s.SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
s.SignalDisconnect("NameChanged", d.onSeriesNameChanged)
delete(d.series, name)
delete(d.rowCounts, name)
}
func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) {
func (d *DataFrame) onSeriesLengthChanged(args ...any) {
if len(args) != 2 {
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
}
@ -345,7 +338,7 @@ func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) {
d.rowCounts[name] = newLen
}
func (d *DataFrame) onSeriesNameChanged(args ...interface{}) {
func (d *DataFrame) onSeriesNameChanged(args ...any) {
if len(args) != 2 {
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
}
@ -358,12 +351,13 @@ func (d *DataFrame) onSeriesNameChanged(args ...interface{}) {
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)
d.series[newName].SignalDisconnect("LengthChanged", d, d.onSeriesLengthChanged)
d.series[newName].SignalDisconnect("NameChanged", d, d.onSeriesNameChanged)
d.series[newName].SignalConnect("LengthChanged", d, d.onSeriesLengthChanged, 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 {
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.
func (d *DataFrame) Value(column string, i int) interface{} {
func (d *DataFrame) Value(column string, i int) any {
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
if s, ok := d.series[column]; ok {
return s.Value(i)
}
return d.series[column].Value(i)
return nil
}
// 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 {
val := d.Value(column, i)
if val == nil {
return 0
}
switch val := val.(type) {
case float64:
return val
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.
func (d *DataFrame) Int(column string, i int) int64 {
// 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) int {
val := d.Value(column, i)
if val == nil {
return 0
}
switch val := val.(type) {
case int64:
case int:
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.
// 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 {
val := d.Value(column, i)
if val == nil {
return ""
}
switch val := val.(type) {
case string:
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 {
val := d.Value(column, i)
if val == nil {
return time.Time{}
}
switch val := val.(type) {
case time.Time:
return val

View File

@ -5,7 +5,70 @@ import (
"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()
if !data.ContainsDOHLCV() {
t.Fatalf("Expected data to contain DOHLCV columns")

287
series.go
View File

@ -3,9 +3,9 @@ package autotrader
import (
"fmt"
"math"
"sort"
"time"
df "github.com/rocketlaunchr/dataframe-go"
"golang.org/x/exp/slices"
)
@ -14,9 +14,7 @@ type Series interface {
// 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.
// 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.
//
// Examples:
//
@ -25,35 +23,36 @@ type Series interface {
// 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
Copy(start, count int) Series
Len() int
Name() string // Name returns the immutable name of the Series.
Float(i int) float64
Int(i int) int64
Int(i int) int
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).
Value(i int) any
ValueRange(start, end int) []any
Values() []any // Values is the same as ValueRange(0, -1).
// Writing data.
Reverse() Series
SetName(name string) Series
SetValue(i int, val interface{}) Series
Push(val interface{}) Series
SetValue(i int, val any) Series
Push(val any) 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.
Filter(f func(i int, val any) bool) Series // Where returns a new Series with only the values that return true for the given function.
Map(f func(i int, val any) any) Series // Map returns a new Series with the values modified by the given function.
MapReverse(f func(i int, val any) any) Series // MapReverse is the same as Map but it starts from the last item and works backwards.
// 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
WithValueFunc(value func(i int) any) Series
}
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.
type AppliedSeries struct {
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.Series = s.WithValueFunc(appliedSeries.Value)
return appliedSeries
}
func (s *AppliedSeries) Copy(start, len int) Series {
return NewAppliedSeries(s.Series.Copy(start, len), s.apply)
func (s *AppliedSeries) Copy(start, count int) Series {
return NewAppliedSeries(s.Series.Copy(start, count), 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{} {
func (s *AppliedSeries) Value(i int) any {
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:
//
// s.Series.Value(i)
func (s *AppliedSeries) ValueUnapplied(i int) interface{} {
func (s *AppliedSeries) ValueUnapplied(i int) any {
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.
//
// 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:
//
// 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
// })
// applied.SetValue(0, 10)
// applied.Value(0) // 20
// 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)
return s
}
func (s *AppliedSeries) Push(val interface{}) Series {
func (s *AppliedSeries) Push(val any) Series {
_ = s.Series.Push(val)
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)
}
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)
}
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)
}
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}
}
@ -142,13 +145,13 @@ func NewRollingSeries(s Series, period int) *RollingSeries {
return series
}
func (s *RollingSeries) Copy(start, len int) Series {
return NewRollingSeries(s.Series.Copy(start, len), s.period)
func (s *RollingSeries) Copy(start, count int) Series {
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.
func (s *RollingSeries) Value(i int) interface{} {
items := make([]interface{}, 0, s.period)
// 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) any {
items := make([]any, 0, s.period)
i = EasyIndex(i, s.Len())
if i < 0 || i >= s.Len() {
return items
@ -160,25 +163,29 @@ func (s *RollingSeries) Value(i int) interface{} {
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)
return s
}
func (s *RollingSeries) Push(val interface{}) Series {
func (s *RollingSeries) Push(val any) Series {
_ = s.Series.Push(val)
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)
}
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)
}
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)
}
@ -188,9 +195,9 @@ func (s *RollingSeries) Average() *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) {
case []interface{}:
case []any:
if len(v) == 0 {
return nil
}
@ -217,9 +224,9 @@ func (s *RollingSeries) Mean() *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) {
case []interface{}:
case []any:
if len(v) == 0 {
return nil
}
@ -246,9 +253,9 @@ func (s *RollingSeries) EMA() *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) {
case []interface{}:
case []any:
if len(v) == 0 {
return nil
}
@ -257,7 +264,7 @@ func (s *RollingSeries) Median() *AppliedSeries {
if len(v) == 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)
return x < y || (math.IsNaN(x) && !math.IsNaN(y))
})
@ -269,7 +276,7 @@ func (s *RollingSeries) Median() *AppliedSeries {
if len(v) == 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)
return x < y
})
@ -287,9 +294,9 @@ func (s *RollingSeries) Median() *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) {
case []interface{}:
case []any:
if len(v) == 0 {
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}
}
@ -326,24 +333,25 @@ func (s *RollingSeries) WithValueFunc(value func(i int) interface{}) Series {
// Signals:
// - LengthChanged(int) - when the data is appended or an item is removed.
// - NameChanged(string) - when the name is changed.
// - ValueChanged(int, any) - when a value is changed.
type DataSeries struct {
SignalManager
data df.Series
value func(i int) interface{}
name string
data []any
value func(i int) any
}
func NewDataSeries(data df.Series) *DataSeries {
func NewDataSeries(name string, vals ...any) *DataSeries {
dataSeries := &DataSeries{
SignalManager: SignalManager{},
data: data,
name: name,
data: vals,
}
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.
// 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.
//
// Examples:
//
@ -352,132 +360,130 @@ func NewDataSeries(data df.Series) *DataSeries {
// 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 {
func (s *DataSeries) Copy(start, count int) Series {
if s.Len() == 0 {
return NewDataSeries(s.name)
}
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
}
var end int
start = Max(Min(start, s.Len()), 0)
if count < 0 {
end = s.Len()
} else {
end = Min(start+count, s.Len())
}
return &DataSeries{
SignalManager: SignalManager{},
data: s.data.Copy(df.Range{Start: &start, End: _end}),
value: s.value,
if end <= start {
return NewDataSeries(s.name) // Return an empty series.
}
data := make([]any, end-start)
copy(data, s.data[start:end])
return NewDataSeries(s.name, data...)
}
func (s *DataSeries) Name() string {
return s.data.Name()
return s.name
}
func (s *DataSeries) SetName(name string) Series {
if name == s.Name() {
if name == s.name {
return s
}
s.data.Rename(name)
s.name = name
s.SignalEmit("NameChanged", name)
return s
}
func (s *DataSeries) Len() int {
if s.data == nil {
return 0
}
return s.data.NRows()
return len(s.data)
}
func (s *DataSeries) Push(value interface{}) Series {
if s.data != nil {
s.data.Append(value)
s.SignalEmit("LengthChanged", s.Len())
func (s *DataSeries) Reverse() Series {
if len(s.data) != 0 {
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) SetValue(i int, val interface{}) Series {
if s.data != nil {
s.data.Update(EasyIndex(i, s.Len()), val)
func (s *DataSeries) Push(value any) Series {
s.data = append(s.data, value)
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
}
func (s *DataSeries) Value(i int) interface{} {
if s.data == nil {
func (s *DataSeries) Value(i int) any {
i = EasyIndex(i, s.Len())
if i >= s.Len() || i < 0 {
return nil
}
i = EasyIndex(i, s.Len()) // Allow for negative indexing.
return s.data.Value(i)
return s.data[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
}
// 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, count int) []any {
start = EasyIndex(start, s.Len())
if end < 0 {
end = s.Len() - 1
start = Max(Min(start, s.Len()), 0)
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 {
return nil
if count <= 0 {
return []any{}
}
items := make([]interface{}, end-start+1)
for i := start; i <= end; i++ {
items[i-start] = s.value(i)
}
end := start + count
items := make([]any, count)
copy(items, s.data[start:end])
return items
}
func (s *DataSeries) Values() []interface{} {
if s.data == nil {
return nil
}
// Values returns a copy of all values. If there are no values, an empty slice is returned.
//
// Same as:
//
// ValueRange(0, -1)
func (s *DataSeries) Values() []any {
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 {
val := s.value(i)
if val == nil {
return 0
}
switch val := val.(type) {
case float64:
return val
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)
if val == nil {
return 0
}
switch val := val.(type) {
case int64:
case int:
return val
default:
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 {
val := s.value(i)
if val == nil {
return ""
}
switch val := val.(type) {
case string:
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 {
val := s.value(i)
if val == nil {
return time.Time{}
}
switch val := val.(type) {
case time.Time:
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 {
if s.data == nil {
return nil
}
series := &DataSeries{SignalManager{}, df.NewSeriesGeneric(s.data.Name(), (interface{})(nil), nil), s.value}
func (s *DataSeries) Filter(f func(i int, val any) bool) Series {
series := NewDataSeries(s.name, make([]any, 0, s.Len())...)
for i := 0; i < s.Len(); i++ {
if val := series.value(i); f(i, val) {
if val := s.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}
// 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.
func (s *DataSeries) Map(f func(i int, val any) any) Series {
series := s.Copy(0, -1)
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
}
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}
// 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.
func (s *DataSeries) MapReverse(f func(i int, val any) any) Series {
series := s.Copy(0, -1)
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
}
@ -538,7 +535,7 @@ func (s *DataSeries) Rolling(period int) *RollingSeries {
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.value = value
return copy

View File

@ -1,14 +1,128 @@
package autotrader
import (
"math"
"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) {
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{} {
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 any) any {
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.
applied.SetValue(0, 100)
applied.SetValue(0, 100.0)
if underlying.Float(0) != 1 {
t.Errorf("Expected 1, got %v", underlying.Float(0))
}
@ -43,7 +157,7 @@ func TestAppliedSeries(t *testing.T) {
func TestRollingAppliedSeries(t *testing.T) {
// 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}
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()
if err != nil {
t.Fatalf("Expected no error, got %s", err)
@ -90,7 +204,7 @@ func TestDataSeries(t *testing.T) {
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.
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))
}
}

View File

@ -2,43 +2,71 @@ package autotrader
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 {
SignalConnect(signal string, handler func(...interface{}), bindings ...interface{}) error // SignalConnect connects the handler to the signal.
SignalConnected(signal string, handler func(...interface{})) bool // SignalConnected returns true if the handler is 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.
SignalEmit(signal string, data ...interface{}) // SignalEmit emits the signal with the data.
SignalConnect(signal string, identity any, handler func(...any), bindings ...any) error // SignalConnect connects the handler to the signal under identity.
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.
SignalDisconnect(signal string, identity any, handler func(...any)) // SignalDisconnect removes the handler under identity from the signal.
SignalEmit(signal string, data ...any) // SignalEmit emits the signal with the data.
}
// SignalHandler wraps a signal handler.
type SignalHandler struct {
Callback func(...interface{})
Bindings []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.
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 {
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 {
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
}
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 {
return false
}
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 false
}
// SignalConnections returns a slice of handlers connected to the signal.
func (s *SignalManager) SignalConnections(signal string) []SignalHandler {
if s.signalConnections == nil {
return nil
@ -46,23 +74,27 @@ func (s *SignalManager) SignalConnections(signal string) []SignalHandler {
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 {
return
}
for i, h := range s.signalConnections[signal] {
if reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() {
s.signalConnections[signal] = append(s.signalConnections[signal][:i], s.signalConnections[signal][i+1:]...)
connections := s.signalConnections[signal]
for i, h := range connections {
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 {
return
}
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[len(data):], handler.Bindings)
handler.Callback(args...)

View File

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