mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 08:23:51 +00:00
Display stats on returns
This commit is contained in:
parent
8fcd7f5cc9
commit
ef9659b450
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/go-echarts/go-echarts/v2/components"
|
"github.com/go-echarts/go-echarts/v2/components"
|
||||||
"github.com/go-echarts/go-echarts/v2/opts"
|
"github.com/go-echarts/go-echarts/v2/opts"
|
||||||
"golang.org/x/exp/rand"
|
"golang.org/x/exp/rand"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -23,6 +24,7 @@ var (
|
|||||||
func Backtest(trader *Trader) {
|
func Backtest(trader *Trader) {
|
||||||
switch broker := trader.Broker.(type) {
|
switch broker := trader.Broker.(type) {
|
||||||
case *TestBroker:
|
case *TestBroker:
|
||||||
|
rand.Seed(uint64(time.Now().UnixNano()))
|
||||||
trader.Init() // Initialize the trader and strategy.
|
trader.Init() // Initialize the trader and strategy.
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for !trader.EOF {
|
for !trader.EOF {
|
||||||
@ -30,20 +32,81 @@ func Backtest(trader *Trader) {
|
|||||||
broker.Advance() // Give the trader access to the next candlestick.
|
broker.Advance() // Give the trader access to the next candlestick.
|
||||||
}
|
}
|
||||||
log.Println("Backtest complete. Opening report...")
|
log.Println("Backtest complete. Opening report...")
|
||||||
|
stats := trader.Stats()
|
||||||
|
|
||||||
page := components.NewPage()
|
page := components.NewPage()
|
||||||
|
|
||||||
// Create a new line chart based on account equity and add it to the page.
|
// Create a new line balChart based on account equity and add it to the page.
|
||||||
chart := charts.NewLine()
|
balChart := charts.NewLine()
|
||||||
chart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
balChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||||
Title: fmt.Sprintf("Backtest (%s)", time.Now().Format(time.DateTime)),
|
Title: "Balance",
|
||||||
Subtitle: fmt.Sprintf("%s %s %T (took %.2f seconds)", trader.Symbol, trader.Frequency, trader.Strategy, time.Since(start).Seconds()),
|
Subtitle: fmt.Sprintf("%s %s %T (took %.2f seconds) %s", trader.Symbol, trader.Frequency, trader.Strategy, time.Since(start).Seconds(), time.Now().Format(time.DateTime)),
|
||||||
}))
|
}))
|
||||||
chart.SetXAxis(seriesStringArray(trader.Stats().Dates())).
|
balChart.SetXAxis(seriesStringArray(stats.Dated.Dates())).
|
||||||
AddSeries("Equity", lineDataFromSeries(trader.Stats().Series("Equity"))).
|
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity"))).
|
||||||
AddSeries("Drawdown", lineDataFromSeries(trader.Stats().Series("Drawdown")))
|
AddSeries("Drawdown", lineDataFromSeries(stats.Dated.Series("Drawdown")))
|
||||||
|
|
||||||
page.AddCharts(chart)
|
// Sort Returns by value.
|
||||||
|
// Plot returns as a bar chart.
|
||||||
|
returnsSeries := stats.Dated.Series("Returns")
|
||||||
|
returns := make([]float64, 0, returnsSeries.Len())
|
||||||
|
// returns := stats.Dated.Series("Returns").Values()
|
||||||
|
// Remove nil values.
|
||||||
|
for i := 0; i < returnsSeries.Len(); i++ {
|
||||||
|
r := returnsSeries.Value(i)
|
||||||
|
if r != nil {
|
||||||
|
returns = append(returns, r.(float64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort the returns.
|
||||||
|
slices.Sort(returns)
|
||||||
|
// Create the X axis labels for the returns chart based on length of the returns slice.
|
||||||
|
returnsLabels := make([]int, len(returns))
|
||||||
|
for i := range returns {
|
||||||
|
returnsLabels[i] = i + 1
|
||||||
|
}
|
||||||
|
returnsBars := make([]opts.BarData, len(returns))
|
||||||
|
for i, r := range returns {
|
||||||
|
returnsBars[i] = opts.BarData{Value: r}
|
||||||
|
if r < 0 {
|
||||||
|
log.Println("Negative return:", r, "at index", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var avg float64
|
||||||
|
for _, r := range returns {
|
||||||
|
avg += r
|
||||||
|
}
|
||||||
|
avg /= float64(len(returns))
|
||||||
|
returnsAverage := make([]opts.LineData, len(returns))
|
||||||
|
for i := range returnsAverage {
|
||||||
|
returnsAverage[i] = opts.LineData{Value: avg}
|
||||||
|
}
|
||||||
|
|
||||||
|
returnsChart := charts.NewBar()
|
||||||
|
returnsChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||||
|
Title: "Returns",
|
||||||
|
Subtitle: fmt.Sprintf("Average: $%.2f", avg),
|
||||||
|
}))
|
||||||
|
returnsChart.SetXAxis(returnsLabels).
|
||||||
|
AddSeries("Returns", returnsBars)
|
||||||
|
|
||||||
|
returnsChartAvg := charts.NewLine()
|
||||||
|
returnsChartAvg.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||||
|
Title: "Average Returns",
|
||||||
|
}))
|
||||||
|
returnsChartAvg.SetXAxis(returnsLabels).
|
||||||
|
AddSeries("Average", returnsAverage, func(s *charts.SingleSeries) {
|
||||||
|
s.LineStyle = &opts.LineStyle{
|
||||||
|
Width: 2,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
returnsChart.Overlap(returnsChartAvg)
|
||||||
|
|
||||||
|
// TODO: Use Radar to display performance metrics.
|
||||||
|
|
||||||
|
// Add all the charts in the desired order.
|
||||||
|
page.PageTitle = "Backtest Report"
|
||||||
|
page.AddCharts(balChart, returnsChart)
|
||||||
|
|
||||||
// Draw the page to a file.
|
// Draw the page to a file.
|
||||||
f, err := os.Create("backtest.html")
|
f, err := os.Create("backtest.html")
|
||||||
@ -62,7 +125,21 @@ func Backtest(trader *Trader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func barDataFromSeries(s Series) []opts.BarData {
|
||||||
|
if s == nil || s.Len() == 0 {
|
||||||
|
return []opts.BarData{}
|
||||||
|
}
|
||||||
|
data := make([]opts.BarData, s.Len())
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
data[i] = opts.BarData{Value: s.Value(i)}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
func lineDataFromSeries(s Series) []opts.LineData {
|
func lineDataFromSeries(s Series) []opts.LineData {
|
||||||
|
if s == nil || s.Len() == 0 {
|
||||||
|
return []opts.LineData{}
|
||||||
|
}
|
||||||
data := make([]opts.LineData, s.Len())
|
data := make([]opts.LineData, s.Len())
|
||||||
for i := 0; i < s.Len(); i++ {
|
for i := 0; i < s.Len(); i++ {
|
||||||
data[i] = opts.LineData{Value: s.Value(i)}
|
data[i] = opts.LineData{Value: s.Value(i)}
|
||||||
@ -280,6 +357,7 @@ func (p *TestPosition) Close() error {
|
|||||||
p.closed = true
|
p.closed = true
|
||||||
p.closePrice = p.broker.Data.Close(p.broker.CandleIndex()) - p.broker.Spread // Get the last close price.
|
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.
|
p.broker.Cash += p.Value() // Return the value of the position to the broker.
|
||||||
|
p.broker.SignalEmit("PositionClosed", p)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,11 @@ type Position interface {
|
|||||||
Value() float64 // Value returns the value of the position at the current price.
|
Value() float64 // Value returns the value of the position at the current price.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broker is an interface that defines the methods that a broker must implement to report symbol data and place orders, etc. All Broker implementations must also implement the Signaler interface and emit the following functions when necessary:
|
||||||
|
//
|
||||||
|
// - PositionClosed(Position) - Emitted after a position is closed either manually or automatically.
|
||||||
type Broker interface {
|
type Broker interface {
|
||||||
|
Signaler
|
||||||
// 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 string, frequency string, count int) (*DataFrame, error)
|
Candles(symbol string, frequency string, count int) (*DataFrame, error)
|
||||||
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
|
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
|
||||||
|
@ -15,18 +15,13 @@ func (s *SMAStrategy) Next(t *auto.Trader) {
|
|||||||
sma1 := t.Data().Closes().Rolling(s.period1).Mean()
|
sma1 := t.Data().Closes().Rolling(s.period1).Mean()
|
||||||
sma2 := t.Data().Closes().Rolling(s.period2).Mean()
|
sma2 := t.Data().Closes().Rolling(s.period2).Mean()
|
||||||
// If the shorter SMA crosses above the longer SMA, buy.
|
// If the shorter SMA crosses above the longer SMA, buy.
|
||||||
if crossover(sma1, sma2) {
|
if auto.Crossover(sma1, sma2) {
|
||||||
t.Buy(1000)
|
t.Buy(1000)
|
||||||
} else if crossover(sma2, sma1) {
|
} else if auto.Crossover(sma2, sma1) {
|
||||||
t.Sell(1000)
|
t.Sell(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// crossover returns true if s1 crosses above s2 at the latest float.
|
|
||||||
func crossover(s1, s2 auto.Series) bool {
|
|
||||||
return s1.Float(-1) > s2.Float(-1) && s1.Float(-2) <= s2.Float(-2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
data, err := auto.EURUSD()
|
data, err := auto.EURUSD()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
13
data.go
13
data.go
@ -35,6 +35,7 @@ type Series interface {
|
|||||||
|
|
||||||
// Writing data.
|
// Writing data.
|
||||||
SetName(name string) Series
|
SetName(name string) Series
|
||||||
|
SetValue(i int, val interface{}) Series
|
||||||
Push(val interface{}) Series
|
Push(val interface{}) Series
|
||||||
|
|
||||||
// Statistical functions.
|
// Statistical functions.
|
||||||
@ -321,6 +322,13 @@ func (s *DataSeries) Push(value interface{}) Series {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DataSeries) SetValue(i int, val interface{}) Series {
|
||||||
|
if s.data != nil {
|
||||||
|
s.data.Update(EasyIndex(i, s.Len()), val)
|
||||||
|
}
|
||||||
|
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
|
||||||
@ -335,10 +343,11 @@ func (s *DataSeries) ValueRange(start, end int) []interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
start = EasyIndex(start, s.Len())
|
start = EasyIndex(start, s.Len())
|
||||||
|
if end < 0 {
|
||||||
|
end = s.Len() - 1
|
||||||
|
}
|
||||||
if start < 0 || start >= s.Len() || end >= s.Len() || start > end {
|
if start < 0 || start >= s.Len() || end >= s.Len() || start > end {
|
||||||
return nil
|
return nil
|
||||||
} else if end < 0 {
|
|
||||||
end = s.Len() - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]interface{}, end-start+1)
|
items := make([]interface{}, end-start+1)
|
||||||
|
35
trader.go
35
trader.go
@ -12,6 +12,12 @@ import (
|
|||||||
"github.com/rocketlaunchr/dataframe-go"
|
"github.com/rocketlaunchr/dataframe-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Performance (financial) reporting and statistics.
|
||||||
|
type TraderStats struct {
|
||||||
|
Dated *DataFrame
|
||||||
|
returnsThisCandle float64
|
||||||
|
}
|
||||||
|
|
||||||
// Trader acts as the primary interface to the broker and strategy. To the strategy, it provides all the information
|
// Trader acts as the primary interface to the broker and strategy. To the strategy, it provides all the information
|
||||||
// about the current state of the market and the portfolio. To the broker, it provides the orders to be executed and
|
// about the current state of the market and the portfolio. To the broker, it provides the orders to be executed and
|
||||||
// requests for the current state of the portfolio.
|
// requests for the current state of the portfolio.
|
||||||
@ -26,14 +32,14 @@ type Trader struct {
|
|||||||
|
|
||||||
data *DataFrame
|
data *DataFrame
|
||||||
sched *gocron.Scheduler
|
sched *gocron.Scheduler
|
||||||
stats *DataFrame // Performance (financial) reporting and statistics.
|
stats *TraderStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) Data() *DataFrame {
|
func (t *Trader) Data() *DataFrame {
|
||||||
return t.data
|
return t.data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) Stats() *DataFrame {
|
func (t *Trader) Stats() *TraderStats {
|
||||||
return t.stats
|
return t.stats
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,11 +85,16 @@ func (t *Trader) Run() {
|
|||||||
|
|
||||||
func (t *Trader) Init() {
|
func (t *Trader) Init() {
|
||||||
t.Strategy.Init(t)
|
t.Strategy.Init(t)
|
||||||
t.stats = NewDataFrame(
|
t.stats.Dated = NewDataFrame(
|
||||||
NewDataSeries(dataframe.NewSeriesTime("Date", nil)),
|
NewDataSeries(dataframe.NewSeriesTime("Date", nil)),
|
||||||
NewDataSeries(dataframe.NewSeriesFloat64("Equity", nil)),
|
NewDataSeries(dataframe.NewSeriesFloat64("Equity", nil)),
|
||||||
NewDataSeries(dataframe.NewSeriesFloat64("Drawdown", nil)),
|
NewDataSeries(dataframe.NewSeriesFloat64("Drawdown", nil)),
|
||||||
|
NewDataSeries(dataframe.NewSeriesFloat64("Returns", nil)),
|
||||||
)
|
)
|
||||||
|
t.Broker.SignalConnect("PositionClosed", func(args ...interface{}) {
|
||||||
|
position := args[0].(Position)
|
||||||
|
t.stats.returnsThisCandle += position.PL()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tick updates the current state of the market and runs the strategy.
|
// Tick updates the current state of the market and runs the strategy.
|
||||||
@ -93,19 +104,27 @@ func (t *Trader) Tick() {
|
|||||||
t.Strategy.Next(t) // Run the strategy.
|
t.Strategy.Next(t) // Run the strategy.
|
||||||
|
|
||||||
// Update the stats.
|
// Update the stats.
|
||||||
t.stats.PushValues(map[string]interface{}{
|
t.stats.Dated.PushValues(map[string]interface{}{
|
||||||
"Date": t.data.Date(-1),
|
"Date": t.data.Date(-1),
|
||||||
"Equity": t.Broker.NAV(),
|
"Equity": t.Broker.NAV(),
|
||||||
"Drawdown": func() float64 {
|
"Drawdown": func() float64 {
|
||||||
var bal float64
|
var bal float64
|
||||||
if t.stats.Len() > 0 {
|
if t.stats.Dated.Len() > 0 {
|
||||||
bal = t.stats.Float("Equity", 0) // Take starting balance
|
bal = t.stats.Dated.Float("Equity", 0) // Take starting balance
|
||||||
} else {
|
} else {
|
||||||
bal = t.Broker.NAV()
|
bal = t.Broker.NAV() // Take current balance for first value
|
||||||
}
|
}
|
||||||
return Max(bal-t.Broker.NAV(), 0)
|
return Max(bal-t.Broker.NAV(), 0)
|
||||||
}(),
|
}(),
|
||||||
|
"Returns": func() interface{} {
|
||||||
|
if t.stats.returnsThisCandle != 0 {
|
||||||
|
return t.stats.returnsThisCandle
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}(),
|
||||||
})
|
})
|
||||||
|
t.stats.returnsThisCandle = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) fetchData() {
|
func (t *Trader) fetchData() {
|
||||||
@ -165,6 +184,6 @@ func NewTrader(config TraderConfig) *Trader {
|
|||||||
Frequency: config.Frequency,
|
Frequency: config.Frequency,
|
||||||
CandlesToKeep: config.CandlesToKeep,
|
CandlesToKeep: config.CandlesToKeep,
|
||||||
Log: logger,
|
Log: logger,
|
||||||
stats: NewDataFrame(),
|
stats: &TraderStats{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
utils.go
5
utils.go
@ -9,6 +9,11 @@ import (
|
|||||||
|
|
||||||
const floatComparisonTolerance = float64(1e-6)
|
const floatComparisonTolerance = float64(1e-6)
|
||||||
|
|
||||||
|
// Crossover returns true if the latest a value crosses above the latest b value, but only if it just happened. For example, if a series is [1, 2, 3, 4, 5] and b series is [1, 2, 3, 4, 3], then Crossover(a, b) returns false because the latest a value is 5 and the latest b value is 3. However, if a series is [1, 2, 3, 4, 5] and b series is [1, 2, 3, 4, 6], then Crossover(a, b) returns true because the latest a value is 5 and the latest b value is 6
|
||||||
|
func Crossover(a, b Series) bool {
|
||||||
|
return a.Float(-1) > b.Float(-1) && a.Float(-2) <= b.Float(-2)
|
||||||
|
}
|
||||||
|
|
||||||
// EasyIndex returns an index to the `n` -length object that allows for negative indexing. For example, EasyIndex(-1, 5) returns 4. This is similar to Python's negative indexing. The return value may be less than zero if (-i) > n.
|
// EasyIndex returns an index to the `n` -length object that allows for negative indexing. For example, EasyIndex(-1, 5) returns 4. This is similar to Python's negative indexing. The return value may be less than zero if (-i) > n.
|
||||||
func EasyIndex(i, n int) int {
|
func EasyIndex(i, n int) int {
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user