mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 16:33:50 +00:00
Implemented backtesting orders
This commit is contained in:
parent
2ca0ccc293
commit
4a12b93992
161
backtesting.go
161
backtesting.go
@ -2,11 +2,17 @@ package autotrader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
df "github.com/rocketlaunchr/dataframe-go"
|
df "github.com/rocketlaunchr/dataframe-go"
|
||||||
|
"golang.org/x/exp/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNoData = errors.New("no data")
|
var (
|
||||||
|
ErrEOF = errors.New("end of the input data")
|
||||||
|
ErrNoData = errors.New("no data")
|
||||||
|
)
|
||||||
|
|
||||||
func Backtest(trader *Trader) {
|
func Backtest(trader *Trader) {
|
||||||
trader.Tick()
|
trader.Tick()
|
||||||
@ -17,11 +23,32 @@ type TestBroker struct {
|
|||||||
Data *df.DataFrame
|
Data *df.DataFrame
|
||||||
Cash float64
|
Cash float64
|
||||||
Leverage float64
|
Leverage float64
|
||||||
|
Spread float64 // Number of pips to add to the price when buying and subtract when selling. (Forex)
|
||||||
StartCandles int
|
StartCandles int
|
||||||
candles int
|
|
||||||
|
candleCount int // The number of candles anyone outside this broker has seen. Also equal to the number of times Candles has been called.
|
||||||
|
orders []Order
|
||||||
|
positions []Position
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TestBroker) Candles(symbol string, frequency string, count int) (*df.DataFrame, error) {
|
func (b *TestBroker) Candles(symbol string, frequency string, count int) (*df.DataFrame, error) {
|
||||||
|
// Check if we reached the end of the existing data.
|
||||||
|
if b.Data != nil && b.candleCount >= b.Data.NRows() {
|
||||||
|
return b.Data.Copy(), ErrEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch up to the start candles.
|
||||||
|
if b.candleCount < b.StartCandles {
|
||||||
|
b.candleCount = b.StartCandles
|
||||||
|
} else {
|
||||||
|
b.candleCount++
|
||||||
|
}
|
||||||
|
return b.candles(symbol, frequency, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// candles does the same as the public Candles except it doesn't increment b.candleCount so that it can be used
|
||||||
|
// internally to fetch candles without incrementing the count.
|
||||||
|
func (b *TestBroker) candles(symbol string, frequency string, count int) (*df.DataFrame, error) {
|
||||||
if b.DataBroker != nil && b.Data == nil {
|
if b.DataBroker != nil && b.Data == nil {
|
||||||
// Fetch a lot of candles from the broker so we don't keep asking.
|
// 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, Max(count, 1000))
|
||||||
@ -33,37 +60,143 @@ func (b *TestBroker) Candles(symbol string, frequency string, count int) (*df.Da
|
|||||||
return nil, ErrNoData
|
return nil, ErrNoData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we reached the end of the existing data.
|
// TODO: check if count > our rows if we are using a data broker and then fetch more data if so.
|
||||||
if b.candles >= b.Data.NRows() {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch up to the start candles.
|
// Catch up to the start candles.
|
||||||
if b.candles < b.StartCandles {
|
if b.candleCount < b.StartCandles {
|
||||||
b.candles = b.StartCandles
|
b.candleCount = b.StartCandles
|
||||||
} else {
|
|
||||||
b.candles++
|
|
||||||
}
|
}
|
||||||
end := b.candles - 1
|
|
||||||
start := Max(b.candles-count, 0)
|
// We use a Max(b.candleCount, 1) because we want to return at least 1 candle (even if b.candleCount is 0),
|
||||||
|
// which may happen if we call this function before the first call to Candles.
|
||||||
|
end := Max(b.candleCount, 1) - 1
|
||||||
|
start := Max(Max(b.candleCount, 1)-count, 0)
|
||||||
|
|
||||||
return b.Data.Copy(df.Range{Start: &start, End: &end}), nil
|
return b.Data.Copy(df.Range{Start: &start, End: &end}), 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) {
|
||||||
return nil, nil
|
if b.Data == nil { // The dataBroker could have data but nobody has called Candles, yet.
|
||||||
|
if b.DataBroker == nil {
|
||||||
|
return nil, ErrNoData
|
||||||
|
}
|
||||||
|
_, err := b.candles("", "", 1) // Fetch 1 candle.
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeIdx, err := b.Data.NameToColumn("Close")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
price := b.Data.Series[closeIdx].Value(Max(b.candleCount-1, 0)).(float64) // Get the last close price.
|
||||||
|
|
||||||
|
// Instantly fulfill the order.
|
||||||
|
b.Cash -= price * units * LeverageToMargin(b.Leverage)
|
||||||
|
position := &TestPosition{}
|
||||||
|
|
||||||
|
order := &TestOrder{
|
||||||
|
id: strconv.Itoa(rand.Int()),
|
||||||
|
leverage: b.Leverage,
|
||||||
|
position: position,
|
||||||
|
price: price,
|
||||||
|
symbol: symbol,
|
||||||
|
stopLoss: stopLoss,
|
||||||
|
takeProfit: takeProfit,
|
||||||
|
time: time.Now(),
|
||||||
|
orderType: MarketOrder,
|
||||||
|
units: units,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.orders = append(b.orders, order)
|
||||||
|
b.positions = append(b.positions, position)
|
||||||
|
|
||||||
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TestBroker) NAV() float64 {
|
func (b *TestBroker) NAV() float64 {
|
||||||
return b.Cash
|
return b.Cash
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestBroker(dataBroker Broker, data *df.DataFrame, cash, leverage float64, startCandles int) *TestBroker {
|
func (b *TestBroker) Orders() []Order {
|
||||||
|
return b.orders
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestBroker) Positions() []Position {
|
||||||
|
return b.positions
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestBroker(dataBroker Broker, data *df.DataFrame, cash, leverage, spread float64, startCandles int) *TestBroker {
|
||||||
return &TestBroker{
|
return &TestBroker{
|
||||||
DataBroker: dataBroker,
|
DataBroker: dataBroker,
|
||||||
Data: data,
|
Data: data,
|
||||||
Cash: cash,
|
Cash: cash,
|
||||||
Leverage: Max(leverage, 1),
|
Leverage: Max(leverage, 1),
|
||||||
|
Spread: spread,
|
||||||
StartCandles: Max(startCandles-1, 0),
|
StartCandles: Max(startCandles-1, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TestPosition struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestOrder struct {
|
||||||
|
id string
|
||||||
|
leverage float64
|
||||||
|
position *TestPosition
|
||||||
|
price float64
|
||||||
|
symbol string
|
||||||
|
stopLoss float64
|
||||||
|
takeProfit float64
|
||||||
|
time time.Time
|
||||||
|
orderType OrderType
|
||||||
|
units float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Cancel() error {
|
||||||
|
return ErrCancelFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Fulfilled() bool {
|
||||||
|
return o.position != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Id() string {
|
||||||
|
return o.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Leverage() float64 {
|
||||||
|
return o.leverage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Position() Position {
|
||||||
|
return o.position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Price() float64 {
|
||||||
|
return o.price
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Symbol() string {
|
||||||
|
return o.symbol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) StopLoss() float64 {
|
||||||
|
return o.stopLoss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) TakeProfit() float64 {
|
||||||
|
return o.takeProfit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Time() time.Time {
|
||||||
|
return o.time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Type() OrderType {
|
||||||
|
return o.orderType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TestOrder) Units() float64 {
|
||||||
|
return o.units
|
||||||
|
}
|
||||||
|
@ -21,13 +21,14 @@ const testDataCSV = `date,open,high,low,close,volume
|
|||||||
|
|
||||||
func newTestingDataframe() *df.DataFrame {
|
func newTestingDataframe() *df.DataFrame {
|
||||||
data, err := ReadDataCSVFromReader(strings.NewReader(testDataCSV), DataCSVLayout{
|
data, err := ReadDataCSVFromReader(strings.NewReader(testDataCSV), DataCSVLayout{
|
||||||
DateFormat: "2006-01-02",
|
LatestFirst: false,
|
||||||
Date: "date",
|
DateFormat: "2006-01-02",
|
||||||
Open: "open",
|
Date: "date",
|
||||||
High: "high",
|
Open: "open",
|
||||||
Low: "low",
|
High: "high",
|
||||||
Close: "close",
|
Low: "low",
|
||||||
Volume: "volume",
|
Close: "close",
|
||||||
|
Volume: "volume",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -37,7 +38,7 @@ func newTestingDataframe() *df.DataFrame {
|
|||||||
|
|
||||||
func TestBacktestingBrokerCandles(t *testing.T) {
|
func TestBacktestingBrokerCandles(t *testing.T) {
|
||||||
data := newTestingDataframe()
|
data := newTestingDataframe()
|
||||||
broker := NewTestBroker(nil, data, 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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -79,17 +80,17 @@ func TestBacktestingBrokerCandles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestingBrokerFunctions(t *testing.T) {
|
func TestBacktestingBrokerFunctions(t *testing.T) {
|
||||||
broker := NewTestBroker(nil, nil, 100000, 20, 0)
|
broker := NewTestBroker(nil, nil, 100_000, 20, 0, 0)
|
||||||
|
|
||||||
if broker.NAV() != 100000 {
|
if broker.NAV() != 100_000 {
|
||||||
t.Errorf("Expected NAV to be 100000, got %f", broker.NAV())
|
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestingBrokerOrders(t *testing.T) {
|
func TestBacktestingBrokerOrders(t *testing.T) {
|
||||||
broker := NewTestBroker(nil, newTestingDataframe(), 100000, 50, 0)
|
broker := NewTestBroker(nil, newTestingDataframe(), 100_000, 50, 0, 0)
|
||||||
timeBeforeOrder := time.Now()
|
timeBeforeOrder := time.Now()
|
||||||
order, err := broker.MarketOrder("EUR_USD", 1000, 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
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -100,11 +101,11 @@ func TestBacktestingBrokerOrders(t *testing.T) {
|
|||||||
if order.Symbol() != "EUR_USD" {
|
if order.Symbol() != "EUR_USD" {
|
||||||
t.Errorf("Expected symbol to be EUR_USD, got %s", order.Symbol())
|
t.Errorf("Expected symbol to be EUR_USD, got %s", order.Symbol())
|
||||||
}
|
}
|
||||||
if order.Units() != 1000 {
|
if order.Units() != 50_000 {
|
||||||
t.Errorf("Expected units to be 1000, got %f", order.Units())
|
t.Errorf("Expected units to be 50_000, got %f", order.Units())
|
||||||
}
|
}
|
||||||
if order.Price() != 1.15 {
|
if order.Price() != 1.15 {
|
||||||
t.Errorf("Expected open price to be 1.15 (first close), got %f", order.Price())
|
t.Errorf("Expected order price to be 1.15 (first close), got %f", order.Price())
|
||||||
}
|
}
|
||||||
if order.Fulfilled() != true {
|
if order.Fulfilled() != true {
|
||||||
t.Error("Expected order to be fulfilled")
|
t.Error("Expected order to be fulfilled")
|
||||||
|
@ -16,6 +16,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ErrCancelFailed = errors.New("cancel failed")
|
||||||
ErrSymbolNotFound = errors.New("symbol not found")
|
ErrSymbolNotFound = errors.New("symbol not found")
|
||||||
ErrInvalidStopLoss = errors.New("invalid stop loss")
|
ErrInvalidStopLoss = errors.New("invalid stop loss")
|
||||||
ErrInvalidTakeProfit = errors.New("invalid take profit")
|
ErrInvalidTakeProfit = errors.New("invalid take profit")
|
||||||
@ -26,7 +27,7 @@ type Order interface {
|
|||||||
Fulfilled() bool // Fulfilled returns true if the order has been filled with the broker and a position is active.
|
Fulfilled() bool // Fulfilled returns true if the order has been filled with the broker and a position is active.
|
||||||
Id() string // Id returns the unique identifier of the order by the broker.
|
Id() string // Id returns the unique identifier of the order by the broker.
|
||||||
Leverage() float64 // Leverage returns the leverage of the order.
|
Leverage() float64 // Leverage returns the leverage of the order.
|
||||||
Position() *Position // Position returns the position of the order. If the order has not been filled, nil is returned.
|
Position() Position // Position returns the position of the order. If the order has not been filled, nil is returned.
|
||||||
Price() float64 // Price returns the price of the symbol at the time the order was placed.
|
Price() float64 // Price returns the price of the symbol at the time the order was placed.
|
||||||
Symbol() string // Symbol returns the symbol name of the order.
|
Symbol() string // Symbol returns the symbol name of the order.
|
||||||
StopLoss() float64 // StopLoss returns the stop loss price of the order.
|
StopLoss() float64 // StopLoss returns the stop loss price of the order.
|
||||||
@ -44,4 +45,6 @@ type Broker interface {
|
|||||||
Candles(symbol string, frequency string, count int) (*df.DataFrame, error)
|
Candles(symbol string, frequency string, count int) (*df.DataFrame, error)
|
||||||
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
|
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
|
||||||
NAV() float64 // NAV returns the net asset value of the account.
|
NAV() float64 // NAV returns the net asset value of the account.
|
||||||
|
Orders() []Order
|
||||||
|
Positions() []Position
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ func main() {
|
|||||||
// AccountID: "101-001-14983263-001",
|
// AccountID: "101-001-14983263-001",
|
||||||
// DemoAccount: true,
|
// DemoAccount: true,
|
||||||
// }),
|
// }),
|
||||||
Broker: auto.NewTestBroker(nil, data, 10000, 50, 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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user