Worked on several new features at once

This commit is contained in:
Luke I. Wilson 2023-05-13 21:07:36 -05:00
parent 4a12b93992
commit 435ce1e144
4 changed files with 321 additions and 2 deletions

View File

@ -10,15 +10,26 @@ import (
)
var (
ErrEOF = errors.New("end of the input data")
ErrNoData = errors.New("no data")
ErrEOF = errors.New("end of the input data")
ErrNoData = errors.New("no data")
ErrPositionClosed = errors.New("position closed")
)
func Backtest(trader *Trader) {
trader.Tick()
}
// TestBroker is a broker that can be used for testing. It implements the Broker interface and fulfills orders
//
// Signals:
// - Tick(nil) - Called when the broker ticks.
// - OrderPlaced(Order) - Called when an order is placed.
// - OrderFilled(Order) - Called when an order is filled.
// - OrderCanceled(Order) - Called when an order is canceled.
// - PositionClosed(Position) - Called when a position is closed.
// - PositionModified(Position) - Called when a position changes.
type TestBroker struct {
SignalManager
DataBroker Broker
Data *df.DataFrame
Cash float64
@ -110,6 +121,7 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
b.orders = append(b.orders, order)
b.positions = append(b.positions, position)
b.SignalEmit("OrderPlaced", order)
return order, nil
}
@ -138,6 +150,63 @@ func NewTestBroker(dataBroker Broker, data *df.DataFrame, cash, leverage, spread
}
type TestPosition struct {
closed bool
entryPrice float64
id string
leverage float64
symbol string
stopLoss float64
takeProfit float64
time time.Time
units float64
}
func (p *TestPosition) Close() error {
if p.closed {
return ErrPositionClosed
}
p.closed = true
return nil
}
func (p *TestPosition) Closed() bool {
return p.closed
}
func (p *TestPosition) EntryPrice() float64 {
return p.entryPrice
}
func (p *TestPosition) Id() string {
return p.id
}
func (p *TestPosition) Leverage() float64 {
return p.leverage
}
func (p *TestPosition) PL() float64 {
return 0
}
func (p *TestPosition) Symbol() string {
return p.symbol
}
func (p *TestPosition) StopLoss() float64 {
return p.stopLoss
}
func (p *TestPosition) TakeProfit() float64 {
return p.takeProfit
}
func (p *TestPosition) Time() time.Time {
return p.time
}
func (p *TestPosition) Units() float64 {
return p.units
}
type TestOrder struct {

View File

@ -38,6 +38,17 @@ type Order interface {
}
type Position interface {
Close() error // Close attempts to close the position and returns an error if it fails. If the error is nil, the position was closed.
Closed() bool // Closed returns true if the position has been closed with the broker.
EntryPrice() float64 // EntryPrice returns the price of the symbol at the time the position was opened.
Id() string // Id returns the unique identifier of the position by the broker.
Leverage() float64 // Leverage returns the leverage of the position.
PL() float64 // PL returns the profit or loss of the position.
Symbol() string // Symbol returns the symbol name of the position.
StopLoss() float64 // StopLoss returns the stop loss price of the position.
TakeProfit() float64 // TakeProfit returns the take profit price of the position.
Time() time.Time // Time returns the time the position was opened.
Units() float64 // Units returns the number of units purchased or sold by the position.
}
type Broker interface {
@ -45,6 +56,10 @@ type Broker interface {
Candles(symbol string, frequency string, count int) (*df.DataFrame, error)
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
NAV() float64 // NAV returns the net asset value of the account.
// Orders returns a slice of orders that have been placed with the broker. If an order has been canceled or
// filled, it will not be returned.
Orders() []Order
// Positions returns a slice of positions that are currently open with the broker. If a position has been
// closed, it will not be returned.
Positions() []Position
}

173
data.go
View File

@ -11,6 +11,179 @@ import (
df "github.com/rocketlaunchr/dataframe-go"
)
type Series interface {
Copy() Series
Len() int
}
type Frame interface {
Copy() Frame
Len() int
// Comparison functions.
Equal(other Frame) bool
NotEqual(other Frame) bool
Less(other Frame) bool
LessEqual(other Frame) bool
Greater(other Frame) bool
GreaterEqual(other Frame) bool
// Easy access functions.
Date(i int) time.Time
Open(i int) float64
High(i int) float64
Low(i int) float64
Close(i int) float64
Volume(i int) float64
Dates() Series
Opens() Series
Highs() Series
Lows() Series
Closes() Series
Volumes() Series
// Custom data columns
Value(column string, i int) interface{}
Float(column string, i int) float64
Int(column string, i int) int
String(column string, i int) string
// Time returns the value of the 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.
Time(column string, i int) time.Time
}
type DataFrame struct {
*df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
}
func (o *DataFrame) Copy() *DataFrame {
return &DataFrame{o.DataFrame.Copy()}
}
func (o *DataFrame) Len() int {
if o.DataFrame == nil {
return 0
}
return o.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 (o *DataFrame) Date(i int) time.Time {
return o.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 (o *DataFrame) Open(i int) float64 {
return o.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 (o *DataFrame) High(i int) float64 {
return o.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 (o *DataFrame) Low(i int) float64 {
return o.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 (o *DataFrame) Close(i int) float64 {
return o.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 (o *DataFrame) Volume(i int) float64 {
return o.Float("Volume", i)
}
// Value returns the value of the 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, nil is returned.
func (o *DataFrame) Value(column string, i int) interface{} {
colIdx, err := o.DataFrame.NameToColumn(column)
if err != nil {
return nil
} else if o.DataFrame == nil || i >= o.Len() {
return 0
} else if i < 0 {
i = o.Len() - i
if i < 0 {
return 0
}
return o.Series[colIdx].Value(i)
}
return o.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 (o *DataFrame) Float(column string, i int) float64 {
val := o.Value(column, i)
if val == nil {
return 0
}
switch val.(type) {
case float64:
return val.(float64)
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 (o *DataFrame) Int(column string, i int) int {
val := o.Value(column, i)
if val == nil {
return 0
}
switch val.(type) {
case int:
return val.(int)
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 (o *DataFrame) String(column string, i int) string {
val := o.Value(column, i)
if val == nil {
return ""
}
switch val.(type) {
case string:
return val.(string)
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 (o *DataFrame) Time(column string, i int) time.Time {
val := o.Value(column, i)
if val == nil {
return time.Time{}
}
switch val.(type) {
case time.Time:
return val.(time.Time)
default:
return time.Time{}
}
}
func NewChartData(data *df.DataFrame) *DataFrame {
return &DataFrame{data}
}
type RollingWindow struct {
DataFrame
Period int
}
type DataCSVLayout struct {
LatestFirst bool // Whether the latest data is first in the dataframe. If false, the latest data is last.
DateFormat string // The format of the date column. Example: "03/22/2006". See https://pkg.go.dev/time#pkg-constants for more information.

62
signals.go Normal file
View File

@ -0,0 +1,62 @@
package autotrader
import "reflect"
type Signaler interface {
SignalConnect(signal string, handler func(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) []func(interface{}) // 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.
}
type SignalManager struct {
signalConnections map[string][]func(interface{})
}
func (s *SignalManager) SignalConnect(signal string, handler func(interface{})) error {
if s.signalConnections == nil {
s.signalConnections = make(map[string][]func(interface{}))
}
s.signalConnections[signal] = append(s.signalConnections[signal], handler)
return nil
}
func (s *SignalManager) SignalConnected(signal string, handler func(interface{})) bool {
if s.signalConnections == nil {
return false
}
for _, h := range s.signalConnections[signal] {
if reflect.ValueOf(h).Pointer() == reflect.ValueOf(handler).Pointer() {
return true
}
}
return false
}
func (s *SignalManager) SignalConnections(signal string) []func(interface{}) {
if s.signalConnections == nil {
return nil
}
return s.signalConnections[signal]
}
func (s *SignalManager) SignalDisconnect(signal string, handler func(interface{})) {
if s.signalConnections == nil {
return
}
for i, h := range s.signalConnections[signal] {
if reflect.ValueOf(h).Pointer() == reflect.ValueOf(handler).Pointer() {
s.signalConnections[signal] = append(s.signalConnections[signal][:i], s.signalConnections[signal][i+1:]...)
}
}
}
func (s *SignalManager) SignalEmit(signal string, data interface{}) {
if s.signalConnections == nil {
return
}
for _, handler := range s.signalConnections[signal] {
handler(data)
}
}