mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 08:23:51 +00:00
Added Round function and unit tests
This commit is contained in:
parent
34f98a1376
commit
494df7827b
@ -31,7 +31,7 @@ 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.
|
||||||
}
|
}
|
||||||
log.Println("Backtest complete. Opening report...")
|
log.Printf("Backtest completed on %d candles. Opening report...\n", trader.Stats().Dated.Len())
|
||||||
stats := trader.Stats()
|
stats := trader.Stats()
|
||||||
|
|
||||||
page := components.NewPage()
|
page := components.NewPage()
|
||||||
@ -40,11 +40,22 @@ func Backtest(trader *Trader) {
|
|||||||
balChart := charts.NewLine()
|
balChart := charts.NewLine()
|
||||||
balChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
balChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||||
Title: "Balance",
|
Title: "Balance",
|
||||||
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)),
|
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()),
|
||||||
|
}), charts.WithTooltipOpts(opts.Tooltip{
|
||||||
|
Show: true,
|
||||||
|
Trigger: "axis",
|
||||||
|
TriggerOn: "mousemove|click",
|
||||||
|
}), charts.WithYAxisOpts(opts.YAxis{
|
||||||
|
AxisLabel: &opts.AxisLabel{
|
||||||
|
Show: true,
|
||||||
|
Formatter: "${value}",
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
balChart.SetXAxis(seriesStringArray(stats.Dated.Dates())).
|
balChart.SetXAxis(seriesStringArray(stats.Dated.Dates())).
|
||||||
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity"))).
|
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity")), func(s *charts.SingleSeries) {
|
||||||
AddSeries("Drawdown", lineDataFromSeries(stats.Dated.Series("Drawdown")))
|
}).
|
||||||
|
AddSeries("Profit", lineDataFromSeries(stats.Dated.Series("Profit")))
|
||||||
|
// AddSeries("Drawdown", lineDataFromSeries(stats.Dated.Series("Drawdown")))
|
||||||
|
|
||||||
// Sort Returns by value.
|
// Sort Returns by value.
|
||||||
// Plot returns as a bar chart.
|
// Plot returns as a bar chart.
|
||||||
@ -83,6 +94,11 @@ func Backtest(trader *Trader) {
|
|||||||
returnsChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
returnsChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||||
Title: "Returns",
|
Title: "Returns",
|
||||||
Subtitle: fmt.Sprintf("Average: $%.2f", avg),
|
Subtitle: fmt.Sprintf("Average: $%.2f", avg),
|
||||||
|
}), charts.WithYAxisOpts(opts.YAxis{
|
||||||
|
AxisLabel: &opts.AxisLabel{
|
||||||
|
Show: true,
|
||||||
|
Formatter: "${value}",
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
returnsChart.SetXAxis(returnsLabels).
|
returnsChart.SetXAxis(returnsLabels).
|
||||||
AddSeries("Returns", returnsBars)
|
AddSeries("Returns", returnsBars)
|
||||||
@ -122,16 +138,16 @@ func Backtest(trader *Trader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func barDataFromSeries(s Series) []opts.BarData {
|
// func barDataFromSeries(s Series) []opts.BarData {
|
||||||
if s == nil || s.Len() == 0 {
|
// if s == nil || s.Len() == 0 {
|
||||||
return []opts.BarData{}
|
// return []opts.BarData{}
|
||||||
}
|
// }
|
||||||
data := make([]opts.BarData, s.Len())
|
// data := make([]opts.BarData, s.Len())
|
||||||
for i := 0; i < s.Len(); i++ {
|
// for i := 0; i < s.Len(); i++ {
|
||||||
data[i] = opts.BarData{Value: s.Value(i)}
|
// data[i] = opts.BarData{Value: s.Value(i)}
|
||||||
}
|
// }
|
||||||
return data
|
// return data
|
||||||
}
|
// }
|
||||||
|
|
||||||
func lineDataFromSeries(s Series) []opts.LineData {
|
func lineDataFromSeries(s Series) []opts.LineData {
|
||||||
if s == nil || s.Len() == 0 {
|
if s == nil || s.Len() == 0 {
|
||||||
@ -139,7 +155,7 @@ func lineDataFromSeries(s Series) []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: Round(s.Value(i).(float64), 2)}
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@ -294,6 +310,14 @@ func (b *TestBroker) NAV() float64 {
|
|||||||
return nav
|
return nav
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *TestBroker) PL() float64 {
|
||||||
|
var pl float64
|
||||||
|
for _, position := range b.positions {
|
||||||
|
pl += position.PL()
|
||||||
|
}
|
||||||
|
return pl
|
||||||
|
}
|
||||||
|
|
||||||
func (b *TestBroker) OpenOrders() []Order {
|
func (b *TestBroker) OpenOrders() []Order {
|
||||||
orders := make([]Order, 0, len(b.orders))
|
orders := make([]Order, 0, len(b.orders))
|
||||||
for _, order := range b.orders {
|
for _, order := range b.orders {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package autotrader
|
package autotrader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -83,7 +82,7 @@ func TestBacktestingBrokerCandles(t *testing.T) {
|
|||||||
func TestBacktestingBrokerFunctions(t *testing.T) {
|
func TestBacktestingBrokerFunctions(t *testing.T) {
|
||||||
broker := NewTestBroker(nil, nil, 100_000, 20, 0, 0)
|
broker := NewTestBroker(nil, nil, 100_000, 20, 0, 0)
|
||||||
|
|
||||||
if broker.NAV() != 100_000 {
|
if !EqualApprox(broker.NAV(), 100_000) {
|
||||||
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
|
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,15 +154,15 @@ func TestBacktestingBrokerOrders(t *testing.T) {
|
|||||||
t.Errorf("Expected take profit to be 0, got %f", position.TakeProfit())
|
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
|
if !EqualApprox(broker.NAV(), 100_000) { // NAV should not change until the next candle
|
||||||
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
|
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
|
||||||
}
|
}
|
||||||
|
|
||||||
broker.Advance() // Advance broker to the next candle
|
broker.Advance() // Advance broker to the next candle
|
||||||
if math.Round(position.PL()) != 2500 { // (1.2-1.15) * 50_000 = 2500
|
if !EqualApprox(position.PL(), 2500) { // (1.2-1.15) * 50_000 = 2500
|
||||||
t.Errorf("Expected position PL to be 2500, got %f", position.PL())
|
t.Errorf("Expected position PL to be 2500, got %f", position.PL())
|
||||||
}
|
}
|
||||||
if math.Round(broker.NAV()) != 102_500 {
|
if !EqualApprox(broker.NAV(), 102_500) {
|
||||||
t.Errorf("Expected NAV to be 102_500, got %f", broker.NAV())
|
t.Errorf("Expected NAV to be 102_500, got %f", broker.NAV())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +172,10 @@ func TestBacktestingBrokerOrders(t *testing.T) {
|
|||||||
t.Error("Expected position to be closed")
|
t.Error("Expected position to be closed")
|
||||||
}
|
}
|
||||||
broker.Advance()
|
broker.Advance()
|
||||||
if broker.NAV() != 102_500 {
|
if !EqualApprox(broker.NAV(), 102_500) {
|
||||||
t.Errorf("Expected NAV to still be 102_500, got %f", broker.NAV())
|
t.Errorf("Expected NAV to still be 102_500, got %f", broker.NAV())
|
||||||
}
|
}
|
||||||
|
if !EqualApprox(broker.PL(), 2500) {
|
||||||
|
t.Errorf("Expected broker PL to be 2500, got %f", broker.PL())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@ type Broker interface {
|
|||||||
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)
|
||||||
NAV() float64 // NAV returns the net asset value of the account.
|
NAV() float64 // NAV returns the net asset value of the account.
|
||||||
|
PL() float64 // PL returns the profit or loss of the account.
|
||||||
OpenOrders() []Order
|
OpenOrders() []Order
|
||||||
OpenPositions() []Position
|
OpenPositions() []Position
|
||||||
// Orders returns a slice of orders that have been placed with the broker. If an order has been canceled or
|
// Orders returns a slice of orders that have been placed with the broker. If an order has been canceled or
|
||||||
|
@ -88,6 +88,7 @@ func (t *Trader) Init() {
|
|||||||
t.stats.Dated = 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("Profit", nil)),
|
||||||
NewDataSeries(dataframe.NewSeriesFloat64("Drawdown", nil)),
|
NewDataSeries(dataframe.NewSeriesFloat64("Drawdown", nil)),
|
||||||
NewDataSeries(dataframe.NewSeriesFloat64("Returns", nil)),
|
NewDataSeries(dataframe.NewSeriesFloat64("Returns", nil)),
|
||||||
)
|
)
|
||||||
@ -104,9 +105,10 @@ 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.Dated.PushValues(map[string]interface{}{
|
err := 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(),
|
||||||
|
"Profit": t.Broker.PL(),
|
||||||
"Drawdown": func() float64 {
|
"Drawdown": func() float64 {
|
||||||
var bal float64
|
var bal float64
|
||||||
if t.stats.Dated.Len() > 0 {
|
if t.stats.Dated.Len() > 0 {
|
||||||
@ -124,6 +126,9 @@ func (t *Trader) Tick() {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error pushing values to stats dataframe: %v\n", err.Error())
|
||||||
|
}
|
||||||
t.stats.returnsThisCandle = 0
|
t.stats.returnsThisCandle = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
utils.go
25
utils.go
@ -1,13 +1,14 @@
|
|||||||
package autotrader
|
package autotrader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"golang.org/x/exp/constraints"
|
"golang.org/x/exp/constraints"
|
||||||
)
|
)
|
||||||
|
|
||||||
const floatComparisonTolerance = float64(1e-6)
|
const float64Tolerance = 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
|
// 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 {
|
func Crossover(a, b Series) bool {
|
||||||
@ -22,8 +23,28 @@ func EasyIndex(i, n int) int {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EqualApprox returns true if a and b are approximately equal. NaN and Inf are handled correctly. The tolerance is 1e-6 or 0.0000001.
|
||||||
func EqualApprox(a, b float64) bool {
|
func EqualApprox(a, b float64) bool {
|
||||||
return Abs(a-b) < floatComparisonTolerance
|
if math.IsNaN(a) || math.IsNaN(b) {
|
||||||
|
return math.IsNaN(a) && math.IsNaN(b)
|
||||||
|
} else if math.IsInf(a, 1) || math.IsInf(b, 1) {
|
||||||
|
return math.IsInf(a, 1) && math.IsInf(b, 1)
|
||||||
|
} else if math.IsInf(a, -1) || math.IsInf(b, -1) {
|
||||||
|
return math.IsInf(a, -1) && math.IsInf(b, -1)
|
||||||
|
}
|
||||||
|
return math.Abs(a-b) <= float64Tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round returns f rounded to d decimal places. d may be negative to round to the left of the decimal point.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// Round(123.456, 0) // 123.0
|
||||||
|
// Round(123.456, 1) // 123.5
|
||||||
|
// Round(123.456, -1) // 120.0
|
||||||
|
func Round(f float64, d int) float64 {
|
||||||
|
ratio := math.Pow10(d)
|
||||||
|
return math.Round(f*ratio) / ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
func Abs[T constraints.Integer | constraints.Float](a T) T {
|
func Abs[T constraints.Integer | constraints.Float](a T) T {
|
||||||
|
51
utils_test.go
Normal file
51
utils_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package autotrader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEqualApprox(t *testing.T) {
|
||||||
|
if !EqualApprox(0.0000000, float64Tolerance/10) { // 1e-6
|
||||||
|
t.Error("Expected 0.0000000 to be approximately equal to 0.0000001")
|
||||||
|
}
|
||||||
|
if EqualApprox(0.0000000, float64Tolerance+float64Tolerance/10) {
|
||||||
|
t.Error("Expected 0.0000000 to not be approximately equal to 0.0000011")
|
||||||
|
}
|
||||||
|
if !EqualApprox(math.NaN(), math.NaN()) {
|
||||||
|
t.Error("Expected NaN to be approximately equal to NaN")
|
||||||
|
}
|
||||||
|
if EqualApprox(math.NaN(), 0) {
|
||||||
|
t.Error("Expected NaN to not be approximately equal to 0")
|
||||||
|
}
|
||||||
|
if !EqualApprox(math.Inf(1), math.Inf(1)) {
|
||||||
|
t.Error("Expected Inf to be approximately equal to Inf")
|
||||||
|
}
|
||||||
|
if EqualApprox(math.Inf(-1), math.Inf(1)) {
|
||||||
|
t.Error("Expected -Inf to not be approximately equal to Inf")
|
||||||
|
}
|
||||||
|
if EqualApprox(1, 2) {
|
||||||
|
t.Error("Expected 1 to not be approximately equal to 2")
|
||||||
|
}
|
||||||
|
if !EqualApprox(0.3, 0.6/2) {
|
||||||
|
t.Errorf("Expected 0.3 to be approximately equal to %f", 6.0/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRound(t *testing.T) {
|
||||||
|
if Round(0.1234567, 0) != 0 {
|
||||||
|
t.Error("Expected 0.1234567 to round to 0")
|
||||||
|
}
|
||||||
|
if Round(0.1234567, 1) != 0.1 {
|
||||||
|
t.Error("Expected 0.1234567 to round to 0.1")
|
||||||
|
}
|
||||||
|
if Round(0.1234567, 2) != 0.12 {
|
||||||
|
t.Error("Expected 0.1234567 to round to 0.12")
|
||||||
|
}
|
||||||
|
if Round(0.128, 2) != 0.13 {
|
||||||
|
t.Error("Expected 0.128 to round to 0.13")
|
||||||
|
}
|
||||||
|
if Round(12.34, -1) != 10 {
|
||||||
|
t.Error("Expected 12.34 to round to 10")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user