diff --git a/analysis.go b/analysis.go index 92a055c..1d69dfe 100644 --- a/analysis.go +++ b/analysis.go @@ -131,7 +131,7 @@ func (apa AverageProfitAnalysis) Analyze(record *TradingRecord) float64 { // and held until the date on the last trade of the trading record. It's useful for comparing the performance of your strategy // against a simple long position. type BuyAndHoldAnalysis struct { - TimeSeries *TimeSeries + TimeSeries TimeSeries StartingMoney float64 } @@ -143,14 +143,14 @@ func (baha BuyAndHoldAnalysis) Analyze(record *TradingRecord) float64 { openOrder := Order{ Side: BUY, - Amount: big.NewDecimal(baha.StartingMoney).Div(baha.TimeSeries.Candles[0].ClosePrice), - Price: baha.TimeSeries.Candles[0].ClosePrice, + Amount: big.NewDecimal(baha.StartingMoney).Div(baha.TimeSeries.FirstCandle().ClosePrice), + Price: baha.TimeSeries.FirstCandle().ClosePrice, } closeOrder := Order{ Side: SELL, Amount: openOrder.Amount, - Price: baha.TimeSeries.Candles[len(baha.TimeSeries.Candles)-1].ClosePrice, + Price: baha.TimeSeries.LastCandle().ClosePrice, } pos := NewPosition(openOrder) diff --git a/go.mod b/go.mod index 53488fa..1dca801 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/davecgh/go-spew v1.1.0 github.com/pmezard/go-difflib v1.0.0 - github.com/sdcoffey/big v0.0.0-20180413224939-438f3d83db4c - github.com/stretchr/testify v1.2.1 + github.com/sdcoffey/big v0.0.0-20190619052155-4007473142bc + github.com/stretchr/testify v1.4.0 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect honnef.co/go/tools v0.0.0-20190412205916-e87e8279b4cd // indirect ) diff --git a/go.sum b/go.sum index 6b93403..0c8643f 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sdcoffey/big v0.0.0-20180413224939-438f3d83db4c h1:avDKoFuBXMQAcu0sxdpbS88WAiPt2e9YtSuEWfOPj7E= github.com/sdcoffey/big v0.0.0-20180413224939-438f3d83db4c/go.mod h1:WzOYJJhNOp7m1u1MrNQaYSHmgqHGkZThC+VgDqdFl0I= +github.com/sdcoffey/big v0.0.0-20190619052155-4007473142bc h1:vVBtQkHPB1I/NA8u0nmwDISEz8hj9vR5NECHdmRQw38= +github.com/sdcoffey/big v0.0.0-20190619052155-4007473142bc/go.mod h1:WzOYJJhNOp7m1u1MrNQaYSHmgqHGkZThC+VgDqdFl0I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -16,5 +21,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190412205916-e87e8279b4cd h1:hnvxEW8n8IJasi4s1HJpanZ1ebTAdsjxzbZPO8Viw+s= honnef.co/go/tools v0.0.0-20190412205916-e87e8279b4cd/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/indicator_basic.go b/indicator_basic.go index e1ebfab..826c150 100644 --- a/indicator_basic.go +++ b/indicator_basic.go @@ -3,87 +3,88 @@ package techan import "github.com/sdcoffey/big" type volumeIndicator struct { - *TimeSeries + TimeSeries } // NewVolumeIndicator returns an indicator which returns the volume of a candle for a given index -func NewVolumeIndicator(series *TimeSeries) Indicator { +func NewVolumeIndicator(series TimeSeries) Indicator { return volumeIndicator{series} } func (vi volumeIndicator) Calculate(index int) big.Decimal { - return vi.Candles[index].Volume + return vi.GetCandle(index).Volume } type closePriceIndicator struct { - *TimeSeries + TimeSeries } // NewClosePriceIndicator returns an Indicator which returns the close price of a candle for a given index -func NewClosePriceIndicator(series *TimeSeries) Indicator { +func NewClosePriceIndicator(series TimeSeries) Indicator { return closePriceIndicator{series} } func (cpi closePriceIndicator) Calculate(index int) big.Decimal { - return cpi.Candles[index].ClosePrice + return cpi.GetCandle(index).ClosePrice } type highPriceIndicator struct { - *TimeSeries + TimeSeries } // NewHighPriceIndicator returns an Indicator which returns the high price of a candle for a given index -func NewHighPriceIndicator(series *TimeSeries) Indicator { +func NewHighPriceIndicator(series TimeSeries) Indicator { return highPriceIndicator{ series, } } func (hpi highPriceIndicator) Calculate(index int) big.Decimal { - return hpi.Candles[index].MaxPrice + return hpi.GetCandle(index).MaxPrice } type lowPriceIndicator struct { - *TimeSeries + TimeSeries } // NewLowPriceIndicator returns an Indicator which returns the low price of a candle for a given index -func NewLowPriceIndicator(series *TimeSeries) Indicator { +func NewLowPriceIndicator(series TimeSeries) Indicator { return lowPriceIndicator{ series, } } func (lpi lowPriceIndicator) Calculate(index int) big.Decimal { - return lpi.Candles[index].MinPrice + return lpi.GetCandle(index).MinPrice } type openPriceIndicator struct { - *TimeSeries + TimeSeries } // NewOpenPriceIndicator returns an Indicator which returns the open price of a candle for a given index -func NewOpenPriceIndicator(series *TimeSeries) Indicator { +func NewOpenPriceIndicator(series TimeSeries) Indicator { return openPriceIndicator{ series, } } func (opi openPriceIndicator) Calculate(index int) big.Decimal { - return opi.Candles[index].OpenPrice + return opi.GetCandle(index).OpenPrice } type typicalPriceIndicator struct { - *TimeSeries + TimeSeries } // NewTypicalPriceIndicator returns an Indicator which returns the typical price of a candle for a given index. // The typical price is an average of the high, low, and close prices for a given candle. -func NewTypicalPriceIndicator(series *TimeSeries) Indicator { +func NewTypicalPriceIndicator(series TimeSeries) Indicator { return typicalPriceIndicator{series} } func (tpi typicalPriceIndicator) Calculate(index int) big.Decimal { - numerator := tpi.Candles[index].MaxPrice.Add(tpi.Candles[index].MinPrice).Add(tpi.Candles[index].ClosePrice) + c := tpi.GetCandle(index) + numerator := c.MaxPrice.Add(c.MinPrice).Add(c.ClosePrice) return numerator.Div(big.NewFromString("3")) } diff --git a/indicator_cci.go b/indicator_cci.go index ac40fda..5e780ed 100644 --- a/indicator_cci.go +++ b/indicator_cci.go @@ -3,13 +3,13 @@ package techan import "github.com/sdcoffey/big" type commidityChannelIndexIndicator struct { - series *TimeSeries + series TimeSeries window int } // NewCCIIndicator Returns a new Commodity Channel Index Indicator // http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:commodity_channel_index_cci -func NewCCIIndicator(ts *TimeSeries, window int) Indicator { +func NewCCIIndicator(ts TimeSeries, window int) Indicator { return commidityChannelIndexIndicator{ series: ts, window: window, diff --git a/indicator_derivative_test.go b/indicator_derivative_test.go index 5001716..d596d9d 100644 --- a/indicator_derivative_test.go +++ b/indicator_derivative_test.go @@ -19,8 +19,8 @@ func TestDerivativeIndicator(t *testing.T) { t.Run("returns the derivative", func(t *testing.T) { assert.EqualValues(t, "0", indicator.Calculate(1).String()) - for i := 2; i < len(series.Candles); i++ { - expected := series.Candles[i-2].ClosePrice + for i := 2; i < series.LastIndex(); i++ { + expected := series.GetCandle(i - 2).ClosePrice assert.EqualValues(t, expected.String(), indicator.Calculate(i).String()) } diff --git a/indicator_relative_vigor_index.go b/indicator_relative_vigor_index.go index 932ecc5..cd3a067 100644 --- a/indicator_relative_vigor_index.go +++ b/indicator_relative_vigor_index.go @@ -11,7 +11,7 @@ type relativeVigorIndexIndicator struct { // a sercurity. Relative Vigor Index is simply the difference of the previous four days' close and open prices divided // by the difference between the previous four days high and low prices. A more in-depth explanation of relative vigor // index can be found here: https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/relative-vigor-index -func NewRelativeVigorIndexIndicator(series *TimeSeries) Indicator { +func NewRelativeVigorIndexIndicator(series TimeSeries) Indicator { return relativeVigorIndexIndicator{ numerator: NewDifferenceIndicator(NewClosePriceIndicator(series), NewOpenPriceIndicator(series)), denominator: NewDifferenceIndicator(NewHighPriceIndicator(series), NewLowPriceIndicator(series)), @@ -48,7 +48,7 @@ type relativeVigorIndexSignalLine struct { // NewRelativeVigorSignalLine returns an Indicator intended to be used in conjunction with Relative vigor index, which // returns the average value of the last 4 indices of the RVI indicator. -func NewRelativeVigorSignalLine(series *TimeSeries) Indicator { +func NewRelativeVigorSignalLine(series TimeSeries) Indicator { return relativeVigorIndexSignalLine{ relativeVigorIndex: NewRelativeVigorIndexIndicator(series), } diff --git a/rule_stop.go b/rule_stop.go index 063840c..aaa89ed 100644 --- a/rule_stop.go +++ b/rule_stop.go @@ -9,7 +9,7 @@ type stopLossRule struct { // NewStopLossRule returns a new rule that is satisfied when the given loss tolerance (a percentage) is met or exceeded. // Loss tolerance should be a value between -1 and 1. -func NewStopLossRule(series *TimeSeries, lossTolerance float64) Rule { +func NewStopLossRule(series TimeSeries, lossTolerance float64) Rule { return stopLossRule{ Indicator: NewClosePriceIndicator(series), tolerance: big.NewDecimal(lossTolerance), diff --git a/testutils.go b/testutils.go index 4927db3..ea17911 100644 --- a/testutils.go +++ b/testutils.go @@ -14,7 +14,7 @@ import ( var candleIndex int -func randomTimeSeries(size int) *TimeSeries { +func randomTimeSeries(size int) TimeSeries { vals := make([]string, size) rand.Seed(time.Now().Unix()) for i := 0; i < size; i++ { @@ -34,7 +34,7 @@ func randomTimeSeries(size int) *TimeSeries { return mockTimeSeries(vals...) } -func mockTimeSeriesOCHL(values ...[]string) *TimeSeries { +func mockTimeSeriesOCHL(values ...[]string) TimeSeries { ts := NewTimeSeries() for i, ochl := range values { candle := NewCandle(NewTimePeriod(time.Unix(int64(i), 0), time.Second)) @@ -50,7 +50,7 @@ func mockTimeSeriesOCHL(values ...[]string) *TimeSeries { return ts } -func mockTimeSeries(values ...string) *TimeSeries { +func mockTimeSeries(values ...string) TimeSeries { ts := NewTimeSeries() for _, val := range values { candle := NewCandle(NewTimePeriod(time.Unix(int64(candleIndex), 0), time.Second)) @@ -68,7 +68,7 @@ func mockTimeSeries(values ...string) *TimeSeries { return ts } -func mockTimeSeriesFl(values ...float64) *TimeSeries { +func mockTimeSeriesFl(values ...float64) TimeSeries { strVals := make([]string, len(values)) for i, val := range values { diff --git a/timeseries.go b/timeseries.go index dca63e6..4bd8c42 100644 --- a/timeseries.go +++ b/timeseries.go @@ -5,21 +5,36 @@ import ( ) // TimeSeries represents an array of candles -type TimeSeries struct { - Candles []*Candle +type TimeSeries interface { + LastIndex() int + FirstCandle() *Candle + LastCandle() *Candle + GetCandle(int) *Candle + GetCandleData() []*Candle + AddCandle(*Candle) bool } // NewTimeSeries returns a new, empty, TimeSeries -func NewTimeSeries() (t *TimeSeries) { - t = new(TimeSeries) - t.Candles = make([]*Candle, 0) +func NewTimeSeries() TimeSeries { + return new(BaseTimeSeries) +} + +// BaseTimeSeries implements TimeSeries using in-memory slice +type BaseTimeSeries struct { + Candles []*Candle +} + +func (ts *BaseTimeSeries) GetCandle(idx int) *Candle { + return ts.Candles[idx] +} - return t +func (ts *BaseTimeSeries) GetCandleData() []*Candle { + return ts.Candles } // AddCandle adds the given candle to this TimeSeries if it is not nil and after the last candle in this timeseries. // If the candle is added, AddCandle will return true, otherwise it will return false. -func (ts *TimeSeries) AddCandle(candle *Candle) bool { +func (ts *BaseTimeSeries) AddCandle(candle *Candle) bool { if candle == nil { panic(fmt.Errorf("error adding Candle: candle cannot be nil")) } @@ -32,8 +47,17 @@ func (ts *TimeSeries) AddCandle(candle *Candle) bool { return false } +// FirstCandle will return the firstCandle in this series, or nil if this series is empty +func (ts *BaseTimeSeries) FirstCandle() *Candle { + if len(ts.Candles) > 0 { + return ts.Candles[0] + } + + return nil +} + // LastCandle will return the lastCandle in this series, or nil if this series is empty -func (ts *TimeSeries) LastCandle() *Candle { +func (ts *BaseTimeSeries) LastCandle() *Candle { if len(ts.Candles) > 0 { return ts.Candles[len(ts.Candles)-1] } @@ -42,6 +66,6 @@ func (ts *TimeSeries) LastCandle() *Candle { } // LastIndex will return the index of the last candle in this series -func (ts *TimeSeries) LastIndex() int { +func (ts *BaseTimeSeries) LastIndex() int { return len(ts.Candles) - 1 } diff --git a/timeseries_test.go b/timeseries_test.go index 2153ae0..aa56663 100644 --- a/timeseries_test.go +++ b/timeseries_test.go @@ -16,6 +16,11 @@ func TestTimeSeries_AddCandle(t *testing.T) { }) }) + t.Run("Empty timeseries returns nil candle", func(t *testing.T) { + ts := NewTimeSeries() + assert.Nil(t, ts.FirstCandle()) + }) + t.Run("Adds candle if last is nil", func(t *testing.T) { ts := NewTimeSeries() @@ -24,7 +29,7 @@ func TestTimeSeries_AddCandle(t *testing.T) { ts.AddCandle(candle) - assert.Len(t, ts.Candles, 1) + assert.Len(t, ts.GetCandleData(), 1) }) t.Run("Does not add candle if before last candle", func(t *testing.T) { @@ -42,8 +47,8 @@ func TestTimeSeries_AddCandle(t *testing.T) { ts.AddCandle(nextCandle) - assert.Len(t, ts.Candles, 1) - assert.EqualValues(t, now.UnixNano(), ts.Candles[0].Period.Start.UnixNano()) + assert.Len(t, ts.GetCandleData(), 1) + assert.EqualValues(t, now.UnixNano(), ts.FirstCandle().Period.Start.UnixNano()) }) } @@ -65,7 +70,7 @@ func TestTimeSeries_LastCandle(t *testing.T) { ts.AddCandle(newCandle) - assert.Len(t, ts.Candles, 2) + assert.Len(t, ts.GetCandleData(), 2) assert.EqualValues(t, next.UnixNano(), ts.LastCandle().Period.Start.UnixNano()) assert.EqualValues(t, 2, ts.LastCandle().ClosePrice.Float())