mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 16:33:50 +00:00
Added trade stats and candlestick chart
This commit is contained in:
parent
52260551e8
commit
565aa6c9fd
195
backtesting.go
195
backtesting.go
@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-echarts/go-echarts/v2/charts"
|
"github.com/go-echarts/go-echarts/v2/charts"
|
||||||
@ -33,31 +34,65 @@ func Backtest(trader *Trader) {
|
|||||||
trader.Tick() // Allow the trader to process the current candlesticks.
|
trader.Tick() // Allow the trader to process the current candlesticks.
|
||||||
broker.Advance() // Give the trader access to the next candlestick.
|
broker.Advance() // Give the trader access to the next candlestick.
|
||||||
}
|
}
|
||||||
|
trader.closeOrdersAndPositions() // Close any outstanding trades now.
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
// Pick a datetime layout based on the frequency.
|
||||||
|
dateLayout := time.DateTime
|
||||||
|
if strings.Contains(trader.Frequency, "S") { // Seconds
|
||||||
|
dateLayout = "15:04:05"
|
||||||
|
} else if strings.Contains(trader.Frequency, "H") { // Hours
|
||||||
|
dateLayout = "2006-01-02 15:04"
|
||||||
|
} else if strings.Contains(trader.Frequency, "D") || trader.Frequency == "W" { // Days or Weeks
|
||||||
|
dateLayout = time.DateOnly
|
||||||
|
} else if trader.Frequency == "M" { // Months
|
||||||
|
dateLayout = "2006-01"
|
||||||
|
} else if strings.Contains(trader.Frequency, "M") { // Minutes
|
||||||
|
dateLayout = "01-02 15:04"
|
||||||
|
}
|
||||||
|
|
||||||
page := components.NewPage()
|
page := components.NewPage()
|
||||||
|
|
||||||
// Create a new line balChart 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.
|
||||||
balChart := charts.NewLine()
|
balChart := charts.NewLine()
|
||||||
balChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
balChart.SetGlobalOptions(
|
||||||
Title: "Balance",
|
charts.WithTitleOpts(opts.Title{
|
||||||
Subtitle: fmt.Sprintf("%s %s %T %s (took %.2f seconds)", trader.Symbol, trader.Frequency, trader.Strategy, time.Now().Format(time.DateTime), time.Since(start).Seconds()),
|
Title: "Balance",
|
||||||
}), charts.WithTooltipOpts(opts.Tooltip{
|
Subtitle: fmt.Sprintf("%s %s %T %s (took %.2f seconds)", trader.Symbol, trader.Frequency, trader.Strategy, time.Now().Format(time.DateTime), time.Since(start).Seconds()),
|
||||||
Show: true,
|
}),
|
||||||
Trigger: "axis",
|
charts.WithTooltipOpts(opts.Tooltip{
|
||||||
TriggerOn: "mousemove|click",
|
|
||||||
}), charts.WithYAxisOpts(opts.YAxis{
|
|
||||||
AxisLabel: &opts.AxisLabel{
|
|
||||||
Show: true,
|
Show: true,
|
||||||
Formatter: "${value}",
|
Trigger: "axis",
|
||||||
},
|
TriggerOn: "mousemove|click",
|
||||||
}))
|
}),
|
||||||
balChart.SetXAxis(seriesStringArray(stats.Dated.Dates())).
|
charts.WithYAxisOpts(opts.YAxis{
|
||||||
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity")), func(s *charts.SingleSeries) {
|
AxisLabel: &opts.AxisLabel{
|
||||||
}).
|
Show: true,
|
||||||
AddSeries("Profit", lineDataFromSeries(stats.Dated.Series("Profit")))
|
Formatter: "${value}",
|
||||||
// AddSeries("Drawdown", lineDataFromSeries(stats.Dated.Series("Drawdown")))
|
},
|
||||||
|
}),
|
||||||
|
charts.WithLegendOpts(opts.Legend{
|
||||||
|
Show: true,
|
||||||
|
Selected: map[string]bool{"Equity": false, "Profit": true},
|
||||||
|
}))
|
||||||
|
balChart.SetXAxis(seriesStringArray(stats.Dated.Dates(), dateLayout)).
|
||||||
|
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity"))).
|
||||||
|
SetSeriesOptions(
|
||||||
|
charts.WithMarkPointNameTypeItemOpts(
|
||||||
|
opts.MarkPointNameTypeItem{Name: "Peak", Type: "max", ItemStyle: &opts.ItemStyle{
|
||||||
|
Color: balChart.Colors[1],
|
||||||
|
}},
|
||||||
|
opts.MarkPointNameTypeItem{Name: "Drawdown", Type: "min", ItemStyle: &opts.ItemStyle{
|
||||||
|
Color: balChart.Colors[3],
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
balChart.AddSeries("Profit", lineDataFromSeries(stats.Dated.Series("Profit")))
|
||||||
|
|
||||||
|
// Create a new kline chart based on the candlesticks and add it to the page.
|
||||||
|
kline := newKline(trader.data, stats.Dated.Series("Trades"), dateLayout)
|
||||||
|
|
||||||
// Sort Returns by value.
|
// Sort Returns by value.
|
||||||
// Plot returns as a bar chart.
|
// Plot returns as a bar chart.
|
||||||
@ -93,15 +128,17 @@ func Backtest(trader *Trader) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
returnsChart := charts.NewBar()
|
returnsChart := charts.NewBar()
|
||||||
returnsChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
returnsChart.SetGlobalOptions(
|
||||||
Title: "Returns",
|
charts.WithTitleOpts(opts.Title{
|
||||||
Subtitle: fmt.Sprintf("Average: $%.2f", avg),
|
Title: "Returns",
|
||||||
}), charts.WithYAxisOpts(opts.YAxis{
|
Subtitle: fmt.Sprintf("Average: $%.2f", avg),
|
||||||
AxisLabel: &opts.AxisLabel{
|
}),
|
||||||
Show: true,
|
charts.WithYAxisOpts(opts.YAxis{
|
||||||
Formatter: "${value}",
|
AxisLabel: &opts.AxisLabel{
|
||||||
},
|
Show: true,
|
||||||
}))
|
Formatter: "${value}",
|
||||||
|
},
|
||||||
|
}))
|
||||||
returnsChart.SetXAxis(returnsLabels).
|
returnsChart.SetXAxis(returnsLabels).
|
||||||
AddSeries("Returns", returnsBars)
|
AddSeries("Returns", returnsBars)
|
||||||
|
|
||||||
@ -121,7 +158,7 @@ func Backtest(trader *Trader) {
|
|||||||
|
|
||||||
// Add all the charts in the desired order.
|
// Add all the charts in the desired order.
|
||||||
page.PageTitle = "Backtest Report"
|
page.PageTitle = "Backtest Report"
|
||||||
page.AddCharts(balChart, returnsChart)
|
page.AddCharts(balChart, kline, 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")
|
||||||
@ -140,16 +177,85 @@ func Backtest(trader *Trader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func barDataFromSeries(s Series) []opts.BarData {
|
func newKline(dohlcv Frame, trades Series, dateLayout string) *charts.Kline {
|
||||||
// if s == nil || s.Len() == 0 {
|
kline := charts.NewKLine()
|
||||||
// return []opts.BarData{}
|
|
||||||
// }
|
x := make([]string, dohlcv.Len())
|
||||||
// data := make([]opts.BarData, s.Len())
|
y := make([]opts.KlineData, dohlcv.Len())
|
||||||
// for i := 0; i < s.Len(); i++ {
|
for i := 0; i < dohlcv.Len(); i++ {
|
||||||
// data[i] = opts.BarData{Value: s.Value(i)}
|
x[i] = dohlcv.Date(i).Format(dateLayout)
|
||||||
// }
|
y[i] = opts.KlineData{Value: [4]float64{
|
||||||
// return data
|
dohlcv.Open(i),
|
||||||
// }
|
dohlcv.Close(i),
|
||||||
|
dohlcv.Low(i),
|
||||||
|
dohlcv.High(i),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
marks := make([]opts.MarkPointNameCoordItem, 0)
|
||||||
|
for i := 0; i < trades.Len(); i++ {
|
||||||
|
if slice := trades.Value(i); slice != nil {
|
||||||
|
for _, trade := range slice.([]TradeStat) {
|
||||||
|
color := "green"
|
||||||
|
rotation := float32(0)
|
||||||
|
if trade.Units < 0 {
|
||||||
|
color = "red"
|
||||||
|
rotation = 180
|
||||||
|
}
|
||||||
|
if trade.Exit {
|
||||||
|
color = "black"
|
||||||
|
}
|
||||||
|
marks = append(marks, opts.MarkPointNameCoordItem{
|
||||||
|
Name: "Trade",
|
||||||
|
Value: fmt.Sprintf("%v units", trade.Units),
|
||||||
|
Coordinate: []interface{}{x[i], y[i].Value.([4]float64)[1]},
|
||||||
|
Label: &opts.Label{
|
||||||
|
Show: true,
|
||||||
|
Position: "inside",
|
||||||
|
},
|
||||||
|
ItemStyle: &opts.ItemStyle{
|
||||||
|
Color: color,
|
||||||
|
},
|
||||||
|
Symbol: "arrow",
|
||||||
|
SymbolRotate: rotation,
|
||||||
|
SymbolSize: 25,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kline.SetGlobalOptions(
|
||||||
|
charts.WithTitleOpts(opts.Title{
|
||||||
|
Title: "Trades",
|
||||||
|
Subtitle: fmt.Sprintf("Showing %d candles", dohlcv.Len()),
|
||||||
|
}),
|
||||||
|
charts.WithXAxisOpts(opts.XAxis{
|
||||||
|
SplitNumber: 20,
|
||||||
|
}),
|
||||||
|
charts.WithYAxisOpts(opts.YAxis{
|
||||||
|
Scale: true,
|
||||||
|
}),
|
||||||
|
charts.WithTooltipOpts(opts.Tooltip{ // Enable seeing details on hover.
|
||||||
|
Show: true,
|
||||||
|
Trigger: "axis",
|
||||||
|
TriggerOn: "mousemove|click",
|
||||||
|
}),
|
||||||
|
charts.WithDataZoomOpts(opts.DataZoom{ // Support zooming with scroll wheel.
|
||||||
|
Type: "inside",
|
||||||
|
Start: 0,
|
||||||
|
End: 100,
|
||||||
|
XAxisIndex: []int{0},
|
||||||
|
}),
|
||||||
|
charts.WithDataZoomOpts(opts.DataZoom{ // Support zooming with bottom slider.
|
||||||
|
Type: "slider",
|
||||||
|
Start: 0,
|
||||||
|
End: 100,
|
||||||
|
XAxisIndex: []int{0},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
kline.SetXAxis(x).AddSeries("Price Action", y, charts.WithMarkPointNameCoordItemOpts(marks...))
|
||||||
|
return kline
|
||||||
|
}
|
||||||
|
|
||||||
func lineDataFromSeries(s Series) []opts.LineData {
|
func lineDataFromSeries(s Series) []opts.LineData {
|
||||||
if s == nil || s.Len() == 0 {
|
if s == nil || s.Len() == 0 {
|
||||||
@ -162,27 +268,14 @@ func lineDataFromSeries(s Series) []opts.LineData {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func seriesStringArray(s Series) []string {
|
func seriesStringArray(s Series, dateLayout string) []string {
|
||||||
if s == nil || s.Len() == 0 {
|
if s == nil || s.Len() == 0 {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
first := true
|
|
||||||
data := make([]string, s.Len())
|
data := make([]string, s.Len())
|
||||||
var dateLayout string
|
|
||||||
for i := 0; i < s.Len(); i++ {
|
for i := 0; i < s.Len(); i++ {
|
||||||
switch val := s.Value(i).(type) {
|
switch val := s.Value(i).(type) {
|
||||||
case time.Time:
|
case time.Time:
|
||||||
if first {
|
|
||||||
first = false
|
|
||||||
dateHead := s.Value(0).(time.Time)
|
|
||||||
dateTail := s.Value(-1).(time.Time)
|
|
||||||
diff := dateTail.Sub(dateHead)
|
|
||||||
if diff.Hours() > 24*365 {
|
|
||||||
dateLayout = time.DateOnly
|
|
||||||
} else {
|
|
||||||
dateLayout = time.DateTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data[i] = val.Format(dateLayout)
|
data[i] = val.Format(dateLayout)
|
||||||
case string:
|
case string:
|
||||||
data[i] = fmt.Sprintf("%q", val)
|
data[i] = fmt.Sprintf("%q", val)
|
||||||
|
44
trader.go
44
trader.go
@ -11,12 +11,6 @@ import (
|
|||||||
"github.com/go-co-op/gocron"
|
"github.com/go-co-op/gocron"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.
|
||||||
@ -38,6 +32,18 @@ func (t *Trader) Data() *DataFrame {
|
|||||||
return t.data
|
return t.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TradeStat struct {
|
||||||
|
Units float64 // Units is the signed number of units bought or sold.
|
||||||
|
Exit bool // Exit is true if the trade was to exit a previous position.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance (financial) reporting and statistics.
|
||||||
|
type TraderStats struct {
|
||||||
|
Dated *DataFrame
|
||||||
|
returnsThisCandle float64
|
||||||
|
tradesThisCandle []TradeStat
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Trader) Stats() *TraderStats {
|
func (t *Trader) Stats() *TraderStats {
|
||||||
return t.stats
|
return t.stats
|
||||||
}
|
}
|
||||||
@ -90,8 +96,10 @@ func (t *Trader) Init() {
|
|||||||
NewDataSeries("Profit"),
|
NewDataSeries("Profit"),
|
||||||
NewDataSeries("Drawdown"),
|
NewDataSeries("Drawdown"),
|
||||||
NewDataSeries("Returns"),
|
NewDataSeries("Returns"),
|
||||||
|
NewDataSeries("Trades"), // []float64 representing the number of units traded positive for buy, negative for sell.
|
||||||
)
|
)
|
||||||
t.Broker.SignalConnect("PositionClosed", t, func(args ...interface{}) {
|
t.stats.tradesThisCandle = make([]TradeStat, 0, 2)
|
||||||
|
t.Broker.SignalConnect("PositionClosed", t, func(args ...any) {
|
||||||
position := args[0].(Position)
|
position := args[0].(Position)
|
||||||
t.stats.returnsThisCandle += position.PL()
|
t.stats.returnsThisCandle += position.PL()
|
||||||
})
|
})
|
||||||
@ -104,7 +112,7 @@ func (t *Trader) Tick() {
|
|||||||
t.Strategy.Next(t) // Run the strategy.
|
t.Strategy.Next(t) // Run the strategy.
|
||||||
|
|
||||||
// Update the stats.
|
// Update the stats.
|
||||||
err := t.stats.Dated.PushValues(map[string]interface{}{
|
err := t.stats.Dated.PushValues(map[string]any{
|
||||||
"Date": t.data.Date(-1),
|
"Date": t.data.Date(-1),
|
||||||
"Equity": t.Broker.NAV(),
|
"Equity": t.Broker.NAV(),
|
||||||
"Profit": t.Broker.PL(),
|
"Profit": t.Broker.PL(),
|
||||||
@ -117,13 +125,22 @@ func (t *Trader) Tick() {
|
|||||||
}
|
}
|
||||||
return Max(bal-t.Broker.NAV(), 0)
|
return Max(bal-t.Broker.NAV(), 0)
|
||||||
}(),
|
}(),
|
||||||
"Returns": func() interface{} {
|
"Returns": func() any {
|
||||||
if t.stats.returnsThisCandle != 0 {
|
if t.stats.returnsThisCandle != 0 {
|
||||||
return t.stats.returnsThisCandle
|
return t.stats.returnsThisCandle
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
|
"Trades": func() any {
|
||||||
|
if len(t.stats.tradesThisCandle) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
trades := make([]TradeStat, len(t.stats.tradesThisCandle))
|
||||||
|
copy(trades, t.stats.tradesThisCandle)
|
||||||
|
t.stats.tradesThisCandle = t.stats.tradesThisCandle[:0]
|
||||||
|
return trades
|
||||||
|
}(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error pushing values to stats dataframe: %v\n", err.Error())
|
log.Printf("error pushing values to stats dataframe: %v\n", err.Error())
|
||||||
@ -146,26 +163,31 @@ func (t *Trader) fetchData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) Buy(units float64) {
|
func (t *Trader) Buy(units float64) {
|
||||||
t.Log.Printf("Buy %f units", units)
|
|
||||||
t.closeOrdersAndPositions()
|
t.closeOrdersAndPositions()
|
||||||
|
t.Log.Printf("Buy %f units", units)
|
||||||
t.Broker.MarketOrder(t.Symbol, units, 0.0, 0.0)
|
t.Broker.MarketOrder(t.Symbol, units, 0.0, 0.0)
|
||||||
|
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{units, false})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) Sell(units float64) {
|
func (t *Trader) Sell(units float64) {
|
||||||
t.Log.Printf("Sell %f units", units)
|
|
||||||
t.closeOrdersAndPositions()
|
t.closeOrdersAndPositions()
|
||||||
|
t.Log.Printf("Sell %f units", units)
|
||||||
t.Broker.MarketOrder(t.Symbol, -units, 0.0, 0.0)
|
t.Broker.MarketOrder(t.Symbol, -units, 0.0, 0.0)
|
||||||
|
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{-units, false})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) closeOrdersAndPositions() {
|
func (t *Trader) closeOrdersAndPositions() {
|
||||||
for _, order := range t.Broker.OpenOrders() {
|
for _, order := range t.Broker.OpenOrders() {
|
||||||
if order.Symbol() == t.Symbol {
|
if order.Symbol() == t.Symbol {
|
||||||
|
t.Log.Printf("Cancelling order %s (%f units)", order.Id(), order.Units())
|
||||||
order.Cancel()
|
order.Cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, position := range t.Broker.OpenPositions() {
|
for _, position := range t.Broker.OpenPositions() {
|
||||||
if position.Symbol() == t.Symbol {
|
if position.Symbol() == t.Symbol {
|
||||||
|
t.Log.Printf("Closing position %s (%f units, %f PL)", position.Id(), position.Units(), position.PL())
|
||||||
position.Close()
|
position.Close()
|
||||||
|
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{position.Units(), true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user