mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-14 16:03:51 +00:00
Fixed MORE bugs
This commit is contained in:
parent
46fd55ab8d
commit
b5434fb5de
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
@ -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()
|
||||
|
@ -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,
|
||||
}))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)),
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
34
trader.go
34
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user