mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 08:23:51 +00:00
Implemented TestPosition for TestBroker
This commit is contained in:
parent
842870011a
commit
977296152c
111
backtesting.go
111
backtesting.go
@ -35,12 +35,11 @@ func Backtest(trader *Trader) {
|
|||||||
// - PositionModified(Position) - Called when a position changes.
|
// - PositionModified(Position) - Called when a position changes.
|
||||||
type TestBroker struct {
|
type TestBroker struct {
|
||||||
SignalManager
|
SignalManager
|
||||||
DataBroker Broker
|
DataBroker Broker
|
||||||
Data *DataFrame
|
Data *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)
|
Spread float64 // Number of pips to add to the price when buying and subtract when selling. (Forex)
|
||||||
StartCandles int
|
|
||||||
|
|
||||||
candleCount int // The number of candles anyone outside this broker has seen. Also equal to the number of times Candles has been called.
|
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
|
orders []Order
|
||||||
@ -48,27 +47,26 @@ type TestBroker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CandleIndex returns the index of the current candle.
|
// CandleIndex returns the index of the current candle.
|
||||||
func (b *TestBroker) candleIndex() int {
|
func (b *TestBroker) CandleIndex() int {
|
||||||
return Max(b.candleCount-1, 0)
|
return Max(b.candleCount-1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance advances the test broker to the next candle in the input data. This should be done at the end of the
|
||||||
|
// strategy loop.
|
||||||
|
func (b *TestBroker) Advance() {
|
||||||
|
b.candleCount++
|
||||||
|
}
|
||||||
|
|
||||||
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).(*DataFrame), ErrEOF
|
return b.Data.Copy(0, -1).(*DataFrame), ErrEOF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catch up to the start candles.
|
data, err := b.candles(symbol, frequency, count)
|
||||||
if b.candleCount < b.StartCandles {
|
return data, err
|
||||||
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) (*DataFrame, error) {
|
func (b *TestBroker) candles(symbol string, frequency string, count int) (*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.
|
||||||
@ -83,14 +81,7 @@ func (b *TestBroker) candles(symbol string, frequency string, count int) (*DataF
|
|||||||
|
|
||||||
// TODO: check if count > our rows if we are using a data broker and then fetch more data if so.
|
// TODO: check if count > our rows if we are using a data broker and then fetch more data if so.
|
||||||
|
|
||||||
// Catch up to the start candles.
|
end := b.candleCount - 1
|
||||||
if b.candleCount < b.StartCandles {
|
|
||||||
b.candleCount = b.StartCandles
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
start := Max(Max(b.candleCount, 1)-count, 0)
|
||||||
|
|
||||||
return b.Data.Copy(start, end).(*DataFrame), nil
|
return b.Data.Copy(start, end).(*DataFrame), nil
|
||||||
@ -106,16 +97,12 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
price := b.Data.Close(b.candleIndex()) // Get the last close price.
|
price := b.Data.Close(b.CandleIndex()) // Get the last close price.
|
||||||
|
|
||||||
// Instantly fulfill the order.
|
|
||||||
b.Cash -= price * units * LeverageToMargin(b.Leverage)
|
|
||||||
position := &TestPosition{}
|
|
||||||
|
|
||||||
order := &TestOrder{
|
order := &TestOrder{
|
||||||
id: strconv.Itoa(rand.Int()),
|
id: strconv.Itoa(rand.Int()),
|
||||||
leverage: b.Leverage,
|
leverage: b.Leverage,
|
||||||
position: position,
|
position: nil,
|
||||||
price: price,
|
price: price,
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
stopLoss: stopLoss,
|
stopLoss: stopLoss,
|
||||||
@ -125,15 +112,37 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
|
|||||||
units: units,
|
units: units,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instantly fulfill the order.
|
||||||
|
order.position = &TestPosition{
|
||||||
|
broker: b,
|
||||||
|
closed: false,
|
||||||
|
entryPrice: price,
|
||||||
|
id: strconv.Itoa(rand.Int()),
|
||||||
|
leverage: b.Leverage,
|
||||||
|
symbol: symbol,
|
||||||
|
stopLoss: stopLoss,
|
||||||
|
takeProfit: takeProfit,
|
||||||
|
time: time.Now(),
|
||||||
|
units: units,
|
||||||
|
}
|
||||||
|
b.Cash -= order.position.EntryValue()
|
||||||
|
|
||||||
b.orders = append(b.orders, order)
|
b.orders = append(b.orders, order)
|
||||||
b.positions = append(b.positions, position)
|
b.positions = append(b.positions, order.position)
|
||||||
b.SignalEmit("OrderPlaced", order)
|
b.SignalEmit("OrderPlaced", order)
|
||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TestBroker) NAV() float64 {
|
func (b *TestBroker) NAV() float64 {
|
||||||
return b.Cash
|
nav := b.Cash
|
||||||
|
// Add the value of open positions to our NAV.
|
||||||
|
for _, position := range b.positions {
|
||||||
|
if !position.Closed() {
|
||||||
|
nav += position.Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nav
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TestBroker) Orders() []Order {
|
func (b *TestBroker) Orders() []Order {
|
||||||
@ -146,12 +155,12 @@ func (b *TestBroker) Positions() []Position {
|
|||||||
|
|
||||||
func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread float64, startCandles int) *TestBroker {
|
func NewTestBroker(dataBroker Broker, data *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,
|
Spread: spread,
|
||||||
StartCandles: Max(startCandles-1, 0),
|
candleCount: Max(startCandles, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +168,7 @@ type TestPosition struct {
|
|||||||
broker *TestBroker
|
broker *TestBroker
|
||||||
closed bool
|
closed bool
|
||||||
entryPrice float64
|
entryPrice float64
|
||||||
|
closePrice float64 // If zero, then position has not been closed.
|
||||||
id string
|
id string
|
||||||
leverage float64
|
leverage float64
|
||||||
symbol string
|
symbol string
|
||||||
@ -173,6 +183,8 @@ func (p *TestPosition) Close() error {
|
|||||||
return ErrPositionClosed
|
return ErrPositionClosed
|
||||||
}
|
}
|
||||||
p.closed = true
|
p.closed = true
|
||||||
|
p.closePrice = p.broker.Data.Close(p.broker.CandleIndex()) - p.broker.Spread // Get the last close price.
|
||||||
|
p.broker.Cash += p.Value() // Return the value of the position to the broker.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +196,14 @@ func (p *TestPosition) EntryPrice() float64 {
|
|||||||
return p.entryPrice
|
return p.entryPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *TestPosition) ClosePrice() float64 {
|
||||||
|
return p.closePrice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TestPosition) EntryValue() float64 {
|
||||||
|
return p.entryPrice * p.units
|
||||||
|
}
|
||||||
|
|
||||||
func (p *TestPosition) Id() string {
|
func (p *TestPosition) Id() string {
|
||||||
return p.id
|
return p.id
|
||||||
}
|
}
|
||||||
@ -193,10 +213,7 @@ func (p *TestPosition) Leverage() float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *TestPosition) PL() float64 {
|
func (p *TestPosition) PL() float64 {
|
||||||
price := p.broker.Data.Close(p.broker.candleIndex()) + p.broker.Spread
|
return p.Value() - p.EntryValue()
|
||||||
priceDiff := price - p.entryPrice
|
|
||||||
units := priceDiff * p.units * LeverageToMargin(p.leverage)
|
|
||||||
return units * price
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TestPosition) Symbol() string {
|
func (p *TestPosition) Symbol() string {
|
||||||
@ -219,6 +236,14 @@ func (p *TestPosition) Units() float64 {
|
|||||||
return p.units
|
return p.units
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *TestPosition) Value() float64 {
|
||||||
|
if p.closed {
|
||||||
|
return p.closePrice * p.units
|
||||||
|
}
|
||||||
|
bid := p.broker.Data.Close(p.broker.CandleIndex()) - p.broker.Spread
|
||||||
|
return bid * p.units
|
||||||
|
}
|
||||||
|
|
||||||
type TestOrder struct {
|
type TestOrder struct {
|
||||||
id string
|
id string
|
||||||
leverage float64
|
leverage float64
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package autotrader
|
package autotrader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -49,6 +50,7 @@ func TestBacktestingBrokerCandles(t *testing.T) {
|
|||||||
t.Errorf("Expected first candle to be 2022-01-01, got %s", candles.Date(0))
|
t.Errorf("Expected first candle to be 2022-01-01, got %s", candles.Date(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
broker.Advance()
|
||||||
candles, err = broker.Candles("EUR_USD", "D", 3)
|
candles, err = broker.Candles("EUR_USD", "D", 3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -60,10 +62,11 @@ func TestBacktestingBrokerCandles(t *testing.T) {
|
|||||||
t.Errorf("Expected second candle to be 2022-01-02, got %s", candles.Date(1))
|
t.Errorf("Expected second candle to be 2022-01-02, got %s", candles.Date(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 7; i++ { // 7 because we want to call broker.Candles 9 times total
|
for i := 0; i < 7; i++ { // 6 because we want to call broker.Candles 9 times total
|
||||||
|
broker.Advance()
|
||||||
candles, err = broker.Candles("EUR_USD", "D", 5)
|
candles, err = broker.Candles("EUR_USD", "D", 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Got an error on iteration %d: %v", i, err)
|
t.Fatalf("Got an error on iteration %d: %v (called Candles %d times)", i, err, 2+i+1)
|
||||||
}
|
}
|
||||||
if candles == nil {
|
if candles == nil {
|
||||||
t.Errorf("Candles is nil on iteration %d", i+1)
|
t.Errorf("Candles is nil on iteration %d", i+1)
|
||||||
@ -88,6 +91,7 @@ func TestBacktestingBrokerFunctions(t *testing.T) {
|
|||||||
func TestBacktestingBrokerOrders(t *testing.T) {
|
func TestBacktestingBrokerOrders(t *testing.T) {
|
||||||
data := 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
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,4 +128,52 @@ func TestBacktestingBrokerOrders(t *testing.T) {
|
|||||||
if order.Type() != MarketOrder {
|
if order.Type() != MarketOrder {
|
||||||
t.Errorf("Expected order type to be MarketOrder, got %s", order.Type())
|
t.Errorf("Expected order type to be MarketOrder, got %s", order.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
position := order.Position()
|
||||||
|
if position == nil {
|
||||||
|
t.Fatal("Position is nil")
|
||||||
|
}
|
||||||
|
if position.Symbol() != "EUR_USD" {
|
||||||
|
t.Errorf("Expected symbol to be EUR_USD, got %s", position.Symbol())
|
||||||
|
}
|
||||||
|
if position.Units() != 50_000 {
|
||||||
|
t.Errorf("Expected units to be 50_000, got %f", position.Units())
|
||||||
|
}
|
||||||
|
if position.EntryPrice() != 1.15 {
|
||||||
|
t.Errorf("Expected entry price to be 1.15 (first close), got %f", position.EntryPrice())
|
||||||
|
}
|
||||||
|
if position.Time().Before(timeBeforeOrder) {
|
||||||
|
t.Error("Expected position time to be after timeBeforeOrder")
|
||||||
|
}
|
||||||
|
if position.Leverage() != 50 {
|
||||||
|
t.Errorf("Expected leverage to be 50, got %f", position.Leverage())
|
||||||
|
}
|
||||||
|
if position.StopLoss() != 0 {
|
||||||
|
t.Errorf("Expected stop loss to be 0, got %f", position.StopLoss())
|
||||||
|
}
|
||||||
|
if position.TakeProfit() != 0 {
|
||||||
|
t.Errorf("Expected take profit to be 0, got %f", position.TakeProfit())
|
||||||
|
}
|
||||||
|
|
||||||
|
if broker.NAV() != 100_000 { // NAV should not change until the next candle
|
||||||
|
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
|
||||||
|
}
|
||||||
|
|
||||||
|
broker.Advance() // Advance broker to the next candle
|
||||||
|
if math.Round(position.PL()) != 2500 { // (1.2-1.15) * 50_000 = 2500
|
||||||
|
t.Errorf("Expected position PL to be 2500, got %f", position.PL())
|
||||||
|
}
|
||||||
|
if math.Round(broker.NAV()) != 102_500 {
|
||||||
|
t.Errorf("Expected NAV to be 102_500, got %f", broker.NAV())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test closing positions.
|
||||||
|
position.Close()
|
||||||
|
if position.Closed() != true {
|
||||||
|
t.Error("Expected position to be closed")
|
||||||
|
}
|
||||||
|
broker.Advance()
|
||||||
|
if broker.NAV() != 102_500 {
|
||||||
|
t.Errorf("Expected NAV to still be 102_500, got %f", broker.NAV())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,9 @@ type Order interface {
|
|||||||
type Position 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.
|
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.
|
Closed() bool // Closed returns true if the position has been closed with the broker.
|
||||||
|
ClosePrice() float64 // ClosePrice returns the price of the symbol at the time the position was closed. May be zero if the position is still open.
|
||||||
EntryPrice() float64 // EntryPrice returns the price of the symbol at the time the position was opened.
|
EntryPrice() float64 // EntryPrice returns the price of the symbol at the time the position was opened.
|
||||||
|
EntryValue() float64 // EntryValue returns the value of the position at the time it was opened.
|
||||||
Id() string // Id returns the unique identifier of the position by the broker.
|
Id() string // Id returns the unique identifier of the position by the broker.
|
||||||
Leverage() float64 // Leverage returns the leverage of the position.
|
Leverage() float64 // Leverage returns the leverage of the position.
|
||||||
PL() float64 // PL returns the profit or loss of the position.
|
PL() float64 // PL returns the profit or loss of the position.
|
||||||
@ -47,6 +49,7 @@ type Position interface {
|
|||||||
TakeProfit() float64 // TakeProfit returns the take profit 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.
|
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.
|
Units() float64 // Units returns the number of units purchased or sold by the position.
|
||||||
|
Value() float64 // Value returns the value of the position at the current price.
|
||||||
}
|
}
|
||||||
|
|
||||||
type Broker interface {
|
type Broker interface {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user