mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-14 16:03: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.
|
||||
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()
|
||||
|
||||
page := components.NewPage()
|
||||
@ -40,11 +40,22 @@ func Backtest(trader *Trader) {
|
||||
balChart := charts.NewLine()
|
||||
balChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||
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())).
|
||||
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity"))).
|
||||
AddSeries("Drawdown", lineDataFromSeries(stats.Dated.Series("Drawdown")))
|
||||
AddSeries("Equity", lineDataFromSeries(stats.Dated.Series("Equity")), func(s *charts.SingleSeries) {
|
||||
}).
|
||||
AddSeries("Profit", lineDataFromSeries(stats.Dated.Series("Profit")))
|
||||
// AddSeries("Drawdown", lineDataFromSeries(stats.Dated.Series("Drawdown")))
|
||||
|
||||
// Sort Returns by value.
|
||||
// Plot returns as a bar chart.
|
||||
@ -83,6 +94,11 @@ func Backtest(trader *Trader) {
|
||||
returnsChart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
|
||||
Title: "Returns",
|
||||
Subtitle: fmt.Sprintf("Average: $%.2f", avg),
|
||||
}), charts.WithYAxisOpts(opts.YAxis{
|
||||
AxisLabel: &opts.AxisLabel{
|
||||
Show: true,
|
||||
Formatter: "${value}",
|
||||
},
|
||||
}))
|
||||
returnsChart.SetXAxis(returnsLabels).
|
||||
AddSeries("Returns", returnsBars)
|
||||
@ -122,16 +138,16 @@ 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 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 {
|
||||
if s == nil || s.Len() == 0 {
|
||||
@ -139,7 +155,7 @@ func lineDataFromSeries(s Series) []opts.LineData {
|
||||
}
|
||||
data := make([]opts.LineData, s.Len())
|
||||
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
|
||||
}
|
||||
@ -294,6 +310,14 @@ func (b *TestBroker) NAV() float64 {
|
||||
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 {
|
||||
orders := make([]Order, 0, len(b.orders))
|
||||
for _, order := range b.orders {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -83,7 +82,7 @@ func TestBacktestingBrokerCandles(t *testing.T) {
|
||||
func TestBacktestingBrokerFunctions(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@ -155,15 +154,15 @@ func TestBacktestingBrokerOrders(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
@ -173,7 +172,10 @@ func TestBacktestingBrokerOrders(t *testing.T) {
|
||||
t.Error("Expected position to be closed")
|
||||
}
|
||||
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())
|
||||
}
|
||||
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)
|
||||
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
|
||||
NAV() float64 // NAV returns the net asset value of the account.
|
||||
PL() float64 // PL returns the profit or loss of the account.
|
||||
OpenOrders() []Order
|
||||
OpenPositions() []Position
|
||||
// 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(
|
||||
NewDataSeries(dataframe.NewSeriesTime("Date", nil)),
|
||||
NewDataSeries(dataframe.NewSeriesFloat64("Equity", nil)),
|
||||
NewDataSeries(dataframe.NewSeriesFloat64("Profit", nil)),
|
||||
NewDataSeries(dataframe.NewSeriesFloat64("Drawdown", nil)),
|
||||
NewDataSeries(dataframe.NewSeriesFloat64("Returns", nil)),
|
||||
)
|
||||
@ -104,9 +105,10 @@ func (t *Trader) Tick() {
|
||||
t.Strategy.Next(t) // Run the strategy.
|
||||
|
||||
// Update the stats.
|
||||
t.stats.Dated.PushValues(map[string]interface{}{
|
||||
err := t.stats.Dated.PushValues(map[string]interface{}{
|
||||
"Date": t.data.Date(-1),
|
||||
"Equity": t.Broker.NAV(),
|
||||
"Profit": t.Broker.PL(),
|
||||
"Drawdown": func() float64 {
|
||||
var bal float64
|
||||
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
|
||||
}
|
||||
|
||||
|
25
utils.go
25
utils.go
@ -1,13 +1,14 @@
|
||||
package autotrader
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"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
|
||||
func Crossover(a, b Series) bool {
|
||||
@ -22,8 +23,28 @@ func EasyIndex(i, n int) int {
|
||||
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 {
|
||||
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 {
|
||||
|
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