mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 08:23:51 +00:00
Slippage and more statistics
This commit is contained in:
parent
565aa6c9fd
commit
5a0a4d0c33
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-echarts/go-echarts/v2/charts"
|
"github.com/go-echarts/go-echarts/v2/charts"
|
||||||
@ -39,6 +40,29 @@ func Backtest(trader *Trader) {
|
|||||||
log.Printf("Backtest completed on %d candles. Opening report...\n", trader.Stats().Dated.Len())
|
log.Printf("Backtest completed on %d candles. Opening report...\n", trader.Stats().Dated.Len())
|
||||||
stats := trader.Stats()
|
stats := trader.Stats()
|
||||||
|
|
||||||
|
// Divide net profit by maximum drawdown to get the profit factor.
|
||||||
|
var maxDrawdown float64
|
||||||
|
stats.Dated.Series("Drawdown").ForEach(func(i int, val any) {
|
||||||
|
f := val.(float64)
|
||||||
|
if f > maxDrawdown {
|
||||||
|
maxDrawdown = f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
profit := stats.Dated.Float("Profit", -1)
|
||||||
|
profitFactor := stats.Dated.Float("Profit", -1) / maxDrawdown
|
||||||
|
maxDrawdownPct := 100 * maxDrawdown / stats.Dated.Float("Equity", 0)
|
||||||
|
|
||||||
|
// Print a summary of the statistics to the console.
|
||||||
|
{
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
fmt.Fprintf(w, "Net Profit:\t$%.2f (%.2f%%)\t\n", profit, 100*profit/stats.Dated.Float("Equity", 0))
|
||||||
|
fmt.Fprintf(w, "Profit Factor:\t%.2f\t\n", profitFactor)
|
||||||
|
fmt.Fprintf(w, "Max Drawdown:\t$%.2f (%.2f%%)\t\n", maxDrawdown, maxDrawdownPct)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
// Pick a datetime layout based on the frequency.
|
// Pick a datetime layout based on the frequency.
|
||||||
dateLayout := time.DateTime
|
dateLayout := time.DateTime
|
||||||
if strings.Contains(trader.Frequency, "S") { // Seconds
|
if strings.Contains(trader.Frequency, "S") { // Seconds
|
||||||
@ -302,12 +326,25 @@ type TestBroker struct {
|
|||||||
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)
|
||||||
|
Slippage float64 // A percentage of the price to add when buying and subtract when selling.
|
||||||
|
|
||||||
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
|
||||||
positions []Position
|
positions []Position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread float64, startCandles int) *TestBroker {
|
||||||
|
return &TestBroker{
|
||||||
|
DataBroker: dataBroker,
|
||||||
|
Data: data,
|
||||||
|
Cash: cash,
|
||||||
|
Leverage: Max(leverage, 1),
|
||||||
|
Spread: spread,
|
||||||
|
Slippage: 0.005, // Price +/- 0.5%
|
||||||
|
candleCount: Max(startCandles, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@ -321,6 +358,16 @@ func (b *TestBroker) Advance() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bid returns the price a seller pays for the current candle.
|
||||||
|
func (b *TestBroker) Bid(_ string) float64 {
|
||||||
|
return b.Data.Close(b.CandleIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask returns the price a buyer pays for the current candle.
|
||||||
|
func (b *TestBroker) Ask(_ string) float64 {
|
||||||
|
return b.Data.Close(b.CandleIndex()) + b.Spread
|
||||||
|
}
|
||||||
|
|
||||||
// Candles returns the last count candles for the given symbol and frequency. If count is greater than the number of candles, then a dataframe with zero rows is returned.
|
// Candles returns the last count candles for the given symbol and frequency. If count is greater than the number of candles, then a dataframe with zero rows is returned.
|
||||||
//
|
//
|
||||||
// If the TestBroker has a data broker set, then it will use that to get candles. Otherwise, it will return the candles from the data that was set. The first call to Candles will fetch candles from the data broker if it is set, so it is recommended to set the data broker before the first call to Candles and to call Candles the first time with the number of candles you want to fetch.
|
// If the TestBroker has a data broker set, then it will use that to get candles. Otherwise, it will return the candles from the data that was set. The first call to Candles will fetch candles from the data broker if it is set, so it is recommended to set the data broker before the first call to Candles and to call Candles the first time with the number of candles you want to fetch.
|
||||||
@ -352,7 +399,16 @@ 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.
|
|
||||||
|
var price float64
|
||||||
|
if units < 0 {
|
||||||
|
price = b.Bid("")
|
||||||
|
} else {
|
||||||
|
price = b.Ask("")
|
||||||
|
}
|
||||||
|
|
||||||
|
slippage := rand.Float64() * b.Slippage * price
|
||||||
|
price += slippage - slippage/2 // Get a slippage as +/- 50% of the slippage.
|
||||||
|
|
||||||
order := &TestOrder{
|
order := &TestOrder{
|
||||||
id: strconv.Itoa(rand.Int()),
|
id: strconv.Itoa(rand.Int()),
|
||||||
@ -436,17 +492,6 @@ func (b *TestBroker) Positions() []Position {
|
|||||||
return b.positions
|
return b.positions
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread float64, startCandles int) *TestBroker {
|
|
||||||
return &TestBroker{
|
|
||||||
DataBroker: dataBroker,
|
|
||||||
Data: data,
|
|
||||||
Cash: cash,
|
|
||||||
Leverage: Max(leverage, 1),
|
|
||||||
Spread: spread,
|
|
||||||
candleCount: Max(startCandles, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestPosition struct {
|
type TestPosition struct {
|
||||||
broker *TestBroker
|
broker *TestBroker
|
||||||
closed bool
|
closed bool
|
||||||
@ -466,8 +511,12 @@ 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.
|
if p.units < 0 {
|
||||||
p.broker.Cash += p.Value() // Return the value of the position to the broker.
|
p.closePrice = p.broker.Ask("") // Ask because we are short so we have to buy.
|
||||||
|
} else {
|
||||||
|
p.closePrice = p.broker.Bid("") // Ask because we are long so we have to sell.
|
||||||
|
}
|
||||||
|
p.broker.Cash += p.Value() // Return the value of the position to the broker.
|
||||||
p.broker.SignalEmit("PositionClosed", p)
|
p.broker.SignalEmit("PositionClosed", p)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -524,8 +573,13 @@ func (p *TestPosition) Value() float64 {
|
|||||||
if p.closed {
|
if p.closed {
|
||||||
return p.closePrice * p.units
|
return p.closePrice * p.units
|
||||||
}
|
}
|
||||||
bid := p.broker.Data.Close(p.broker.CandleIndex()) - p.broker.Spread
|
var price float64
|
||||||
return bid * p.units
|
if p.units < 0 {
|
||||||
|
price = p.broker.Ask("")
|
||||||
|
} else {
|
||||||
|
price = p.broker.Bid("")
|
||||||
|
}
|
||||||
|
return price * p.units
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestOrder struct {
|
type TestOrder struct {
|
||||||
|
@ -90,6 +90,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)
|
||||||
|
broker.Slippage = 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
|
||||||
|
@ -57,6 +57,8 @@ type Position interface {
|
|||||||
// - PositionClosed(Position) - Emitted after a position is closed either manually or automatically.
|
// - PositionClosed(Position) - Emitted after a position is closed either manually or automatically.
|
||||||
type Broker interface {
|
type Broker interface {
|
||||||
Signaler
|
Signaler
|
||||||
|
Bid(symbol string) float64 // Bid returns the sell price of the symbol.
|
||||||
|
Ask(symbol string) float64 // Ask returns the buy price of the symbol, which is typically higher than the sell price.
|
||||||
// Candles returns a dataframe of candles for the given symbol, frequency, and count by querying the broker.
|
// Candles returns a dataframe of candles for the given symbol, frequency, and count by querying the broker.
|
||||||
Candles(symbol, frequency string, count int) (*DataFrame, error)
|
Candles(symbol, frequency string, count int) (*DataFrame, error)
|
||||||
MarketOrder(symbol string, units, stopLoss, takeProfit float64) (Order, error)
|
MarketOrder(symbol string, units, stopLoss, takeProfit float64) (Order, error)
|
||||||
|
@ -42,6 +42,14 @@ func NewOandaBroker(token, accountID string, practice bool) *OandaBroker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) Bid(symbol string) float64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) Ask(symbol string) float64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (b *OandaBroker) Candles(symbol, frequency string, count int) (*auto.DataFrame, error) {
|
func (b *OandaBroker) Candles(symbol, frequency string, count int) (*auto.DataFrame, error) {
|
||||||
req, err := http.NewRequest("GET", b.baseUrl+"/v3/accounts/"+b.accountID+"/instruments/"+symbol+"/candles", nil)
|
req, err := http.NewRequest("GET", b.baseUrl+"/v3/accounts/"+b.accountID+"/instruments/"+symbol+"/candles", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
18
series.go
18
series.go
@ -46,6 +46,7 @@ type Series interface {
|
|||||||
Filter(f func(i int, val any) bool) Series // Where returns a new Series with only the values that return true for the given function.
|
Filter(f func(i int, val any) bool) Series // Where returns a new Series with only the values that return true for the given function.
|
||||||
Map(f func(i int, val any) any) Series // Map returns a new Series with the values modified by the given function.
|
Map(f func(i int, val any) any) Series // Map returns a new Series with the values modified by the given function.
|
||||||
MapReverse(f func(i int, val any) any) Series // MapReverse is the same as Map but it starts from the last item and works backwards.
|
MapReverse(f func(i int, val any) any) Series // MapReverse is the same as Map but it starts from the last item and works backwards.
|
||||||
|
ForEach(f func(i int, val any)) Series // ForEach calls f for each item in the Series.
|
||||||
|
|
||||||
// Statistical functions.
|
// Statistical functions.
|
||||||
|
|
||||||
@ -128,6 +129,11 @@ func (s *AppliedSeries) MapReverse(f func(i int, val any) any) Series {
|
|||||||
return NewAppliedSeries(s.Series.MapReverse(f), s.apply)
|
return NewAppliedSeries(s.Series.MapReverse(f), s.apply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AppliedSeries) ForEach(f func(i int, val any)) Series {
|
||||||
|
_ = s.Series.ForEach(f)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AppliedSeries) WithValueFunc(value func(i int) any) Series {
|
func (s *AppliedSeries) WithValueFunc(value func(i int) any) Series {
|
||||||
return &AppliedSeries{Series: s.Series.WithValueFunc(value), apply: s.apply}
|
return &AppliedSeries{Series: s.Series.WithValueFunc(value), apply: s.apply}
|
||||||
}
|
}
|
||||||
@ -189,6 +195,11 @@ func (s *RollingSeries) MapReverse(f func(i int, val any) any) Series {
|
|||||||
return NewRollingSeries(s.Series.MapReverse(f), s.period)
|
return NewRollingSeries(s.Series.MapReverse(f), s.period)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RollingSeries) ForEach(f func(i int, val any)) Series {
|
||||||
|
_ = s.Series.ForEach(f)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// Average is an alias for Mean.
|
// Average is an alias for Mean.
|
||||||
func (s *RollingSeries) Average() *AppliedSeries {
|
func (s *RollingSeries) Average() *AppliedSeries {
|
||||||
return s.Mean()
|
return s.Mean()
|
||||||
@ -531,6 +542,13 @@ func (s *DataSeries) MapReverse(f func(i int, val any) any) Series {
|
|||||||
return series
|
return series
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DataSeries) ForEach(f func(i int, val any)) Series {
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
f(i, s.value(i))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DataSeries) Rolling(period int) *RollingSeries {
|
func (s *DataSeries) Rolling(period int) *RollingSeries {
|
||||||
return NewRollingSeries(s, period)
|
return NewRollingSeries(s, period)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user