Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Represent TimeSeries as an interface #14

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
37 changes: 19 additions & 18 deletions indicator_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
4 changes: 2 additions & 2 deletions indicator_cci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions indicator_derivative_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
4 changes: 2 additions & 2 deletions indicator_relative_vigor_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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),
}
Expand Down
2 changes: 1 addition & 1 deletion rule_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 4 additions & 4 deletions testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -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 {
Expand Down
42 changes: 33 additions & 9 deletions timeseries.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,36 @@ import (
)

// TimeSeries represents an array of candles
type TimeSeries struct {
Candles []*Candle
type TimeSeries interface {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea behind this, but, because the original TimeSeries struct was exported, I don't think we can just replace the struct def with an interface def, because it will break every project where someone was previously using a concrete TimeSeries struct. I think what we need to do is invert the naming here, s.t. the we give the interface another name (maybe ITimeSeries to borrow from C++, but I'm sure you can suggest something better) and keep TimeSeries bound to the struct def. WDYT?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there must be a more elegant way. Sorry for bothering you too early. I need to think a bit more about it all.

LastIndex() int
FirstCandle() *Candle
LastCandle() *Candle
GetCandle(int) *Candle
GetCandleData() []*Candle
AddCandle(*Candle) bool
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in terms of the methods in this interface, I want to make sure we're being deliberate about what we choose, because once we commit to these we can't remove or change any of them. Based on the usages in this repo, I think we could get away with something like this:

type ITimeSeries interface {
  AddCandle(*Candle) bool
  Candles() []*Candle
}

the others seem like convenience methods to access data at a particular location.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand your concerns. I guess that was a bit premature decision, disregarding legacy code. And presenting as small interface as possible is also a valid point. As for motivation behind this PR, it's just what I think is usually right: parts that "do" or "provide" stuff are usually interfaces, and parts that circulate around as DTOs are usually structs. The time series looks like a "provider" of Candles to me, hence it's an interface. But you're right, I should have thought it all through more carefully. For instance, some indicators use internal caches that also are slices. If we decide to go the whole way, cache must be more abstract too. For example, "infinite" time series implemented as some kind of ring buffer will not use too much memory, but cache as a slice definitely will.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries! feel free to iterate on this and tag me for another review. Thanks for your contribution! It's exciting to see folks wanting to work on this and make it better

}

// 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"))
}
Expand All @@ -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]
}
Expand All @@ -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
}
13 changes: 9 additions & 4 deletions timeseries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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) {
Expand All @@ -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())
})
}

Expand All @@ -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())
Expand Down