Fixed MORE bugs

This commit is contained in:
Luke I. Wilson 2023-05-24 15:58:59 -05:00
parent 46fd55ab8d
commit b5434fb5de
10 changed files with 205 additions and 55 deletions

8
.vscode/launch.json vendored
View File

@ -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}"
}
]
}

View File

@ -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()

View File

@ -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,
}))

View File

@ -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)
}
}

View File

@ -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])
}
}

View File

@ -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)),
)
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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