mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 16:33:50 +00:00
Switch everything over to use our own DataFrame
This commit is contained in:
parent
5046f3b785
commit
842870011a
@ -2,6 +2,7 @@ package autotrader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -18,6 +19,9 @@ func Backtest(trader *Trader) {
|
|||||||
for !trader.EOF {
|
for !trader.EOF {
|
||||||
trader.Tick()
|
trader.Tick()
|
||||||
}
|
}
|
||||||
|
log.Println("Backtest complete.")
|
||||||
|
log.Println("Stats:")
|
||||||
|
log.Println(trader.Stats())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBroker is a broker that can be used for testing. It implements the Broker interface and fulfills orders
|
// TestBroker is a broker that can be used for testing. It implements the Broker interface and fulfills orders
|
||||||
|
@ -4,8 +4,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
df "github.com/rocketlaunchr/dataframe-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const testDataCSV = `date,open,high,low,close,volume
|
const testDataCSV = `date,open,high,low,close,volume
|
||||||
@ -19,8 +17,8 @@ const testDataCSV = `date,open,high,low,close,volume
|
|||||||
2022-01-08,1.25,1.3,1.2,1.1,150
|
2022-01-08,1.25,1.3,1.2,1.1,150
|
||||||
2022-01-09,1.1,1.4,1.0,1.3,220`
|
2022-01-09,1.1,1.4,1.0,1.3,220`
|
||||||
|
|
||||||
func newTestingDataframe() *df.DataFrame {
|
func newTestingDataframe() *DataFrame {
|
||||||
data, err := ReadDataCSVFromReader(strings.NewReader(testDataCSV), DataCSVLayout{
|
data, err := DataFrameFromCSVReaderLayout(strings.NewReader(testDataCSV), DataCSVLayout{
|
||||||
LatestFirst: false,
|
LatestFirst: false,
|
||||||
DateFormat: "2006-01-02",
|
DateFormat: "2006-01-02",
|
||||||
Date: "date",
|
Date: "date",
|
||||||
@ -37,7 +35,7 @@ func newTestingDataframe() *df.DataFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestingBrokerCandles(t *testing.T) {
|
func TestBacktestingBrokerCandles(t *testing.T) {
|
||||||
data := NewDataFrame(newTestingDataframe())
|
data := newTestingDataframe()
|
||||||
broker := NewTestBroker(nil, data, 0, 0, 0, 0)
|
broker := NewTestBroker(nil, data, 0, 0, 0, 0)
|
||||||
|
|
||||||
candles, err := broker.Candles("EUR_USD", "D", 3)
|
candles, err := broker.Candles("EUR_USD", "D", 3)
|
||||||
@ -88,7 +86,7 @@ func TestBacktestingBrokerFunctions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestingBrokerOrders(t *testing.T) {
|
func TestBacktestingBrokerOrders(t *testing.T) {
|
||||||
data := NewDataFrame(newTestingDataframe())
|
data := newTestingDataframe()
|
||||||
broker := NewTestBroker(nil, data, 100_000, 50, 0, 0)
|
broker := NewTestBroker(nil, data, 100_000, 50, 0, 0)
|
||||||
timeBeforeOrder := time.Now()
|
timeBeforeOrder := time.Now()
|
||||||
order, err := broker.MarketOrder("EUR_USD", 50_000, 0, 0) // Buy 50,000 USD for 1000 EUR with no stop loss or take profit
|
order, err := broker.MarketOrder("EUR_USD", 50_000, 0, 0) // Buy 50,000 USD for 1000 EUR with no stop loss or take profit
|
||||||
|
@ -29,16 +29,7 @@ func main() {
|
|||||||
// os.Exit(1)
|
// os.Exit(1)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
data, err := auto.ReadDataCSV("./EUR_USD Historical Data.csv", auto.DataCSVLayout{
|
data, err := auto.EURUSD()
|
||||||
LatestFirst: true,
|
|
||||||
DateFormat: "01/02/2006",
|
|
||||||
Date: "\ufeff\"Date\"",
|
|
||||||
Open: "Open",
|
|
||||||
High: "High",
|
|
||||||
Low: "Low",
|
|
||||||
Close: "Price",
|
|
||||||
Volume: "Vol.",
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -49,7 +40,7 @@ func main() {
|
|||||||
// AccountID: "101-001-14983263-001",
|
// AccountID: "101-001-14983263-001",
|
||||||
// DemoAccount: true,
|
// DemoAccount: true,
|
||||||
// }),
|
// }),
|
||||||
Broker: auto.NewTestBroker(nil, auto.NewDataFrame(data), 10000, 50, 0.0002, 0),
|
Broker: auto.NewTestBroker(nil, data, 10000, 50, 0.0002, 0),
|
||||||
Strategy: &SMAStrategy{},
|
Strategy: &SMAStrategy{},
|
||||||
Symbol: "EUR_USD",
|
Symbol: "EUR_USD",
|
||||||
Frequency: "D",
|
Frequency: "D",
|
||||||
|
574
data.go
574
data.go
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
df "github.com/rocketlaunchr/dataframe-go"
|
df "github.com/rocketlaunchr/dataframe-go"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,7 +24,11 @@ func EasyIndex(i, n int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Series interface {
|
type Series interface {
|
||||||
|
Signaler
|
||||||
|
|
||||||
Copy(start, end int) Series
|
Copy(start, end int) Series
|
||||||
|
Name() string // Name returns the immutable name of the Series.
|
||||||
|
SetName(name string) Series
|
||||||
Len() int
|
Len() int
|
||||||
|
|
||||||
// Statistical functions.
|
// Statistical functions.
|
||||||
@ -58,10 +63,14 @@ type Frame interface {
|
|||||||
Lows() Series
|
Lows() Series
|
||||||
Closes() Series
|
Closes() Series
|
||||||
Volumes() Series
|
Volumes() Series
|
||||||
|
Contains(names ...string) bool // Contains returns true if the frame contains all the columns specified.
|
||||||
|
ContainsDOHLCV() bool // ContainsDOHLCV returns true if the frame contains the columns: Date, Open, High, Low, Close, Volume.
|
||||||
|
|
||||||
PushCandle(date time.Time, open, high, low, close, volume float64) Frame
|
PushCandle(date time.Time, open, high, low, close, volume float64) error
|
||||||
// AddSeries(name string, s Series) error
|
PushSeries(s ...Series) error
|
||||||
|
RemoveSeries(name string)
|
||||||
|
|
||||||
|
Names() []string
|
||||||
Series(name string) Series
|
Series(name string) Series
|
||||||
Value(column string, i int) interface{}
|
Value(column string, i int) interface{}
|
||||||
Float(column string, i int) float64
|
Float(column string, i int) float64
|
||||||
@ -250,203 +259,15 @@ func (s *RollingSeries) Value(i int) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DataSeries is a Series that wraps a column of data. The data can be of the following types: float64, int64, string, or time.Time.
|
// DataSeries is a Series that wraps a column of data. The data can be of the following types: float64, int64, string, or time.Time.
|
||||||
|
//
|
||||||
|
// Signals:
|
||||||
|
// - LengthChanged(int) - when the data is appended or an item is removed.
|
||||||
|
// - NameChanged(string) - when the name is changed.
|
||||||
type DataSeries struct {
|
type DataSeries struct {
|
||||||
|
SignalManager
|
||||||
data df.Series
|
data df.Series
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataFrame struct {
|
|
||||||
data *df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy copies the DataFrame from start to end (inclusive). If end is -1, it will copy to the end of the DataFrame. If start is out of bounds, nil is returned.
|
|
||||||
func (d *DataFrame) Copy(start, end int) Frame {
|
|
||||||
var _end *int
|
|
||||||
if start < 0 || start >= d.Len() {
|
|
||||||
return nil
|
|
||||||
} else if end >= 0 {
|
|
||||||
if end < start {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_end = &end
|
|
||||||
}
|
|
||||||
return &DataFrame{d.data.Copy(df.Range{Start: &start, End: _end})}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of rows in the DataFrame or 0 if the DataFrame is nil.
|
|
||||||
func (d *DataFrame) Len() int {
|
|
||||||
if d.data == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return d.data.NRows()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date returns the value of the Date column at index i. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
// This is the equivalent to calling Time("Date", i).
|
|
||||||
func (d *DataFrame) Date(i int) time.Time {
|
|
||||||
return d.Time("Date", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open returns the open price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
// This is the equivalent to calling Float("Open", i).
|
|
||||||
func (d *DataFrame) Open(i int) float64 {
|
|
||||||
return d.Float("Open", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// High returns the high price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
// This is the equivalent to calling Float("High", i).
|
|
||||||
func (d *DataFrame) High(i int) float64 {
|
|
||||||
return d.Float("High", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Low returns the low price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
// This is the equivalent to calling Float("Low", i).
|
|
||||||
func (d *DataFrame) Low(i int) float64 {
|
|
||||||
return d.Float("Low", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close returns the close price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
// This is the equivalent to calling Float("Close", i).
|
|
||||||
func (d *DataFrame) Close(i int) float64 {
|
|
||||||
return d.Float("Close", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume returns the volume of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
// This is the equivalent to calling Float("Volume", i).
|
|
||||||
func (d *DataFrame) Volume(i int) float64 {
|
|
||||||
return d.Float("Volume", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dates returns a Series of all the dates in the DataFrame.
|
|
||||||
func (d *DataFrame) Dates() Series {
|
|
||||||
return d.Series("Date")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens returns a Series of all the open prices in the DataFrame.
|
|
||||||
func (d *DataFrame) Opens() Series {
|
|
||||||
return d.Series("Open")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highs returns a Series of all the high prices in the DataFrame.
|
|
||||||
func (d *DataFrame) Highs() Series {
|
|
||||||
return d.Series("High")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lows returns a Series of all the low prices in the DataFrame.
|
|
||||||
func (d *DataFrame) Lows() Series {
|
|
||||||
return d.Series("Low")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closes returns a Series of all the close prices in the DataFrame.
|
|
||||||
func (d *DataFrame) Closes() Series {
|
|
||||||
return d.Series("Close")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volumes returns a Series of all the volumes in the DataFrame.
|
|
||||||
func (d *DataFrame) Volumes() Series {
|
|
||||||
return d.Series("Volume")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close, volume float64) Frame {
|
|
||||||
if d.data == nil {
|
|
||||||
d.data = df.NewDataFrame([]df.Series{
|
|
||||||
df.NewSeriesTime("Date", nil, date),
|
|
||||||
df.NewSeriesFloat64("Open", nil, open),
|
|
||||||
df.NewSeriesFloat64("High", nil, high),
|
|
||||||
df.NewSeriesFloat64("Low", nil, low),
|
|
||||||
df.NewSeriesFloat64("Close", nil, close),
|
|
||||||
df.NewSeriesFloat64("Volume", nil, volume),
|
|
||||||
}...)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
d.data.Append(nil, date, open, high, low, close, volume)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Series returns a Series of the column with the given name. If the column does not exist, nil is returned.
|
|
||||||
func (d *DataFrame) Series(name string) Series {
|
|
||||||
if d.data == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
colIdx, err := d.data.NameToColumn(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &DataSeries{d.data.Series[colIdx]}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns the value of the column at index i. The first value is at index 0. A negative value for i can be used to get i values from the latest, like Python's negative indexing. If i is out of bounds, nil is returned.
|
|
||||||
func (d *DataFrame) Value(column string, i int) interface{} {
|
|
||||||
if d.data == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i = EasyIndex(i, d.Len()) // Allow for negative indexing.
|
|
||||||
colIdx, err := d.data.NameToColumn(column)
|
|
||||||
if err != nil || i < 0 || i >= d.Len() { // Prevent out of bounds access.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.data.Series[colIdx].Value(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float returns the value of the column at index i casted to float64. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
func (d *DataFrame) Float(column string, i int) float64 {
|
|
||||||
val := d.Value(column, i)
|
|
||||||
if val == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
switch val := val.(type) {
|
|
||||||
case float64:
|
|
||||||
return val
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int returns the value of the column at index i casted to int. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
|
||||||
func (d *DataFrame) Int(column string, i int) int64 {
|
|
||||||
val := d.Value(column, i)
|
|
||||||
if val == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
switch val := val.(type) {
|
|
||||||
case int64:
|
|
||||||
return val
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the value of the column at index i casted to string. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, "" is returned.
|
|
||||||
func (d *DataFrame) String(column string, i int) string {
|
|
||||||
val := d.Value(column, i)
|
|
||||||
if val == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
switch val := val.(type) {
|
|
||||||
case string:
|
|
||||||
return val
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time returns the value of the column at index i casted to time.Time. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, time.Time{} is returned.
|
|
||||||
func (d *DataFrame) Time(column string, i int) time.Time {
|
|
||||||
val := d.Value(column, i)
|
|
||||||
if val == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
switch val := val.(type) {
|
|
||||||
case time.Time:
|
|
||||||
return val
|
|
||||||
default:
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDataFrame(data *df.DataFrame) *DataFrame {
|
|
||||||
return &DataFrame{data}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy copies the Series from start to end (inclusive). If end is -1, it will copy to the end of the Series. If start is out of bounds, nil is returned.
|
// Copy copies the Series from start to end (inclusive). If end is -1, it will copy to the end of the Series. If start is out of bounds, nil is returned.
|
||||||
func (s *DataSeries) Copy(start, end int) Series {
|
func (s *DataSeries) Copy(start, end int) Series {
|
||||||
var _end *int
|
var _end *int
|
||||||
@ -458,7 +279,20 @@ func (s *DataSeries) Copy(start, end int) Series {
|
|||||||
}
|
}
|
||||||
_end = &end
|
_end = &end
|
||||||
}
|
}
|
||||||
return &DataSeries{s.data.Copy(df.Range{Start: &start, End: _end})}
|
return &DataSeries{SignalManager{}, s.data.Copy(df.Range{Start: &start, End: _end})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DataSeries) Name() string {
|
||||||
|
return s.data.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DataSeries) SetName(name string) Series {
|
||||||
|
if name == s.Name() {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s.data.Rename(name)
|
||||||
|
s.SignalEmit("NameChanged", name)
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DataSeries) Len() int {
|
func (s *DataSeries) Len() int {
|
||||||
@ -475,6 +309,7 @@ func (s *DataSeries) Rolling(period int) *RollingSeries {
|
|||||||
func (s *DataSeries) Push(value interface{}) Series {
|
func (s *DataSeries) Push(value interface{}) Series {
|
||||||
if s.data != nil {
|
if s.data != nil {
|
||||||
s.data.Append(value)
|
s.data.Append(value)
|
||||||
|
s.SignalEmit("LengthChanged", s.Len())
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@ -493,12 +328,10 @@ func (s *DataSeries) ValueRange(start, end int) []interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
start = EasyIndex(start, s.Len())
|
start = EasyIndex(start, s.Len())
|
||||||
if start < 0 || start >= s.Len() || end >= s.Len() {
|
if start < 0 || start >= s.Len() || end >= s.Len() || start > end {
|
||||||
return nil
|
return nil
|
||||||
} else if end < 0 {
|
} else if end < 0 {
|
||||||
end = s.Len() - 1
|
end = s.Len() - 1
|
||||||
} else if start > end {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]interface{}, end-start+1)
|
items := make([]interface{}, end-start+1)
|
||||||
@ -567,6 +400,293 @@ func (s *DataSeries) Time(i int) time.Time {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewDataSeries(data df.Series) *DataSeries {
|
||||||
|
return &DataSeries{SignalManager{}, data}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataFrame struct {
|
||||||
|
series map[string]Series
|
||||||
|
rowCounts map[string]int
|
||||||
|
// data *df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies the DataFrame from start to end (inclusive). If end is -1, it will copy to the end of the DataFrame. If start is out of bounds, nil is returned.
|
||||||
|
func (d *DataFrame) Copy(start, end int) Frame {
|
||||||
|
out := &DataFrame{}
|
||||||
|
for _, v := range d.series {
|
||||||
|
newSeries := v.Copy(start, end)
|
||||||
|
out.PushSeries(newSeries)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of rows in the DataFrame or 0 if the DataFrame is nil. A value less than zero means the
|
||||||
|
// DataFrame has Series of varying lengths.
|
||||||
|
func (d *DataFrame) Len() int {
|
||||||
|
if len(d.series) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Check if all the Series have the same length.
|
||||||
|
var length int
|
||||||
|
for _, v := range d.rowCounts {
|
||||||
|
if length == 0 {
|
||||||
|
length = v
|
||||||
|
} else if length != v {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date returns the value of the Date column at index i. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
// This is the equivalent to calling Time("Date", i).
|
||||||
|
func (d *DataFrame) Date(i int) time.Time {
|
||||||
|
return d.Time("Date", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open returns the open price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
// This is the equivalent to calling Float("Open", i).
|
||||||
|
func (d *DataFrame) Open(i int) float64 {
|
||||||
|
return d.Float("Open", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// High returns the high price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
// This is the equivalent to calling Float("High", i).
|
||||||
|
func (d *DataFrame) High(i int) float64 {
|
||||||
|
return d.Float("High", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Low returns the low price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
// This is the equivalent to calling Float("Low", i).
|
||||||
|
func (d *DataFrame) Low(i int) float64 {
|
||||||
|
return d.Float("Low", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close returns the close price of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
// This is the equivalent to calling Float("Close", i).
|
||||||
|
func (d *DataFrame) Close(i int) float64 {
|
||||||
|
return d.Float("Close", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume returns the volume of the candle at index i. The first candle is at index 0. A negative value for i (-n) can be used to get n candles from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
// This is the equivalent to calling Float("Volume", i).
|
||||||
|
func (d *DataFrame) Volume(i int) float64 {
|
||||||
|
return d.Float("Volume", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dates returns a Series of all the dates in the DataFrame.
|
||||||
|
func (d *DataFrame) Dates() Series {
|
||||||
|
return d.Series("Date")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens returns a Series of all the open prices in the DataFrame.
|
||||||
|
func (d *DataFrame) Opens() Series {
|
||||||
|
return d.Series("Open")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highs returns a Series of all the high prices in the DataFrame.
|
||||||
|
func (d *DataFrame) Highs() Series {
|
||||||
|
return d.Series("High")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lows returns a Series of all the low prices in the DataFrame.
|
||||||
|
func (d *DataFrame) Lows() Series {
|
||||||
|
return d.Series("Low")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes returns a Series of all the close prices in the DataFrame.
|
||||||
|
func (d *DataFrame) Closes() Series {
|
||||||
|
return d.Series("Close")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volumes returns a Series of all the volumes in the DataFrame.
|
||||||
|
func (d *DataFrame) Volumes() Series {
|
||||||
|
return d.Series("Volume")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) Contains(names ...string) bool {
|
||||||
|
for _, name := range names {
|
||||||
|
if _, ok := d.series[name]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) ContainsDOHLCV() bool {
|
||||||
|
return d.Contains("Date", "Open", "High", "Low", "Close", "Volume")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close, volume float64) error {
|
||||||
|
if len(d.series) == 0 {
|
||||||
|
d.PushSeries([]Series{
|
||||||
|
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.NewSeriesFloat64("Volume", nil, volume)),
|
||||||
|
}...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !d.ContainsDOHLCV() {
|
||||||
|
return fmt.Errorf("DataFrame does not contain Date, Open, High, Low, Close, Volume columns")
|
||||||
|
}
|
||||||
|
d.series["Date"].Push(date)
|
||||||
|
d.series["Open"].Push(open)
|
||||||
|
d.series["High"].Push(high)
|
||||||
|
d.series["Low"].Push(low)
|
||||||
|
d.series["Close"].Push(close)
|
||||||
|
d.series["Volume"].Push(volume)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) PushSeries(series ...Series) error {
|
||||||
|
if d.series == nil {
|
||||||
|
d.series = make(map[string]Series, len(series))
|
||||||
|
d.rowCounts = make(map[string]int, len(series))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range series {
|
||||||
|
name := s.Name()
|
||||||
|
s.SignalConnect("LengthChanged", d.onSeriesLengthChanged, name)
|
||||||
|
s.SignalConnect("NameChanged", d.onSeriesNameChanged, name)
|
||||||
|
d.series[name] = s
|
||||||
|
d.rowCounts[name] = s.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) onSeriesLengthChanged(args ...interface{}) {
|
||||||
|
if len(args) != 2 {
|
||||||
|
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
|
||||||
|
}
|
||||||
|
newLen := args[0].(int)
|
||||||
|
name := args[1].(string)
|
||||||
|
d.rowCounts[name] = newLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) onSeriesNameChanged(args ...interface{}) {
|
||||||
|
if len(args) != 2 {
|
||||||
|
panic(fmt.Sprintf("expected two arguments, got %d", len(args)))
|
||||||
|
}
|
||||||
|
newName := args[0].(string)
|
||||||
|
oldName := args[1].(string)
|
||||||
|
|
||||||
|
d.series[newName] = d.series[oldName]
|
||||||
|
d.rowCounts[newName] = d.rowCounts[oldName]
|
||||||
|
delete(d.series, oldName)
|
||||||
|
delete(d.rowCounts, oldName)
|
||||||
|
|
||||||
|
// Reconnect our signal handlers to update the name we use in the handlers.
|
||||||
|
d.series[newName].SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
|
||||||
|
d.series[newName].SignalDisconnect("NameChanged", d.onSeriesNameChanged)
|
||||||
|
d.series[newName].SignalConnect("LengthChanged", d.onSeriesLengthChanged, newName)
|
||||||
|
d.series[newName].SignalConnect("NameChanged", d.onSeriesNameChanged, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) RemoveSeries(name string) {
|
||||||
|
s, ok := d.series[name]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.SignalDisconnect("LengthChanged", d.onSeriesLengthChanged)
|
||||||
|
s.SignalDisconnect("NameChanged", d.onSeriesNameChanged)
|
||||||
|
delete(d.series, name)
|
||||||
|
delete(d.rowCounts, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataFrame) Names() []string {
|
||||||
|
return maps.Keys(d.series)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Series returns a Series of the column with the given name. If the column does not exist, nil is returned.
|
||||||
|
func (d *DataFrame) Series(name string) Series {
|
||||||
|
if len(d.series) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v, ok := d.series[name]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the value of the column at index i. The first value is at index 0. A negative value for i can be used to get i values from the latest, like Python's negative indexing. If i is out of bounds, nil is returned.
|
||||||
|
func (d *DataFrame) Value(column string, i int) interface{} {
|
||||||
|
if len(d.series) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i = EasyIndex(i, d.Len()) // Allow for negative indexing.
|
||||||
|
if i < 0 || i >= d.Len() { // Prevent out of bounds access.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.series[column].Value(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float returns the value of the column at index i casted to float64. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
func (d *DataFrame) Float(column string, i int) float64 {
|
||||||
|
val := d.Value(column, i)
|
||||||
|
if val == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch val := val.(type) {
|
||||||
|
case float64:
|
||||||
|
return val
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns the value of the column at index i casted to int. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned.
|
||||||
|
func (d *DataFrame) Int(column string, i int) int64 {
|
||||||
|
val := d.Value(column, i)
|
||||||
|
if val == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch val := val.(type) {
|
||||||
|
case int64:
|
||||||
|
return val
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the value of the column at index i casted to string. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, "" is returned.
|
||||||
|
func (d *DataFrame) String(column string, i int) string {
|
||||||
|
val := d.Value(column, i)
|
||||||
|
if val == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch val := val.(type) {
|
||||||
|
case string:
|
||||||
|
return val
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the value of the column at index i casted to time.Time. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, time.Time{} is returned.
|
||||||
|
func (d *DataFrame) Time(column string, i int) time.Time {
|
||||||
|
val := d.Value(column, i)
|
||||||
|
if val == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
switch val := val.(type) {
|
||||||
|
case time.Time:
|
||||||
|
return val
|
||||||
|
default:
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDataFrame(series ...Series) *DataFrame {
|
||||||
|
d := &DataFrame{}
|
||||||
|
d.PushSeries(series...)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
type DataCSVLayout struct {
|
type DataCSVLayout struct {
|
||||||
LatestFirst bool // Whether the latest data is first in the dataframe. If false, the latest data is last.
|
LatestFirst bool // Whether the latest data is first in the dataframe. If false, the latest data is last.
|
||||||
DateFormat string // The format of the date column. Example: "03/22/2006". See https://pkg.go.dev/time#pkg-constants for more information.
|
DateFormat string // The format of the date column. Example: "03/22/2006". See https://pkg.go.dev/time#pkg-constants for more information.
|
||||||
@ -578,17 +698,8 @@ type DataCSVLayout struct {
|
|||||||
Volume string
|
Volume string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadDataCSV(path string, layout DataCSVLayout) (*df.DataFrame, error) {
|
func EURUSD() (*DataFrame, error) {
|
||||||
f, err := os.Open(path)
|
return DataFrameFromCSVLayout("./EUR_USD Historical Data.csv", DataCSVLayout{
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return ReadDataCSVFromReader(f, layout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadEURUSDDataCSV() (*df.DataFrame, error) {
|
|
||||||
return ReadDataCSV("./EUR_USD Historical Data.csv", DataCSVLayout{
|
|
||||||
LatestFirst: true,
|
LatestFirst: true,
|
||||||
DateFormat: "01/02/2006",
|
DateFormat: "01/02/2006",
|
||||||
Date: "\ufeff\"Date\"",
|
Date: "\ufeff\"Date\"",
|
||||||
@ -600,8 +711,17 @@ func ReadEURUSDDataCSV() (*df.DataFrame, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadDataCSVFromReader(r io.Reader, layout DataCSVLayout) (*df.DataFrame, error) {
|
func DataFrameFromCSVLayout(path string, layout DataCSVLayout) (*DataFrame, error) {
|
||||||
data, err := ReadCSVFromReader(r, layout.DateFormat, layout.LatestFirst)
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return DataFrameFromCSVReaderLayout(f, layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DataFrameFromCSVReaderLayout(r io.Reader, layout DataCSVLayout) (*DataFrame, error) {
|
||||||
|
data, err := DataFrameFromCSVReader(r, layout.DateFormat, layout.LatestFirst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@ -626,23 +746,19 @@ func ReadDataCSVFromReader(r io.Reader, layout DataCSVLayout) (*df.DataFrame, er
|
|||||||
data.RemoveSeries(name)
|
data.RemoveSeries(name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
idx, err := data.NameToColumn(name)
|
data.Series(name).SetName(newName)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
data.Series[idx].Rename(newName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = data.ReorderColumns([]string{"Date", "Open", "High", "Low", "Close", "Volume"})
|
// err = data.ReorderColumns([]string{"Date", "Open", "High", "Low", "Close", "Volume"})
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return data, err
|
// return data, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
// TODO: Reverse the dataframe if the latest data is first.
|
// TODO: Reverse the dataframe if the latest data is first.
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadCSVFromReader(r io.Reader, dateLayout string, readReversed bool) (*df.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()
|
records, err := csv.ReadAll()
|
||||||
@ -654,7 +770,7 @@ func ReadCSVFromReader(r io.Reader, dateLayout string, readReversed bool) (*df.D
|
|||||||
return nil, errors.New("csv file must have at least 2 rows")
|
return nil, errors.New("csv file must have at least 2 rows")
|
||||||
}
|
}
|
||||||
|
|
||||||
seriesSlice := make([]df.Series, 0, 12)
|
dfSeriesSlice := make([]df.Series, 0, 12)
|
||||||
// TODO: change Capacity to Size.
|
// TODO: change Capacity to Size.
|
||||||
initOptions := &df.SeriesInit{Capacity: len(records) - 1}
|
initOptions := &df.SeriesInit{Capacity: len(records) - 1}
|
||||||
|
|
||||||
@ -674,7 +790,7 @@ func ReadCSVFromReader(r io.Reader, dateLayout string, readReversed bool) (*df.D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the series columns and label them.
|
// Create the series columns and label them.
|
||||||
seriesSlice = append(seriesSlice, series)
|
dfSeriesSlice = append(dfSeriesSlice, series)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the direction to iterate the records.
|
// Set the direction to iterate the records.
|
||||||
@ -694,7 +810,7 @@ func ReadCSVFromReader(r io.Reader, dateLayout string, readReversed bool) (*df.D
|
|||||||
|
|
||||||
// Add rows to the series.
|
// Add rows to the series.
|
||||||
for j, val := range rec {
|
for j, val := range rec {
|
||||||
series := seriesSlice[j]
|
series := dfSeriesSlice[j]
|
||||||
switch series.Type() {
|
switch series.Type() {
|
||||||
case "float64":
|
case "float64":
|
||||||
val, err := strconv.ParseFloat(val, 64)
|
val, err := strconv.ParseFloat(val, 64)
|
||||||
@ -720,11 +836,15 @@ func ReadCSVFromReader(r io.Reader, dateLayout string, readReversed bool) (*df.D
|
|||||||
case "string":
|
case "string":
|
||||||
series.Append(val)
|
series.Append(val)
|
||||||
}
|
}
|
||||||
seriesSlice[j] = series
|
dfSeriesSlice[j] = series
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: we specifically construct the DataFrame at the end of the function because it likes to set
|
// 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.
|
// state like number of rows and columns at initialization and won't let you change it later.
|
||||||
return df.NewDataFrame(seriesSlice...), nil
|
seriesSlice := make([]Series, len(dfSeriesSlice))
|
||||||
|
for i, series := range dfSeriesSlice {
|
||||||
|
seriesSlice[i] = NewDataSeries(series)
|
||||||
|
}
|
||||||
|
return NewDataFrame(seriesSlice...), nil
|
||||||
}
|
}
|
||||||
|
55
data_test.go
55
data_test.go
@ -6,18 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func newTestingDataFrame() *DataFrame {
|
func newTestingDataFrame() *DataFrame {
|
||||||
_dataframe, err := ReadEURUSDDataCSV()
|
data, err := EURUSD()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
panic(err)
|
||||||
}
|
}
|
||||||
return NewDataFrame(_dataframe)
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDataSeries(t *testing.T) {
|
func TestDataSeries(t *testing.T) {
|
||||||
data := newTestingDataFrame()
|
data := newTestingDataFrame()
|
||||||
if data == nil {
|
|
||||||
t.Fatal("Could not create DataFrame")
|
|
||||||
}
|
|
||||||
|
|
||||||
dates, closes := data.Dates(), data.Closes()
|
dates, closes := data.Dates(), data.Closes()
|
||||||
|
|
||||||
@ -39,9 +36,6 @@ func TestDataSeries(t *testing.T) {
|
|||||||
|
|
||||||
func TestDataFrame(t *testing.T) {
|
func TestDataFrame(t *testing.T) {
|
||||||
data := newTestingDataFrame()
|
data := newTestingDataFrame()
|
||||||
if data == nil {
|
|
||||||
t.Fatal("Could not create DataFrame")
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Len() != 2610 {
|
if data.Len() != 2610 {
|
||||||
t.Fatalf("Expected 2610 rows, got %d", data.Len())
|
t.Fatalf("Expected 2610 rows, got %d", data.Len())
|
||||||
@ -55,7 +49,11 @@ func TestDataFrame(t *testing.T) {
|
|||||||
t.Fatalf("Expected 2013-05-13, got %s", date.Format(time.DateOnly))
|
t.Fatalf("Expected 2013-05-13, got %s", date.Format(time.DateOnly))
|
||||||
}
|
}
|
||||||
|
|
||||||
data.PushCandle(time.Date(2023, 5, 14, 0, 0, 0, 0, time.UTC), 1.0, 1.0, 1.0, 1.0, 1)
|
err := data.PushCandle(time.Date(2023, 5, 14, 0, 0, 0, 0, time.UTC), 1.0, 1.0, 1.0, 1.0, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(data.Names())
|
||||||
|
t.Fatalf("Expected no error, got %s", err)
|
||||||
|
}
|
||||||
if data.Len() != 2611 {
|
if data.Len() != 2611 {
|
||||||
t.Fatalf("Expected 2611 rows, got %d", data.Len())
|
t.Fatalf("Expected 2611 rows, got %d", data.Len())
|
||||||
}
|
}
|
||||||
@ -65,37 +63,34 @@ func TestDataFrame(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReadDataCSV(t *testing.T) {
|
func TestReadDataCSV(t *testing.T) {
|
||||||
data, err := ReadEURUSDDataCSV()
|
data := newTestingDataFrame()
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.NRows() != 2610 {
|
if data.Len() != 2610 {
|
||||||
t.Fatalf("Expected 2610 rows, got %d", data.NRows())
|
t.Fatalf("Expected 2610 rows, got %d", data.Len())
|
||||||
}
|
}
|
||||||
if len(data.Names()) != 6 {
|
if len(data.Names()) != 6 {
|
||||||
t.Fatalf("Expected 6 columns, got %d", len(data.Names()))
|
t.Fatalf("Expected 6 columns, got %d", len(data.Names()))
|
||||||
}
|
}
|
||||||
if data.Series[0].Name() != "Date" {
|
if data.Series("Date") == nil {
|
||||||
t.Fatalf("Expected Date column, got %s", data.Series[0].Name())
|
t.Fatalf("Expected Date column, got nil")
|
||||||
}
|
}
|
||||||
if data.Series[1].Name() != "Open" {
|
if data.Series("Open") == nil {
|
||||||
t.Fatalf("Expected Open column, got %s", data.Series[1].Name())
|
t.Fatalf("Expected Open column, got nil")
|
||||||
}
|
}
|
||||||
if data.Series[2].Name() != "High" {
|
if data.Series("High") == nil {
|
||||||
t.Fatalf("Expected High column, got %s", data.Series[2].Name())
|
t.Fatalf("Expected High column, got nil")
|
||||||
}
|
}
|
||||||
if data.Series[3].Name() != "Low" {
|
if data.Series("Low") == nil {
|
||||||
t.Fatalf("Expected Low column, got %s", data.Series[3].Name())
|
t.Fatalf("Expected Low column, got nil")
|
||||||
}
|
}
|
||||||
if data.Series[4].Name() != "Close" {
|
if data.Series("Close") == nil {
|
||||||
t.Fatalf("Expected Close column, got %s", data.Series[4].Name())
|
t.Fatalf("Expected Close column, got nil")
|
||||||
}
|
}
|
||||||
if data.Series[5].Name() != "Volume" {
|
if data.Series("Volume") == nil {
|
||||||
t.Fatalf("Expected Volume column, got %s", data.Series[5].Name())
|
t.Fatalf("Expected Volume column, got nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Series[0].Type() != "time" {
|
if data.Series("Date").Time(0).Equal(time.Time{}) {
|
||||||
t.Fatalf("Expected Date column type time, got %s", data.Series[0].Type())
|
t.Fatalf("Expected Date column to have type time.Time, got %s", data.Value("Date", 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
signals.go
40
signals.go
@ -3,60 +3,68 @@ package autotrader
|
|||||||
import "reflect"
|
import "reflect"
|
||||||
|
|
||||||
type Signaler interface {
|
type Signaler interface {
|
||||||
SignalConnect(signal string, handler func(interface{})) error // SignalConnect connects the handler to the signal.
|
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.
|
SignalConnected(signal string, handler func(...interface{})) bool // SignalConnected returns true if the handler is connected to the signal.
|
||||||
SignalConnections(signal string) []func(interface{}) // 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, handler func(...interface{})) // SignalDisconnect removes the handler from the signal.
|
||||||
SignalEmit(signal string, data interface{}) // SignalEmit emits the signal with the data.
|
SignalEmit(signal string, data ...interface{}) // SignalEmit emits the signal with the data.
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignalHandler struct {
|
||||||
|
Callback func(...interface{})
|
||||||
|
Bindings []interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SignalManager struct {
|
type SignalManager struct {
|
||||||
signalConnections map[string][]func(interface{})
|
signalConnections map[string][]SignalHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalManager) SignalConnect(signal string, handler func(interface{})) error {
|
func (s *SignalManager) SignalConnect(signal string, callback func(...interface{}), bindings ...interface{}) error {
|
||||||
if s.signalConnections == nil {
|
if s.signalConnections == nil {
|
||||||
s.signalConnections = make(map[string][]func(interface{}))
|
s.signalConnections = make(map[string][]SignalHandler)
|
||||||
}
|
}
|
||||||
s.signalConnections[signal] = append(s.signalConnections[signal], handler)
|
s.signalConnections[signal] = append(s.signalConnections[signal], SignalHandler{callback, bindings})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalManager) SignalConnected(signal string, handler func(interface{})) bool {
|
func (s *SignalManager) SignalConnected(signal string, callback func(...interface{})) 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).Pointer() == reflect.ValueOf(handler).Pointer() {
|
if reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalManager) SignalConnections(signal string) []func(interface{}) {
|
func (s *SignalManager) SignalConnections(signal string) []SignalHandler {
|
||||||
if s.signalConnections == nil {
|
if s.signalConnections == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return s.signalConnections[signal]
|
return s.signalConnections[signal]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalManager) SignalDisconnect(signal string, handler func(interface{})) {
|
func (s *SignalManager) SignalDisconnect(signal string, callback func(...interface{})) {
|
||||||
if s.signalConnections == nil {
|
if s.signalConnections == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, h := range s.signalConnections[signal] {
|
for i, h := range s.signalConnections[signal] {
|
||||||
if reflect.ValueOf(h).Pointer() == reflect.ValueOf(handler).Pointer() {
|
if reflect.ValueOf(h.Callback).Pointer() == reflect.ValueOf(callback).Pointer() {
|
||||||
s.signalConnections[signal] = append(s.signalConnections[signal][:i], s.signalConnections[signal][i+1:]...)
|
s.signalConnections[signal] = append(s.signalConnections[signal][:i], s.signalConnections[signal][i+1:]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalManager) SignalEmit(signal string, data interface{}) {
|
func (s *SignalManager) SignalEmit(signal string, data ...interface{}) {
|
||||||
if s.signalConnections == nil {
|
if s.signalConnections == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, handler := range s.signalConnections[signal] {
|
for _, handler := range s.signalConnections[signal] {
|
||||||
handler(data)
|
args := make([]interface{}, len(data)+len(handler.Bindings))
|
||||||
|
copy(args, data)
|
||||||
|
copy(args[len(data):], handler.Bindings)
|
||||||
|
handler.Callback(args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user