From b5434fb5de294fbea3a2aea435e2c72643455ffa Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 24 May 2023 15:58:59 -0500 Subject: [PATCH] Fixed MORE bugs --- .vscode/launch.json | 8 ++++++ backtesting.go | 2 +- cmd/ichimoku.go | 68 +++++++++++++++++++++++++++++++++++++------- cmd/sma_crossover.go | 4 +-- frame_indexed.go | 6 ++-- indicators.go | 37 ++++++------------------ series.go | 4 +-- series_indexed.go | 44 ++++++++++++++++++++++++++++ series_test.go | 53 +++++++++++++++++++++++++++++++++- trader.go | 34 +++++++++++++++++----- 10 files changed, 205 insertions(+), 55 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a97ab65..e9824e2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,6 +19,14 @@ "mode": "auto", "program": "${workspaceFolder}/cmd/sma_crossover.go", "cwd": "${workspaceFolder}" + }, + { + "name": "Run Ichimoku", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/ichimoku.go", + "cwd": "${workspaceFolder}" } ] } \ No newline at end of file diff --git a/backtesting.go b/backtesting.go index cc927b9..2e4d608 100644 --- a/backtesting.go +++ b/backtesting.go @@ -36,7 +36,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. } - trader.closeOrdersAndPositions() // Close any outstanding trades now. + trader.CloseOrdersAndPositions() // Close any outstanding trades now. log.Printf("Backtest completed on %d candles. Opening report...\n", trader.Stats().Dated.Len()) stats := trader.Stats() diff --git a/cmd/ichimoku.go b/cmd/ichimoku.go index b986a63..d4163b8 100644 --- a/cmd/ichimoku.go +++ b/cmd/ichimoku.go @@ -2,7 +2,13 @@ package main -import auto "github.com/fivemoreminix/autotrader" +import ( + "os" + "time" + + auto "github.com/fivemoreminix/autotrader" + "github.com/fivemoreminix/autotrader/oanda" +) type IchimokuStrategy struct { convPeriod, basePeriod, leadingPeriods int @@ -12,23 +18,63 @@ func (s *IchimokuStrategy) Init(_ *auto.Trader) { } func (s *IchimokuStrategy) Next(t *auto.Trader) { - ichimoku := auto.Ichimoku(t.Data().Closes(), s.convPeriod, s.basePeriod, s.leadingPeriods) - time := t.Data().Date(-1) - // If the price crosses above the Conversion Line, buy. - if auto.CrossoverIndex(*time, t.Data().Closes(), ichimoku.Series("Conversion")) { - t.Buy(1000) + data := t.Data() + now := *data.Date(-1) + laggingTime := data.Date(-s.leadingPeriods - 1) + + // Extract ichimoku elements + ichimoku := auto.Ichimoku(data, s.convPeriod, s.basePeriod, s.leadingPeriods, time.Minute*15) + conv := ichimoku.Series("Conversion") + base := ichimoku.Series("Base") + leadA := ichimoku.Series("LeadingA") + leadB := ichimoku.Series("LeadingB") + lagging := ichimoku.Series("Lagging") + + // Conditions to buy: + // - price closed above the cloud at the current time + // - conversion above baseline + // - future cloud must be green (LeadingA > LeadingB) + + // Oposite conditions for sell... + + if laggingTime == nil { // Not enough candles to see the lagging. + return } - // If the price crosses below the Conversion Line, sell. - if auto.CrossoverIndex(*time, ichimoku.Series("Conversion"), t.Data().Closes()) { - t.Sell(1000) + + if t.IsLong() { + if data.CloseIndex(now) < base.FloatIndex(now) || + leadA.FloatIndex(now) < leadB.FloatIndex(now) { + t.CloseOrdersAndPositions() + } + } else if t.IsShort() { + if data.CloseIndex(now) > base.FloatIndex(now) || + leadA.FloatIndex(now) > leadB.FloatIndex(now) { + t.CloseOrdersAndPositions() + } + } else { + // Look to enter a trade + if data.CloseIndex(now) > leadA.FloatIndex(now) && + leadA.FloatIndex(now) > leadB.FloatIndex(now) && + conv.FloatIndex(now) > base.FloatIndex(now) && + leadA.Float(-1) > leadB.Float(-1) && + lagging.FloatIndex(*laggingTime) > leadA.FloatIndex(*laggingTime) { + t.Buy(10000, 0, 0) + } else if data.CloseIndex(now) < leadA.FloatIndex(now) && + leadA.FloatIndex(now) < leadB.FloatIndex(now) && + conv.FloatIndex(now) < base.FloatIndex(now) && + leadA.Float(-1) < leadB.Float(-1) && + lagging.FloatIndex(*laggingTime) < leadA.FloatIndex(*laggingTime) { + t.Sell(10000, 0, 0) + } } } func main() { + broker := oanda.NewOandaBroker(os.Getenv("OANDA_TOKEN"), os.Getenv("OANDA_ACCOUNT_ID"), true) auto.Backtest(auto.NewTrader(auto.TraderConfig{ - Broker: auto.NewTestBroker(nil, nil, 10000, 50, 0.0002, 0), + Broker: auto.NewTestBroker(broker, nil, 10000, 50, 0.0002, 0), Strategy: &IchimokuStrategy{convPeriod: 9, basePeriod: 26, leadingPeriods: 52}, - Symbol: "EUR_USD", + Symbol: "USD_JPY", Frequency: "M15", CandlesToKeep: 2500, })) diff --git a/cmd/sma_crossover.go b/cmd/sma_crossover.go index 74d8333..3e4ec40 100644 --- a/cmd/sma_crossover.go +++ b/cmd/sma_crossover.go @@ -21,9 +21,9 @@ func (s *SMAStrategy) Next(t *auto.Trader) { sma2 := t.Data().Closes().Copy().Rolling(s.period2).Mean() // If the shorter SMA crosses above the longer SMA, buy. if auto.CrossoverIndex(*t.Data().Date(-1), sma1, sma2) { - t.Buy(1000) + t.Buy(1000, 0, 0) } else if auto.CrossoverIndex(*t.Data().Date(-1), sma2, sma1) { - t.Sell(1000) + t.Sell(1000, 0, 0) } } diff --git a/frame_indexed.go b/frame_indexed.go index e412d6a..0ba28b7 100644 --- a/frame_indexed.go +++ b/frame_indexed.go @@ -128,7 +128,7 @@ func (f *IndexedFrame[I]) String() string { // Print the first ten rows and the last ten rows if the IndexedFrame has more than 20 rows. if f.Len() > 20 { for i := 0; i < 10; i++ { - printRow(i, indexes[i]) + printRow(i+1, indexes[i]) } fmt.Fprintf(t, "...\t") for range names { @@ -136,11 +136,11 @@ func (f *IndexedFrame[I]) String() string { } fmt.Fprintln(t) // Print new line character. for i := 10; i > 0; i-- { - printRow(i, indexes[len(indexes)-i]) + printRow(len(indexes)-i, indexes[len(indexes)-i]) } } else { for i := 0; i < f.Len(); i++ { - printRow(i, indexes[i]) + printRow(i+1, indexes[i]) } } diff --git a/indicators.go b/indicators.go index be316b0..e083bae 100644 --- a/indicators.go +++ b/indicators.go @@ -49,40 +49,21 @@ func RSI(series *FloatSeries, periods int) *FloatSeries { // - LeadingA // - LeadingB // - Lagging -func Ichimoku(series *IndexedSeries[UnixTime], convPeriod, basePeriod, leadingPeriods int) *IndexedFrame[UnixTime] { +func Ichimoku(price *IndexedFrame[UnixTime], convPeriod, basePeriod, leadingPeriods int, frequency time.Duration) *IndexedFrame[UnixTime] { // TODO: make this run concurrently. - // Calculate the Conversion Line. - conv := series.Copy().Rolling(convPeriod).Max().Add(series.Copy().Rolling(convPeriod).Min()). - Map(func(_ UnixTime, _ int, val any) any { - return val.(float64) / float64(2) - }) - // Calculate the Base Line. - base := series.Copy().Rolling(basePeriod).Max().Add(series.Copy().Rolling(basePeriod).Min()). - Map(func(_ UnixTime, _ int, val any) any { - return val.(float64) / float64(2) - }) - - // Calculate the Leading Span A. - leadingA := conv.Copy().Rolling(leadingPeriods).Max().Add(base.Copy().Rolling(leadingPeriods).Max()). - Map(func(_ UnixTime, _ int, val any) any { - return val.(float64) / float64(2) - }) - // Calculate the Leading Span B. - leadingB := series.Copy().Rolling(leadingPeriods).Max().Add(series.Copy().Rolling(leadingPeriods).Min()). - Map(func(_ UnixTime, _ int, val any) any { - return val.(float64) / float64(2) - }) - - // Calculate the Lagging Span. - lagging := series.Copy().ShiftIndex(-leadingPeriods, UnixTimeStep(time.Hour)) + conv := price.Highs().Copy().Rolling(convPeriod).Max().Add(price.Lows().Copy().Rolling(convPeriod).Min()).DivFloat(2) + base := price.Highs().Copy().Rolling(basePeriod).Max().Add(price.Lows().Copy().Rolling(basePeriod).Min()).DivFloat(2) + lagging := price.Closes().Copy() + leadingA := conv.Copy().Add(base).DivFloat(2) + leadingB := price.Highs().Copy().Rolling(leadingPeriods).Max().Add(price.Lows().Copy().Rolling(leadingPeriods).Min()).DivFloat(2) // Return a DataFrame of the results. return NewIndexedFrame( conv.SetName("Conversion"), base.SetName("Base"), - leadingA.SetName("LeadingA"), - leadingB.SetName("LeadingB"), - lagging.SetName("Lagging"), + leadingA.SetName("LeadingA").ShiftIndex(leadingPeriods, UnixTimeStep(frequency)), + leadingB.SetName("LeadingB").ShiftIndex(leadingPeriods, UnixTimeStep(frequency)), + lagging.SetName("Lagging").ShiftIndex(-leadingPeriods, UnixTimeStep(frequency)), ) } diff --git a/series.go b/series.go index 342d05f..feb5aba 100644 --- a/series.go +++ b/series.go @@ -468,7 +468,7 @@ func (s *RollingSeries) Period(row int) []any { // // Will work with all signed int and float types. Ignores all other values. func (s *RollingSeries) Max() *Series { - return s.series.Map(func(i int, _ any) any { + return s.series.MapReverse(func(i int, _ any) any { period := s.Period(i) if len(period) == 0 { return 0 @@ -514,7 +514,7 @@ func (s *RollingSeries) Max() *Series { // // Will work with all signed int and float types. Ignores all other values. func (s *RollingSeries) Min() *Series { - return s.series.Map(func(i int, _ any) any { + return s.series.MapReverse(func(i int, _ any) any { period := s.Period(i) if len(period) == 0 { return 0 diff --git a/series_indexed.go b/series_indexed.go index 5bfb6fd..ca8720c 100644 --- a/series_indexed.go +++ b/series_indexed.go @@ -82,6 +82,17 @@ func (s *IndexedSeries[I]) Add(other *IndexedSeries[I]) *IndexedSeries[I] { return s } +func (s *IndexedSeries[I]) AddFloat(num float64) *IndexedSeries[I] { + for index, row := range s.index { + newValue, err := anymath.Add(s.series.Value(row), num) + if err != nil { + panic(fmt.Errorf("error adding values at index %v: %w", index, err)) + } + s.series.SetValue(row, newValue) + } + return s +} + // Copy returns a copy of this series. func (s *IndexedSeries[I]) Copy() *IndexedSeries[I] { return s.CopyRange(0, -1) @@ -124,6 +135,17 @@ func (s *IndexedSeries[I]) Div(other *IndexedSeries[I]) *IndexedSeries[I] { return s } +func (s *IndexedSeries[I]) DivFloat(num float64) *IndexedSeries[I] { + for index, row := range s.index { + newValue, err := anymath.Divide(s.series.Value(row), num) + if err != nil { + panic(fmt.Errorf("error dividing values at index %v: %w", index, err)) + } + s.series.SetValue(row, newValue) + } + return s +} + func (s *IndexedSeries[I]) Filter(f func(i int, val any) bool) *IndexedSeries[I] { _ = s.series.Filter(f) return s @@ -202,6 +224,17 @@ func (s *IndexedSeries[I]) Mul(other *IndexedSeries[I]) *IndexedSeries[I] { return s } +func (s *IndexedSeries[I]) MulFloat(num float64) *IndexedSeries[I] { + for index, row := range s.index { + newValue, err := anymath.Multiply(s.series.Value(row), num) + if err != nil { + panic(fmt.Errorf("error multiplying values at index %v: %w", index, err)) + } + s.series.SetValue(row, newValue) + } + return s +} + // Name returns the name of the series. func (s *IndexedSeries[I]) Name() string { return s.series.Name() @@ -374,6 +407,17 @@ func (s *IndexedSeries[I]) Sub(other *IndexedSeries[I]) *IndexedSeries[I] { return s } +func (s *IndexedSeries[I]) SubFloat(num float64) *IndexedSeries[I] { + for index, row := range s.index { + newValue, err := anymath.Subtract(s.series.Value(row), num) + if err != nil { + panic(fmt.Errorf("error subtracting values at index %v: %w", index, err)) + } + s.series.SetValue(row, newValue) + } + return s +} + // Value returns the value at the given row. func (s *IndexedSeries[I]) Value(i int) any { return s.series.Value(i) diff --git a/series_test.go b/series_test.go index fbe5e25..aeaacf2 100644 --- a/series_test.go +++ b/series_test.go @@ -140,7 +140,7 @@ func TestRollingSeries(t *testing.T) { } ema5Expected := []float64{1, 1.3333333333333333, 1.8888888888888888, 2.5925925925925926, 3.3950617283950617, 4.395061728395062, 5.395061728395062, 6.395061728395062, 7.395061728395062, 8.395061728395062} - ema5 := series.Rolling(5).EMA() // Take the 5 period exponential moving average. + ema5 := series.Copy().Rolling(5).EMA() // Take the 5 period exponential moving average. if ema5.Len() != 10 { t.Fatalf("Expected 10 rows, got %d", ema5.Len()) } @@ -149,6 +149,29 @@ func TestRollingSeries(t *testing.T) { t.Errorf("(%d)\tExpected %f, got %v", i, ema5Expected[i], val) } } + + // Test min and max functions + minExpected := []float64{1, 1, 1, 1, 1, 2, 3, 4, 5, 6} + min := series.Copy().Rolling(5).Min() + if min.Len() != 10 { + t.Fatalf("Expected 10 rows, got %d", min.Len()) + } + for i := 0; i < 10; i++ { + if val := min.Float(i); val != minExpected[i] { + t.Errorf("(%d)\tExpected %f, got %v", i, minExpected[i], val) + } + } + + maxExpected := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + max := series.Copy().Rolling(5).Max() + if max.Len() != 10 { + t.Fatalf("Expected 10 rows, got %d", max.Len()) + } + for i := 0; i < 10; i++ { + if val := max.Float(i); val != maxExpected[i] { + t.Errorf("(%d)\tExpected %f, got %v", i, maxExpected[i], val) + } + } } func TestIndexedSeriesInsert(t *testing.T) { @@ -239,3 +262,31 @@ func TestIndexedSeries(t *testing.T) { t.Errorf("Expected value at index 2018-01-01 to be 1.0, got %v", timeIndexed.ValueIndex(index)) } } + +func TestIndexedOperations(t *testing.T) { + indexed := NewIndexedSeries("test", map[UnixTime]float64{ + UnixTime(0): 1.0, + UnixTime(1): 2.0, + UnixTime(2): 3.0, + UnixTime(3): 4.0, + UnixTime(4): 5.0, + }) + + added := indexed.Copy().Add(indexed) + expected := []float64{2.0, 4.0, 6.0, 8.0, 10.0} + if added.Len() != 5 { + t.Fatalf("Expected 5 rows, got %d", added.Len()) + } + for i := 0; i < 5; i++ { + if val := added.Float(i); val != expected[i] { + t.Errorf("(%d)\tExpected %f, got %v", i, expected[i], val) + } + } + + added.DivFloat(2.0) + for i := 0; i < 5; i++ { + if val := added.Float(i); !EqualApprox(val, indexed.Float(i)) { + t.Errorf("(%d)\tExpected %f, got %v", i, indexed.Float(i), val) + } + } +} diff --git a/trader.go b/trader.go index d03be99..dfceac7 100644 --- a/trader.go +++ b/trader.go @@ -161,21 +161,21 @@ func (t *Trader) fetchData() { } } -func (t *Trader) Buy(units float64) { - t.closeOrdersAndPositions() +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, 0, 0) + t.Broker.Order(Market, t.Symbol, units, 0, stopLoss, takeProfit) t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{units, false}) } -func (t *Trader) Sell(units float64) { - t.closeOrdersAndPositions() +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, 0, 0) + t.Broker.Order(Market, t.Symbol, -units, 0, stopLoss, takeProfit) t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{-units, false}) } -func (t *Trader) closeOrdersAndPositions() { +func (t *Trader) CloseOrdersAndPositions() { for _, order := range t.Broker.OpenOrders() { if order.Symbol() == t.Symbol { t.Log.Printf("Cancelling order: %v units", order.Units()) @@ -191,6 +191,26 @@ func (t *Trader) closeOrdersAndPositions() { } } +func (t *Trader) IsLong() bool { + positions := t.Broker.OpenPositions() + if len(positions) <= 0 { + return false + } else if len(positions) > 1 { + panic("cannot call IsLong with hedging enabled") + } + return positions[0].Units() > 0 +} + +func (t *Trader) IsShort() bool { + positions := t.Broker.OpenPositions() + if len(positions) <= 0 { + return false + } else if len(positions) > 1 { + panic("cannot call IsShort with hedging enabled") + } + return positions[0].Units() < 0 +} + type TraderConfig struct { Broker Broker Strategy Strategy