diff --git a/backtesting.go b/backtesting.go index 2e4d608..0e81942 100644 --- a/backtesting.go +++ b/backtesting.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "math" "os" "strconv" "strings" @@ -42,6 +43,23 @@ func Backtest(trader *Trader) { stats := trader.Stats() // log.Println(trader.Stats().Dated.String()) + var totalTraded float64 + stats.Dated.Series("Trades").ForEach(func(i int, val any) { + if val == nil { + return + } + switch typ := val.(type) { + case []TradeStat: + for _, trade := range typ { + if trade.Exit { // Only count entry trades. + continue + } + totalTraded += trade.Price * math.Abs(trade.Units) + } + default: + panic("unknown type when calculating totalTraded") + } + }) // Divide net profit by maximum drawdown to get the profit factor. var maxDrawdown float64 stats.Dated.Series("Drawdown").ForEach(func(i int, val any) { @@ -59,6 +77,7 @@ func Backtest(trader *Trader) { w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) fmt.Fprintln(w) fmt.Fprintf(w, "Timespan:\t%s\t\n", stats.Dated.Date(-1).Sub(stats.Dated.Date(0)).Round(time.Second)) + fmt.Fprintf(w, "Total Traded:\t$%.2f\t\n", totalTraded) 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) @@ -600,7 +619,7 @@ func (p *TestPosition) close(atPrice float64, closeType OrderCloseType) { p.closePrice = atPrice p.closeType = closeType p.broker.Cash += p.Value() // Return the value of the position to the broker. - p.broker.spreadCollectedUSD += p.broker.Spread * p.units + p.broker.spreadCollectedUSD += p.broker.Spread * math.Abs(p.units) * p.closePrice p.broker.SignalEmit("PositionClosed", p) } diff --git a/cmd/ichimoku.go b/cmd/ichimoku.go index 5e7933b..15af31f 100644 --- a/cmd/ichimoku.go +++ b/cmd/ichimoku.go @@ -23,7 +23,7 @@ func (s *IchimokuStrategy) Next(t *auto.Trader) { laggingTime := data.Date(-s.leadingPeriods - 1) // Extract ichimoku elements - ichimoku := auto.Ichimoku(data, s.convPeriod, s.basePeriod, s.leadingPeriods, time.Minute*15) + ichimoku := auto.Ichimoku(data, s.convPeriod, s.basePeriod, s.leadingPeriods, time.Minute*1) conv := ichimoku.Series("Conversion") base := ichimoku.Series("Base") leadA := ichimoku.Series("LeadingA") @@ -91,7 +91,7 @@ func main() { Broker: auto.NewTestBroker(broker, nil, 10000, 50, 0.0002, 0), Strategy: &IchimokuStrategy{convPeriod: 9, basePeriod: 26, leadingPeriods: 52}, Symbol: "EUR_USD", - Frequency: "M15", + Frequency: "M1", // If the frequency is changed, update the call to Ichimoku() above. CandlesToKeep: 2500, })) } diff --git a/trader.go b/trader.go index dfceac7..563c475 100644 --- a/trader.go +++ b/trader.go @@ -33,6 +33,7 @@ func (t *Trader) Data() *IndexedFrame[UnixTime] { } type TradeStat struct { + Price float64 // Price is the price at which the trade was executed. If Exit is true, this is the exit price. Otherwise, this is the entry price. 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. } @@ -165,14 +166,18 @@ func (t *Trader) Buy(units, stopLoss, takeProfit float64) { t.CloseOrdersAndPositions() t.Log.Printf("Buy %v units", units) t.Broker.Order(Market, t.Symbol, units, 0, stopLoss, takeProfit) - t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{units, false}) + + tradeStat := TradeStat{t.Broker.Ask(t.Symbol), units, false} + t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat) } func (t *Trader) Sell(units, stopLoss, takeProfit float64) { t.CloseOrdersAndPositions() t.Log.Printf("Sell %v units", units) t.Broker.Order(Market, t.Symbol, -units, 0, stopLoss, takeProfit) - t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{-units, false}) + + tradeStat := TradeStat{t.Broker.Bid(t.Symbol), units, false} + t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat) } func (t *Trader) CloseOrdersAndPositions() { @@ -186,7 +191,9 @@ func (t *Trader) CloseOrdersAndPositions() { if position.Symbol() == t.Symbol { t.Log.Printf("Closing position: %v units, $%.2f PL", position.Units(), position.PL()) position.Close() - t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{position.Units(), true}) + + tradeStat := TradeStat{t.Broker.Price(t.Symbol, position.Units() < 0), position.Units(), true} + t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat) } } }