PushCandle

This commit is contained in:
Luke I. Wilson 2023-05-14 20:40:22 -05:00
parent c00a468249
commit 7f09664454
4 changed files with 111 additions and 15 deletions

View File

@ -15,7 +15,9 @@ var (
) )
func Backtest(trader *Trader) { func Backtest(trader *Trader) {
for !trader.EOF {
trader.Tick() trader.Tick()
}
} }
// 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
@ -41,10 +43,15 @@ type TestBroker struct {
positions []Position positions []Position
} }
// CandleIndex returns the index of the current candle.
func (b *TestBroker) candleIndex() int {
return Max(b.candleCount-1, 0)
}
func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataFrame, error) { func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataFrame, error) {
// Check if we reached the end of the existing data. // Check if we reached the end of the existing data.
if b.Data != nil && b.candleCount >= b.Data.Len() { if b.Data != nil && b.candleCount >= b.Data.Len() {
return b.Data.Copy(0, -1), ErrEOF return b.Data.Copy(0, -1).(*DataFrame), ErrEOF
} }
// Catch up to the start candles. // Catch up to the start candles.
@ -82,7 +89,7 @@ func (b *TestBroker) candles(symbol string, frequency string, count int) (*DataF
end := Max(b.candleCount, 1) - 1 end := Max(b.candleCount, 1) - 1
start := Max(Max(b.candleCount, 1)-count, 0) start := Max(Max(b.candleCount, 1)-count, 0)
return b.Data.Copy(start, end), nil return b.Data.Copy(start, end).(*DataFrame), nil
} }
func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) { func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) {
@ -95,7 +102,7 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
return nil, err return nil, err
} }
} }
price := b.Data.Close(Max(b.candleCount-1, 0)) // Get the last close price. price := b.Data.Close(b.candleIndex()) // Get the last close price.
// Instantly fulfill the order. // Instantly fulfill the order.
b.Cash -= price * units * LeverageToMargin(b.Leverage) b.Cash -= price * units * LeverageToMargin(b.Leverage)
@ -145,6 +152,7 @@ func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread fl
} }
type TestPosition struct { type TestPosition struct {
broker *TestBroker
closed bool closed bool
entryPrice float64 entryPrice float64
id string id string
@ -181,7 +189,10 @@ func (p *TestPosition) Leverage() float64 {
} }
func (p *TestPosition) PL() float64 { func (p *TestPosition) PL() float64 {
return 0 price := p.broker.Data.Close(p.broker.candleIndex()) + p.broker.Spread
priceDiff := price - p.entryPrice
units := priceDiff * p.units * LeverageToMargin(p.leverage)
return units * price
} }
func (p *TestPosition) Symbol() string { func (p *TestPosition) Symbol() string {

83
data.go
View File

@ -23,14 +23,18 @@ func EasyIndex(i, n int) int {
} }
type Series interface { type Series interface {
Copy() Series Copy(start, end int) Series
Len() int Len() int
// Statistical functions. // Statistical functions.
Rolling(period int) *RollingSeries Rolling(period int) *RollingSeries
Push(val interface{}) Series
// Data access functions. // Data access functions.
Value(i int) interface{} Value(i int) interface{}
ValueRange(start, end int) []interface{}
Values() []interface{} // Values is the same as ValueRange(0, -1).
Float(i int) float64 Float(i int) float64
Int(i int) int64 Int(i int) int64
String(i int) string String(i int) string
@ -38,7 +42,7 @@ type Series interface {
} }
type Frame interface { type Frame interface {
Copy() Frame Copy(start, end int) Frame
Len() int Len() int
// Easy access functions. // Easy access functions.
@ -55,7 +59,9 @@ type Frame interface {
Closes() Series Closes() Series
Volumes() Series Volumes() Series
// Custom data columns PushCandle(date time.Time, open, high, low, close, volume float64) Frame
// AddSeries(name string, s Series) error
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
@ -253,7 +259,7 @@ type DataFrame struct {
} }
// 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. // 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 (o *DataFrame) Copy(start, end int) *DataFrame { func (o *DataFrame) Copy(start, end int) Frame {
var _end *int var _end *int
if start < 0 || start >= o.Len() { if start < 0 || start >= o.Len() {
return nil return nil
@ -340,6 +346,22 @@ func (o *DataFrame) Volumes() Series {
return o.Series("Volume") return o.Series("Volume")
} }
func (o *DataFrame) PushCandle(date time.Time, open, high, low, close, volume float64) Frame {
if o.data == nil {
o.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 o
}
o.data.Append(nil, date, open, high, low, close, volume)
return o
}
// Series returns a Series of the column with the given name. If the column does not exist, nil is returned. // Series returns a Series of the column with the given name. If the column does not exist, nil is returned.
func (o *DataFrame) Series(name string) Series { func (o *DataFrame) Series(name string) Series {
if o.data == nil { if o.data == nil {
@ -380,13 +402,13 @@ func (o *DataFrame) Float(column string, i int) float64 {
} }
// Int returns the value of the column at index i casted to int. The first value is at index 0. A negative value for i (-n) can be used to get n values from the latest, like Python's negative indexing. If i is out of bounds, 0 is returned. // Int returns the value of the column at index i casted to int. The first value is at index 0. A negative value for i (-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 (o *DataFrame) Int(column string, i int) int { func (o *DataFrame) Int(column string, i int) int64 {
val := o.Value(column, i) val := o.Value(column, i)
if val == nil { if val == nil {
return 0 return 0
} }
switch val := val.(type) { switch val := val.(type) {
case int: case int64:
return val return val
default: default:
return 0 return 0
@ -425,8 +447,18 @@ func NewDataFrame(data *df.DataFrame) *DataFrame {
return &DataFrame{data} return &DataFrame{data}
} }
func (s *DataSeries) Copy() Series { // 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.
return &DataSeries{s.data.Copy()} func (s *DataSeries) Copy(start, end int) Series {
var _end *int
if start < 0 || start >= s.Len() {
return nil
} else if end >= 0 {
if end < start {
return nil
}
_end = &end
}
return &DataSeries{s.data.Copy(df.Range{Start: &start, End: _end})}
} }
func (s *DataSeries) Len() int { func (s *DataSeries) Len() int {
@ -440,6 +472,13 @@ func (s *DataSeries) Rolling(period int) *RollingSeries {
return &RollingSeries{s, period} return &RollingSeries{s, period}
} }
func (s *DataSeries) Push(value interface{}) Series {
if s.data != nil {
s.data.Append(value)
}
return s
}
func (s *DataSeries) Value(i int) interface{} { func (s *DataSeries) Value(i int) interface{} {
if s.data == nil { if s.data == nil {
return nil return nil
@ -448,6 +487,34 @@ func (s *DataSeries) Value(i int) interface{} {
return s.data.Value(i) return s.data.Value(i)
} }
// ValueRange returns a slice of values from start to end, including start and end. The first value is at index 0. A negative value for start or end can be used to get values from the latest, like Python's negative indexing. If end is less than zero, it will be sliced from start to the last item. If start or end is out of bounds, nil is returned. If start is greater than end, nil is returned.
func (s *DataSeries) ValueRange(start, end int) []interface{} {
if s.data == nil {
return nil
}
start = EasyIndex(start, s.Len())
if start < 0 || start >= s.Len() || end >= s.Len() {
return nil
} else if end < 0 {
end = s.Len() - 1
} else if start > end {
return nil
}
items := make([]interface{}, end-start+1)
for i := start; i <= end; i++ {
items[i-start] = s.Value(i)
}
return items
}
func (s *DataSeries) Values() []interface{} {
if s.data == nil {
return nil
}
return s.ValueRange(0, -1)
}
func (s *DataSeries) Float(i int) float64 { func (s *DataSeries) Float(i int) float64 {
val := s.Value(i) val := s.Value(i)
if val == nil { if val == nil {

View File

@ -54,6 +54,14 @@ func TestDataFrame(t *testing.T) {
if date.Year() != 2013 || date.Month() != 5 || date.Day() != 13 { if date.Year() != 2013 || date.Month() != 5 || date.Day() != 13 {
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)
if data.Len() != 2611 {
t.Fatalf("Expected 2611 rows, got %d", data.Len())
}
if data.Close(-1) != 1.0 {
t.Fatalf("Expected latest close to be 1.0, got %f", data.Close(-1))
}
} }
func TestReadDataCSV(t *testing.T) { func TestReadDataCSV(t *testing.T) {

View File

@ -21,16 +21,22 @@ type Trader struct {
Frequency string Frequency string
CandlesToKeep int CandlesToKeep int
Log *log.Logger Log *log.Logger
EOF bool
data *DataFrame data *DataFrame
sched *gocron.Scheduler sched *gocron.Scheduler
idx int idx int
stats *DataFrame // Performance (financial) reporting and statistics.
} }
func (t *Trader) Data() *DataFrame { func (t *Trader) Data() *DataFrame {
return t.data return t.data
} }
func (t *Trader) Stats() *DataFrame {
return t.stats
}
// Run starts the trader. This is a blocking call. // Run starts the trader. This is a blocking call.
func (t *Trader) Run() { func (t *Trader) Run() {
t.sched = gocron.NewScheduler(time.UTC) t.sched = gocron.NewScheduler(time.UTC)
@ -71,18 +77,21 @@ func (t *Trader) Run() {
// Tick updates the current state of the market and runs the strategy. // Tick updates the current state of the market and runs the strategy.
func (t *Trader) Tick() { func (t *Trader) Tick() {
t.Log.Println("Tick")
if t.idx == 0 { if t.idx == 0 {
t.Strategy.Init(t) t.Strategy.Init(t)
} }
t.fetchData() t.fetchData()
t.Strategy.Next(t) t.Strategy.Next(t)
t.Log.Println("Tick")
} }
func (t *Trader) fetchData() { func (t *Trader) fetchData() {
var err error var err error
t.data, err = t.Broker.Candles(t.Symbol, t.Frequency, t.CandlesToKeep) t.data, err = t.Broker.Candles(t.Symbol, t.Frequency, t.CandlesToKeep)
if err != nil { if err == ErrEOF {
t.Log.Println("End of data")
t.sched.Clear()
} else if err != nil {
panic(err) // TODO: implement safe shutdown procedure panic(err) // TODO: implement safe shutdown procedure
} }
} }
@ -105,5 +114,6 @@ func NewTrader(config TraderConfig) *Trader {
Frequency: config.Frequency, Frequency: config.Frequency,
CandlesToKeep: config.CandlesToKeep, CandlesToKeep: config.CandlesToKeep,
Log: logger, Log: logger,
stats: NewDataFrame(nil),
} }
} }