From 68c34dabae6dc92d8c89714e914bcd464e33e824 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Fri, 11 Oct 2024 18:40:09 -0700 Subject: [PATCH] Envelope trend indicator and Envelope strategy are added. (#233) # Describe Request Envelope trend indicator and Envelope strategy are added. Fixed #228 # Change Type New feature. ## Summary by CodeRabbit - **New Features** - Introduced a new `Envelope` indicator and multiple new strategies, enhancing the library's capabilities. - Added dedicated test data in CSV format for better validation of indicators and strategies. - **Documentation** - Added detailed documentation for the new `Envelope` type and its methods. --- README.md | 1 + strategy/README.md | 8 +- strategy/testdata/x | 1 + strategy/trend/README.md | 63 +++++ strategy/trend/envelope_strategy.go | 117 ++++++++ strategy/trend/envelope_strategy_test.go | 55 ++++ strategy/trend/testdata/envelope_strategy.csv | 252 ++++++++++++++++++ trend/README.md | 88 ++++++ trend/envelope.go | 82 ++++++ trend/envelope_test.go | 95 +++++++ trend/testdata/envelope_ema.csv | 252 ++++++++++++++++++ trend/testdata/envelope_sma.csv | 252 ++++++++++++++++++ 12 files changed, 1262 insertions(+), 4 deletions(-) create mode 100644 strategy/testdata/x create mode 100644 strategy/trend/envelope_strategy.go create mode 100644 strategy/trend/envelope_strategy_test.go create mode 100644 strategy/trend/testdata/envelope_strategy.csv create mode 100644 trend/envelope.go create mode 100644 trend/envelope_test.go create mode 100644 trend/testdata/envelope_ema.csv create mode 100644 trend/testdata/envelope_sma.csv diff --git a/README.md b/README.md index 034382a..72cc8e8 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The following list of indicators are currently supported by this package: - [Balance of Power (BoP)](trend/README.md#type-bop) - Chande Forecast Oscillator (CFO) - [Community Channel Index (CCI)](trend/README.md#type-cci) +- [Envelope](trend/README.md#type-envelope) - [Hull Moving Average (HMA)](trend/README.md#type-hma) - [Double Exponential Moving Average (DEMA)](trend/README.md#type-dema) - [Exponential Moving Average (EMA)](trend/README.md#type-ema) diff --git a/strategy/README.md b/strategy/README.md index b82fc36..fbd9c86 100644 --- a/strategy/README.md +++ b/strategy/README.md @@ -35,7 +35,7 @@ The information provided on this project is strictly for informational purposes - [type Action](<#Action>) - [func \(a Action\) Annotation\(\) string](<#Action.Annotation>) - [type AndStrategy](<#AndStrategy>) - - [func NewAndStrategy\(name string\) \*AndStrategy](<#NewAndStrategy>) + - [func NewAndStrategy\(name string, strategies ...Strategy\) \*AndStrategy](<#NewAndStrategy>) - [func \(a \*AndStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#AndStrategy.Compute>) - [func \(a \*AndStrategy\) Name\(\) string](<#AndStrategy.Name>) - [func \(a \*AndStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#AndStrategy.Report>) @@ -51,7 +51,7 @@ The information provided on this project is strictly for informational purposes - [func \(a \*MajorityStrategy\) Name\(\) string](<#MajorityStrategy.Name>) - [func \(a \*MajorityStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#MajorityStrategy.Report>) - [type OrStrategy](<#OrStrategy>) - - [func NewOrStrategy\(name string\) \*OrStrategy](<#NewOrStrategy>) + - [func NewOrStrategy\(name string, strategies ...Strategy\) \*OrStrategy](<#NewOrStrategy>) - [func \(a \*OrStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#OrStrategy.Compute>) - [func \(a \*OrStrategy\) Name\(\) string](<#OrStrategy.Name>) - [func \(a \*OrStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#OrStrategy.Report>) @@ -194,7 +194,7 @@ type AndStrategy struct { ### func [NewAndStrategy]() ```go -func NewAndStrategy(name string) *AndStrategy +func NewAndStrategy(name string, strategies ...Strategy) *AndStrategy ``` NewAndStrategy function initializes an empty and strategies group with the given name. @@ -347,7 +347,7 @@ type OrStrategy struct { ### func [NewOrStrategy]() ```go -func NewOrStrategy(name string) *OrStrategy +func NewOrStrategy(name string, strategies ...Strategy) *OrStrategy ``` NewOrStrategy function initializes an empty or strategies group with the given name. diff --git a/strategy/testdata/x b/strategy/testdata/x new file mode 100644 index 0000000..850c889 --- /dev/null +++ b/strategy/testdata/x @@ -0,0 +1 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/strategy/trend/README.md b/strategy/trend/README.md index f61c4b8..a880ee9 100644 --- a/strategy/trend/README.md +++ b/strategy/trend/README.md @@ -51,6 +51,12 @@ The information provided on this project is strictly for informational purposes - [func \(d \*DemaStrategy\) Compute\(c \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#DemaStrategy.Compute>) - [func \(\*DemaStrategy\) Name\(\) string](<#DemaStrategy.Name>) - [func \(d \*DemaStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#DemaStrategy.Report>) +- [type EnvelopeStrategy](<#EnvelopeStrategy>) + - [func NewEnvelopeStrategy\(\) \*EnvelopeStrategy](<#NewEnvelopeStrategy>) + - [func NewEnvelopeStrategyWith\(envelope \*trend.Envelope\[float64\]\) \*EnvelopeStrategy](<#NewEnvelopeStrategyWith>) + - [func \(e \*EnvelopeStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#EnvelopeStrategy.Compute>) + - [func \(e \*EnvelopeStrategy\) Name\(\) string](<#EnvelopeStrategy.Name>) + - [func \(e \*EnvelopeStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#EnvelopeStrategy.Report>) - [type GoldenCrossStrategy](<#GoldenCrossStrategy>) - [func NewGoldenCrossStrategy\(\) \*GoldenCrossStrategy](<#NewGoldenCrossStrategy>) - [func NewGoldenCrossStrategyWith\(fastPeriod, slowPeriod int\) \*GoldenCrossStrategy](<#NewGoldenCrossStrategyWith>) @@ -436,6 +442,63 @@ func (d *DemaStrategy) Report(c <-chan *asset.Snapshot) *helper.Report Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + +## type [EnvelopeStrategy]() + +EnvelopeStrategy represents the configuration parameters for calculating the Envelope strategy. When the closing is above the upper band suggests a Sell recommendation, and when the closing is below the lower band suggests a buy recommendation. + +```go +type EnvelopeStrategy struct { + // Envelope is the envelope indicator instance. + Envelope *trend.Envelope[float64] +} +``` + + +### func [NewEnvelopeStrategy]() + +```go +func NewEnvelopeStrategy() *EnvelopeStrategy +``` + +NewEnvelopeStrategy function initializes a new Envelope strategy with the default parameters. + + +### func [NewEnvelopeStrategyWith]() + +```go +func NewEnvelopeStrategyWith(envelope *trend.Envelope[float64]) *EnvelopeStrategy +``` + +NewEnvelopeStrategyWith function initializes a new Envelope strategy with the given Envelope instance. + + +### func \(\*EnvelopeStrategy\) [Compute]() + +```go +func (e *EnvelopeStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*EnvelopeStrategy\) [Name]() + +```go +func (e *EnvelopeStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*EnvelopeStrategy\) [Report]() + +```go +func (e *EnvelopeStrategy) Report(c <-chan *asset.Snapshot) *helper.Report +``` + +Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + ## type [GoldenCrossStrategy]() diff --git a/strategy/trend/envelope_strategy.go b/strategy/trend/envelope_strategy.go new file mode 100644 index 0000000..6eed4b3 --- /dev/null +++ b/strategy/trend/envelope_strategy.go @@ -0,0 +1,117 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend + +import ( + "fmt" + + "github.com/cinar/indicator/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/strategy" + "github.com/cinar/indicator/v2/trend" +) + +// EnvelopeStrategy represents the configuration parameters for calculating the Envelope strategy. When the closing +// is above the upper band suggests a Sell recommendation, and when the closing is below the lower band suggests a +// buy recommendation. +type EnvelopeStrategy struct { + // Envelope is the envelope indicator instance. + Envelope *trend.Envelope[float64] +} + +// NewEnvelopeStrategy function initializes a new Envelope strategy with the default parameters. +func NewEnvelopeStrategy() *EnvelopeStrategy { + return NewEnvelopeStrategyWith( + trend.NewEnvelopeWithSma[float64](), + ) +} + +// NewEnvelopeStrategyWith function initializes a new Envelope strategy with the given Envelope instance. +func NewEnvelopeStrategyWith(envelope *trend.Envelope[float64]) *EnvelopeStrategy { + return &EnvelopeStrategy{ + Envelope: envelope, + } +} + +// Name returns the name of the strategy. +func (e *EnvelopeStrategy) Name() string { + return fmt.Sprintf("Envelope Strategy (%s)", e.Envelope.String()) +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (e *EnvelopeStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + closingsSplice := helper.Duplicate( + asset.SnapshotsAsClosings(snapshots), + 2, + ) + + closingsSplice[1] = helper.Skip(closingsSplice[1], e.Envelope.IdlePeriod()) + + uppers, middles, lowers := e.Envelope.Compute(closingsSplice[0]) + go helper.Drain(middles) + + actions := helper.Operate3(uppers, lowers, closingsSplice[1], func(upper, lower, closing float64) strategy.Action { + // When the closing is below the lower band suggests a buy recommendation. + if closing < lower { + return strategy.Buy + } + + // When the closing is above the upper band suggests a Sell recommendation. + if closing > upper { + return strategy.Sell + } + + return strategy.Hold + }) + + // Envelope start only after a full period. + actions = helper.Shift(actions, e.Envelope.IdlePeriod(), strategy.Hold) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (e *EnvelopeStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> closings[0] -> closings + // closings[1] -> envelope -> upper + // -> middle + // -> lower + // snapshots[2] -> actions -> annotations + // -> outcomes + // + snapshotsSplice := helper.Duplicate(c, 3) + + dates := helper.Skip( + asset.SnapshotsAsDates(snapshotsSplice[0]), + e.Envelope.IdlePeriod(), + ) + + closingsSplice := helper.Duplicate(asset.SnapshotsAsClosings(snapshotsSplice[1]), 2) + closingsSplice[0] = helper.Skip(closingsSplice[0], e.Envelope.IdlePeriod()) + + uppers, middles, lowers := e.Envelope.Compute(closingsSplice[1]) + + actions, outcomes := strategy.ComputeWithOutcome(e, snapshotsSplice[2]) + actions = helper.Skip(actions, e.Envelope.IdlePeriod()) + outcomes = helper.Skip(outcomes, e.Envelope.IdlePeriod()) + + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(e.Name(), dates) + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[0])) + report.AddColumn(helper.NewNumericReportColumn("Upper", uppers)) + report.AddColumn(helper.NewNumericReportColumn("Middle", middles)) + report.AddColumn(helper.NewNumericReportColumn("Lower", lowers)) + report.AddColumn(helper.NewAnnotationReportColumn(annotations)) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 1) + + return report +} diff --git a/strategy/trend/envelope_strategy_test.go b/strategy/trend/envelope_strategy_test.go new file mode 100644 index 0000000..2efdbb2 --- /dev/null +++ b/strategy/trend/envelope_strategy_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend_test + +import ( + "os" + "testing" + + "github.com/cinar/indicator/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/strategy" + "github.com/cinar/indicator/v2/strategy/trend" +) + +func TestEnvelopeStrategy(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/envelope_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + envelope := trend.NewEnvelopeStrategy() + actual := envelope.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestEnvelopeStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + envelope := trend.NewEnvelopeStrategy() + report := envelope.Report(snapshots) + + fileName := "envelope_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/trend/testdata/envelope_strategy.csv b/strategy/trend/testdata/envelope_strategy.csv new file mode 100644 index 0000000..792409e --- /dev/null +++ b/strategy/trend/testdata/envelope_strategy.csv @@ -0,0 +1,252 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/trend/README.md b/trend/README.md index 5a4bd0f..935adde 100644 --- a/trend/README.md +++ b/trend/README.md @@ -49,6 +49,13 @@ The information provided on this project is strictly for informational purposes - [func \(e \*Ema\[T\]\) Compute\(c \<\-chan T\) \<\-chan T](<#Ema[T].Compute>) - [func \(e \*Ema\[T\]\) IdlePeriod\(\) int](<#Ema[T].IdlePeriod>) - [func \(e \*Ema\[T\]\) String\(\) string](<#Ema[T].String>) +- [type Envelope](<#Envelope>) + - [func NewEnvelope\[T helper.Number\]\(ma Ma\[T\], percentage T\) \*Envelope\[T\]](<#NewEnvelope>) + - [func NewEnvelopeWithEma\[T helper.Number\]\(\) \*Envelope\[T\]](<#NewEnvelopeWithEma>) + - [func NewEnvelopeWithSma\[T helper.Number\]\(\) \*Envelope\[T\]](<#NewEnvelopeWithSma>) + - [func \(e \*Envelope\[T\]\) Compute\(closings \<\-chan T\) \(\<\-chan T, \<\-chan T, \<\-chan T\)](<#Envelope[T].Compute>) + - [func \(e \*Envelope\[T\]\) IdlePeriod\(\) int](<#Envelope[T].IdlePeriod>) + - [func \(e \*Envelope\[T\]\) String\(\) string](<#Envelope[T].String>) - [type Hma](<#Hma>) - [func NewHmaWithPeriod\[T helper.Number\]\(period int\) \*Hma\[T\]](<#NewHmaWithPeriod>) - [func \(h \*Hma\[T\]\) Compute\(values \<\-chan T\) \<\-chan T](<#Hma[T].Compute>) @@ -172,6 +179,18 @@ const ( ) ``` + + +```go +const ( + // DefaultEnvelopePercentage is the default envelope percentage of 20%. + DefaultEnvelopePercentage = 20 + + // DefaultEnvelopePeriod is the default envelope period of 20. + DefaultEnvelopePeriod = 20 +) +``` + ```go @@ -627,6 +646,75 @@ func (e *Ema[T]) String() string String is the string representation of the EMA. + +## type [Envelope]() + +Envelope represents the parameters neededd to calcualte the Envelope. + +```go +type Envelope[T helper.Number] struct { + // Ma is the moving average used. + Ma Ma[T] + + // Percentage is the envelope percentage. + Percentage T +} +``` + + +### func [NewEnvelope]() + +```go +func NewEnvelope[T helper.Number](ma Ma[T], percentage T) *Envelope[T] +``` + +NewEnvelope function initializes a new Envelope instance with the default parameters. + + +### func [NewEnvelopeWithEma]() + +```go +func NewEnvelopeWithEma[T helper.Number]() *Envelope[T] +``` + +NewEnvelopeWithEma function initializes a new Envelope instance using EMA. + + +### func [NewEnvelopeWithSma]() + +```go +func NewEnvelopeWithSma[T helper.Number]() *Envelope[T] +``` + +NewEnvelopeWithSma function initalizes a new Envelope instance using SMA. + + +### func \(\*Envelope\[T\]\) [Compute]() + +```go +func (e *Envelope[T]) Compute(closings <-chan T) (<-chan T, <-chan T, <-chan T) +``` + +Compute function takes a channel of numbers and computes the Envelope over the specified period. + + +### func \(\*Envelope\[T\]\) [IdlePeriod]() + +```go +func (e *Envelope[T]) IdlePeriod() int +``` + +IdlePeriod is the initial period that Envelope yield any results. + + +### func \(\*Envelope\[T\]\) [String]() + +```go +func (e *Envelope[T]) String() string +``` + +String is the string representation of the Envelope. + ## type [Hma]() diff --git a/trend/envelope.go b/trend/envelope.go new file mode 100644 index 0000000..97ce533 --- /dev/null +++ b/trend/envelope.go @@ -0,0 +1,82 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend + +import ( + "fmt" + + "github.com/cinar/indicator/v2/helper" +) + +const ( + // DefaultEnvelopePercentage is the default envelope percentage of 20%. + DefaultEnvelopePercentage = 20 + + // DefaultEnvelopePeriod is the default envelope period of 20. + DefaultEnvelopePeriod = 20 +) + +// Envelope represents the parameters neededd to calcualte the Envelope. +type Envelope[T helper.Number] struct { + // Ma is the moving average used. + Ma Ma[T] + + // Percentage is the envelope percentage. + Percentage T +} + +// NewEnvelope function initializes a new Envelope instance with the default parameters. +func NewEnvelope[T helper.Number](ma Ma[T], percentage T) *Envelope[T] { + return &Envelope[T]{ + Ma: ma, + Percentage: percentage, + } +} + +// NewEnvelopeWithSma function initalizes a new Envelope instance using SMA. +func NewEnvelopeWithSma[T helper.Number]() *Envelope[T] { + return NewEnvelope( + NewSmaWithPeriod[T](DefaultEnvelopePeriod), + DefaultEnvelopePercentage, + ) +} + +// NewEnvelopeWithEma function initializes a new Envelope instance using EMA. +func NewEnvelopeWithEma[T helper.Number]() *Envelope[T] { + return NewEnvelope( + NewEmaWithPeriod[T](DefaultEnvelopePeriod), + DefaultEnvelopePercentage, + ) +} + +// Compute function takes a channel of numbers and computes the Envelope over the specified period. +func (e *Envelope[T]) Compute(closings <-chan T) (<-chan T, <-chan T, <-chan T) { + middleSplice := helper.Duplicate( + e.Ma.Compute(closings), + 3, + ) + + upper := helper.MultiplyBy( + middleSplice[0], + 1+(e.Percentage/100), + ) + + lower := helper.MultiplyBy( + middleSplice[2], + 1-(e.Percentage/100), + ) + + return upper, middleSplice[1], lower +} + +// IdlePeriod is the initial period that Envelope yield any results. +func (e *Envelope[T]) IdlePeriod() int { + return e.Ma.IdlePeriod() +} + +// String is the string representation of the Envelope. +func (e *Envelope[T]) String() string { + return fmt.Sprintf("Envelope(%s,%v)", e.Ma.String(), e.Percentage) +} diff --git a/trend/envelope_test.go b/trend/envelope_test.go new file mode 100644 index 0000000..974ad1a --- /dev/null +++ b/trend/envelope_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend_test + +import ( + "testing" + + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/trend" +) + +func TestEnvelopeWithSma(t *testing.T) { + type Data struct { + Close float64 + Upper float64 + Middle float64 + Lower float64 + } + + input, err := helper.ReadFromCsvFile[Data]("testdata/envelope_sma.csv", true) + if err != nil { + t.Fatal(err) + } + + inputs := helper.Duplicate(input, 4) + closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) + expectedUpper := helper.Map(inputs[1], func(d *Data) float64 { return d.Upper }) + expectedMiddle := helper.Map(inputs[2], func(d *Data) float64 { return d.Middle }) + expectedLower := helper.Map(inputs[3], func(d *Data) float64 { return d.Lower }) + + envelope := trend.NewEnvelopeWithSma[float64]() + actualUpper, actualMiddle, actualLower := envelope.Compute(closing) + + actualUpper = helper.RoundDigits(actualUpper, 2) + actualMiddle = helper.RoundDigits(actualMiddle, 2) + actualLower = helper.RoundDigits(actualLower, 2) + + expectedUpper = helper.Skip(expectedUpper, envelope.IdlePeriod()) + expectedMiddle = helper.Skip(expectedMiddle, envelope.IdlePeriod()) + expectedLower = helper.Skip(expectedLower, envelope.IdlePeriod()) + + err = helper.CheckEquals(actualUpper, expectedUpper, actualMiddle, expectedMiddle, actualLower, expectedLower) + if err != nil { + t.Fatal(err) + } +} + +func TestEnvelopeWithEma(t *testing.T) { + type Data struct { + Close float64 + Upper float64 + Middle float64 + Lower float64 + } + + input, err := helper.ReadFromCsvFile[Data]("testdata/envelope_ema.csv", true) + if err != nil { + t.Fatal(err) + } + + inputs := helper.Duplicate(input, 4) + closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) + expectedUpper := helper.Map(inputs[1], func(d *Data) float64 { return d.Upper }) + expectedMiddle := helper.Map(inputs[2], func(d *Data) float64 { return d.Middle }) + expectedLower := helper.Map(inputs[3], func(d *Data) float64 { return d.Lower }) + + envelope := trend.NewEnvelopeWithEma[float64]() + actualUpper, actualMiddle, actualLower := envelope.Compute(closing) + + actualUpper = helper.RoundDigits(actualUpper, 2) + actualMiddle = helper.RoundDigits(actualMiddle, 2) + actualLower = helper.RoundDigits(actualLower, 2) + + expectedUpper = helper.Skip(expectedUpper, envelope.IdlePeriod()) + expectedMiddle = helper.Skip(expectedMiddle, envelope.IdlePeriod()) + expectedLower = helper.Skip(expectedLower, envelope.IdlePeriod()) + + err = helper.CheckEquals(actualUpper, expectedUpper, actualMiddle, expectedMiddle, actualLower, expectedLower) + if err != nil { + t.Fatal(err) + } +} + +func TestEnvelopeString(t *testing.T) { + expected := "Envelope(SMA(1),2)" + + envelope := trend.NewEnvelope(trend.NewSmaWithPeriod[float64](1), 2) + actual := envelope.String() + + if actual != expected { + t.Fatalf("actual %v expected %v", actual, expected) + } +} diff --git a/trend/testdata/envelope_ema.csv b/trend/testdata/envelope_ema.csv new file mode 100644 index 0000000..4164c80 --- /dev/null +++ b/trend/testdata/envelope_ema.csv @@ -0,0 +1,252 @@ +Close,Upper,Middle,Lower +318.600006,0,0,0 +315.839996,0,0,0 +316.149994,0,0,0 +310.570007,0,0,0 +307.779999,0,0,0 +305.820007,0,0,0 +305.98999,0,0,0 +306.390015,0,0,0 +311.450012,0,0,0 +312.329987,0,0,0 +309.290009,0,0,0 +301.910004,0,0,0 +300,0,0,0 +300.029999,0,0,0 +302,0,0,0 +307.820007,0,0,0 +302.690002,0,0,0 +306.48999,0,0,0 +305.549988,0,0,0 +303.429993,369.01,307.51,246.01 +309.059998,369.19,307.65,246.12 +308.899994,369.33,307.77,246.22 +309.910004,369.57,307.98,246.38 +314.549988,370.32,308.6,246.88 +312.899994,370.81,309.01,247.21 +318.690002,371.92,309.93,247.95 +315.529999,372.56,310.47,248.37 +316.350006,373.23,311.03,248.82 +320.369995,374.3,311.92,249.53 +318.929993,375.1,312.58,250.07 +317.640015,375.68,313.07,250.45 +314.859985,375.88,313.24,250.59 +308.299988,375.32,312.77,250.21 +305.230011,374.46,312.05,249.64 +309.869995,374.21,311.84,249.47 +310.420013,374.05,311.71,249.36 +311.299988,374,311.67,249.33 +311.899994,374.03,311.69,249.35 +310.950012,373.94,311.62,249.3 +309.170013,373.66,311.39,249.11 +307.329987,373.2,311,248.8 +311.519989,373.26,311.05,248.84 +310.570007,373.2,311,248.8 +311.859985,373.3,311.09,248.87 +308.51001,373.01,310.84,248.67 +308.429993,372.73,310.61,248.49 +312.970001,373,310.84,248.67 +308.480011,372.73,310.61,248.49 +307.209991,372.34,310.29,248.23 +309.890015,372.3,310.25,248.2 +313.73999,372.7,310.58,248.47 +310.790009,372.72,310.6,248.48 +309.630005,372.61,310.51,248.41 +308.179993,372.34,310.29,248.23 +308.23999,372.11,310.09,248.07 +302.720001,371.27,309.39,247.51 +303.160004,370.56,308.8,247.04 +303.070007,369.9,308.25,246.6 +304.019989,369.42,307.85,246.28 +304.660004,369.05,307.54,246.04 +305.179993,368.78,307.32,245.86 +304.619995,368.47,307.06,245.65 +307.75,368.55,307.13,245.7 +312.450012,369.16,307.63,246.11 +316.970001,370.23,308.52,246.82 +311.119995,370.53,308.77,247.02 +311.369995,370.82,309.02,247.21 +304.820007,370.34,308.62,246.89 +303.630005,369.77,308.14,246.51 +302.880005,369.17,307.64,246.11 +305.329987,368.91,307.42,245.94 +297.880005,367.82,306.51,245.21 +302.01001,367.3,306.08,244.87 +293.51001,365.86,304.89,243.91 +301.059998,365.43,304.52,243.62 +303.850006,365.35,304.46,243.57 +299.730011,364.81,304.01,243.21 +298.369995,364.17,303.47,242.78 +298.920013,363.65,303.04,242.43 +302.140015,363.54,302.95,242.36 +302.320007,363.47,302.89,242.31 +305.299988,363.75,303.12,242.5 +305.079987,363.97,303.31,242.65 +308.769989,364.59,303.83,243.06 +310.309998,365.33,304.45,243.56 +309.070007,365.86,304.89,243.91 +310.390015,366.49,305.41,244.33 +312.51001,367.3,306.09,244.87 +312.619995,368.05,306.71,245.37 +313.700012,368.85,307.37,245.9 +314.549988,369.67,308.06,246.45 +318.049988,370.81,309.01,247.21 +319.73999,372.04,310.03,248.03 +323.790009,373.61,311.34,249.07 +324.630005,375.13,312.61,250.09 +323.089996,376.33,313.61,250.88 +323.820007,377.49,314.58,251.66 +324.329987,378.61,315.51,252.41 +326.049988,379.81,316.51,253.21 +324.339996,380.71,317.26,253.81 +320.529999,381.08,317.57,254.05 +326.230011,382.07,318.39,254.71 +328.549988,383.23,319.36,255.49 +330.170013,384.47,320.39,256.31 +325.859985,385.09,320.91,256.73 +323.220001,385.36,321.13,256.9 +320,385.23,321.02,256.82 +323.880005,385.55,321.3,257.04 +326.140015,386.11,321.76,257.41 +324.869995,386.46,322.05,257.64 +322.98999,386.57,322.14,257.71 +322.640015,386.63,322.19,257.75 +322.48999,386.66,322.22,257.77 +323.529999,386.81,322.34,257.87 +323.75,386.97,322.48,257.98 +327.390015,387.53,322.95,258.36 +329.76001,388.31,323.59,258.88 +330.390015,389.09,324.24,259.39 +329.130005,389.65,324.71,259.77 +323.109985,389.47,324.55,259.64 +320.200012,388.97,324.14,259.31 +319.019989,388.38,323.65,258.92 +320.600006,388.03,323.36,258.69 +322.190002,387.9,323.25,258.6 +321.079987,387.65,323.04,258.43 +323.119995,387.66,323.05,258.44 +329.480011,388.4,323.66,258.93 +328.579987,388.96,324.13,259.31 +333.410004,390.02,325.02,260.01 +335.420013,391.21,326.01,260.8 +335.950012,392.34,326.95,261.56 +335.290009,393.3,327.75,262.2 +333.600006,393.97,328.3,262.64 +336.390015,394.89,329.07,263.26 +335.899994,395.67,329.72,263.78 +339.820007,396.82,330.69,264.55 +338.309998,397.69,331.41,265.13 +338.670013,398.52,332.1,265.68 +338.609985,399.27,332.72,266.18 +336.959991,399.75,333.13,266.5 +335.25,399.99,333.33,266.66 +334.119995,400.08,333.4,266.72 +335.339996,400.31,333.59,266.87 +334.149994,400.37,333.64,266.91 +336.910004,400.74,333.95,267.16 +341,401.55,334.62,267.7 +342,402.39,335.33,268.26 +341.559998,403.1,335.92,268.74 +341.459991,403.74,336.45,269.16 +340.899994,404.25,336.87,269.5 +341.130005,404.73,337.28,269.82 +343.369995,405.43,337.86,270.29 +345.350006,406.29,338.57,270.86 +343.540009,406.85,339.04,271.24 +341.089996,407.09,339.24,271.39 +344.25,407.66,339.72,271.77 +345.339996,408.3,340.25,272.2 +342.429993,408.55,340.46,272.37 +346.609985,409.25,341.05,272.84 +345.76001,409.79,341.49,273.2 +349.630005,410.72,342.27,273.82 +347.579987,411.33,342.77,274.22 +349.799988,412.13,343.44,274.76 +349.309998,412.8,344,275.2 +349.809998,413.47,344.56,275.64 +351.959991,414.31,345.26,276.21 +352.26001,415.11,345.93,276.74 +351.190002,415.71,346.43,277.14 +353.809998,416.56,347.13,277.71 +349.98999,416.88,347.4,277.92 +362.579987,418.62,348.85,279.08 +363.730011,420.32,350.27,280.21 +358.019989,421.21,351,280.8 +356.980011,421.89,351.57,281.26 +358.350006,422.66,352.22,281.78 +358.480011,423.38,352.82,282.25 +354.5,423.57,352.98,282.38 +354.109985,423.7,353.08,282.47 +353.190002,423.71,353.09,282.48 +352.559998,423.65,353.04,282.43 +352.089996,423.54,352.95,282.36 +350.570007,423.27,352.73,282.18 +354.26001,423.45,352.87,282.3 +354.299988,423.61,353.01,282.41 +355.929993,423.94,353.29,282.63 +355.549988,424.2,353.5,282.8 +358.290009,424.75,353.96,283.17 +361.059998,425.56,354.63,283.71 +360.200012,426.2,355.16,284.13 +362.459991,427.03,355.86,284.69 +360.470001,427.56,356.3,285.04 +361.670013,428.17,356.81,285.45 +361.799988,428.74,357.29,285.83 +363.149994,429.41,357.84,286.27 +365.519989,430.29,358.57,286.86 +367.779999,431.34,359.45,287.56 +367.820007,432.3,360.25,288.2 +369.5,433.36,361.13,288.9 +367.859985,434.12,361.77,289.42 +370.429993,435.11,362.6,290.08 +370.480011,436.02,363.35,290.68 +366.820007,436.41,363.68,290.94 +363.279999,436.37,363.64,290.91 +360.160004,435.97,363.31,290.65 +361.709991,435.79,363.16,290.52 +359.420013,435.36,362.8,290.24 +357.779999,434.79,362.32,289.86 +357.059998,434.18,361.82,289.46 +350.299988,432.87,360.72,288.58 +348.079987,431.42,359.52,287.62 +343.040009,429.54,357.95,286.36 +343.690002,427.91,356.59,285.27 +345.059998,426.59,355.49,284.39 +346.339996,425.55,354.62,283.7 +345.450012,424.5,353.75,283 +348.559998,423.9,353.25,282.6 +348.429993,423.35,352.79,282.24 +345.660004,422.54,352.12,281.69 +345.089996,421.74,351.45,281.16 +346.230011,421.14,350.95,280.76 +345.390015,420.5,350.42,280.34 +340.890015,419.41,349.51,279.61 +338.660004,418.17,348.48,278.78 +335.859985,416.73,347.28,277.82 +336.839996,415.54,346.28,277.03 +338.630005,414.66,345.55,276.44 +336.899994,413.68,344.73,275.78 +336.160004,412.7,343.91,275.13 +331.709991,411.3,342.75,274.2 +337.410004,410.69,342.24,273.79 +341.329987,410.59,342.16,273.72 +343.75,410.77,342.31,273.85 +349.019989,411.54,342.95,274.36 +351.809998,412.55,343.79,275.03 +346.630005,412.87,344.06,275.25 +346.170013,413.11,344.26,275.41 +346.299988,413.35,344.46,275.57 +348.179993,413.77,344.81,275.85 +350.559998,414.43,345.36,276.29 +350.01001,414.96,345.8,276.64 +354.25,415.93,346.61,277.28 +356.790009,417.09,347.58,278.06 +359.859985,418.5,348.75,279 +358.929993,419.66,349.72,279.77 +361.329987,420.99,350.82,280.66 +361,422.15,351.79,281.43 +361.799988,423.29,352.74,282.2 +362.679993,424.43,353.69,282.95 +361.339996,425.3,354.42,283.54 +360.049988,425.95,354.96,283.96 +358.690002,426.37,355.31,284.25 diff --git a/trend/testdata/envelope_sma.csv b/trend/testdata/envelope_sma.csv new file mode 100644 index 0000000..1fb6f01 --- /dev/null +++ b/trend/testdata/envelope_sma.csv @@ -0,0 +1,252 @@ +Close,Upper,Middle,Lower +318.600006,0,0,0 +315.839996,0,0,0 +316.149994,0,0,0 +310.570007,0,0,0 +307.779999,0,0,0 +305.820007,0,0,0 +305.98999,0,0,0 +306.390015,0,0,0 +311.450012,0,0,0 +312.329987,0,0,0 +309.290009,0,0,0 +301.910004,0,0,0 +300,0,0,0 +300.029999,0,0,0 +302,0,0,0 +307.820007,0,0,0 +302.690002,0,0,0 +306.48999,0,0,0 +305.549988,0,0,0 +303.429993,369.01,307.51,246.01 +309.059998,368.44,307.03,245.62 +308.899994,368.02,306.68,245.35 +309.910004,367.64,306.37,245.1 +314.549988,367.88,306.57,245.26 +312.899994,368.19,306.83,245.46 +318.690002,368.96,307.47,245.98 +315.529999,369.54,307.95,246.36 +316.350006,370.13,308.44,246.76 +320.369995,370.67,308.89,247.11 +318.929993,371.06,309.22,247.38 +317.640015,371.56,309.64,247.71 +314.859985,372.34,310.28,248.23 +308.299988,372.84,310.7,248.56 +305.230011,373.15,310.96,248.77 +309.869995,373.62,311.35,249.08 +310.420013,373.78,311.48,249.19 +311.299988,374.3,311.91,249.53 +311.899994,374.62,312.18,249.75 +310.950012,374.95,312.45,249.96 +309.170013,375.29,312.74,250.19 +307.329987,375.19,312.65,250.12 +311.519989,375.34,312.79,250.23 +310.570007,375.38,312.82,250.26 +311.859985,375.22,312.68,250.15 +308.51001,374.96,312.46,249.97 +308.429993,374.34,311.95,249.56 +312.970001,374.19,311.82,249.46 +308.480011,373.72,311.43,249.14 +307.209991,372.93,310.77,248.62 +309.890015,372.38,310.32,248.26 +313.73999,372.15,310.13,248.1 +310.790009,371.91,309.92,247.94 +309.630005,371.99,309.99,247.99 +308.179993,372.16,310.14,248.11 +308.23999,372.07,310.05,248.04 +302.720001,371.6,309.67,247.74 +303.160004,371.11,309.26,247.41 +303.070007,370.59,308.82,247.06 +304.019989,370.17,308.47,246.78 +304.660004,369.9,308.25,246.6 +305.179993,369.77,308.14,246.51 +304.619995,369.36,307.8,246.24 +307.75,369.19,307.66,246.12 +312.450012,369.22,307.69,246.15 +316.970001,369.73,308.11,246.49 +311.119995,369.89,308.24,246.59 +311.369995,369.79,308.16,246.53 +304.820007,369.58,307.98,246.38 +303.630005,369.36,307.8,246.24 +302.880005,368.94,307.45,245.96 +305.329987,368.44,307.03,245.62 +297.880005,367.66,306.38,245.11 +302.01001,367.2,306,244.8 +293.51001,366.32,305.27,244.22 +301.059998,365.89,304.91,243.93 +303.850006,365.96,304.97,243.97 +299.730011,365.75,304.8,243.84 +298.369995,365.47,304.56,243.65 +298.920013,365.17,304.31,243.44 +302.140015,365.02,304.18,243.34 +302.320007,364.84,304.04,243.23 +305.299988,364.88,304.07,243.26 +305.079987,364.72,303.94,243.15 +308.769989,364.5,303.75,243 +310.309998,364.1,303.42,242.74 +309.070007,363.98,303.32,242.65 +310.390015,363.92,303.27,242.61 +312.51001,364.38,303.65,242.92 +312.619995,364.92,304.1,243.28 +313.700012,365.57,304.64,243.71 +314.549988,366.13,305.1,244.08 +318.049988,367.34,306.11,244.89 +319.73999,368.4,307,245.6 +323.790009,370.22,308.51,246.81 +324.630005,371.63,309.69,247.75 +323.089996,372.78,310.65,248.52 +323.820007,374.23,311.86,249.49 +324.329987,375.79,313.16,250.53 +326.049988,377.42,314.51,251.61 +324.339996,378.75,315.62,252.5 +320.529999,379.84,316.53,253.23 +326.230011,381.1,317.58,254.06 +328.549988,382.5,318.75,255 +330.170013,383.79,319.82,255.86 +325.859985,384.72,320.6,256.48 +323.220001,385.57,321.31,257.05 +320,386.15,321.79,257.43 +323.880005,386.83,322.36,257.89 +326.140015,387.64,323.03,258.43 +324.869995,388.31,323.59,258.87 +322.98999,388.82,324.01,259.21 +322.640015,389.09,324.24,259.39 +322.48999,389.26,324.38,259.5 +323.529999,389.24,324.37,259.49 +323.75,389.19,324.32,259.46 +327.390015,389.45,324.54,259.63 +329.76001,389.8,324.84,259.87 +330.390015,390.17,325.14,260.11 +329.130005,390.35,325.29,260.23 +323.109985,390.28,325.23,260.19 +320.200012,390.26,325.22,260.17 +319.019989,389.83,324.85,259.88 +320.600006,389.35,324.46,259.57 +322.190002,388.87,324.06,259.25 +321.079987,388.58,323.82,259.06 +323.119995,388.58,323.81,259.05 +329.480011,389.15,324.29,259.43 +328.579987,389.43,324.52,259.62 +333.410004,389.86,324.89,259.91 +335.420013,390.5,325.41,260.33 +335.950012,391.27,326.06,260.85 +335.290009,392.03,326.69,261.36 +333.600006,392.7,327.25,261.8 +336.390015,393.47,327.89,262.31 +335.899994,394.2,328.5,262.8 +339.820007,394.95,329.12,263.3 +338.309998,395.46,329.55,263.64 +338.670013,395.96,329.96,263.97 +338.609985,396.53,330.44,264.35 +336.959991,397.36,331.13,264.9 +335.25,398.26,331.88,265.51 +334.119995,399.17,332.64,266.11 +335.339996,400.05,333.37,266.7 +334.149994,400.77,333.97,267.18 +336.910004,401.72,334.76,267.81 +341,402.79,335.66,268.53 +342,403.54,336.28,269.03 +341.559998,404.32,336.93,269.55 +341.459991,404.8,337.34,269.87 +340.899994,405.13,337.61,270.09 +341.130005,405.44,337.87,270.29 +343.369995,405.93,338.27,270.62 +345.350006,406.63,338.86,271.09 +343.540009,407.06,339.22,271.37 +341.089996,407.37,339.48,271.58 +344.25,407.64,339.7,271.76 +345.339996,408.06,340.05,272.04 +342.429993,408.29,340.24,272.19 +346.609985,408.77,340.64,272.51 +345.76001,409.29,341.08,272.86 +349.630005,410.16,341.8,273.44 +347.579987,410.96,342.47,273.98 +349.799988,411.83,343.19,274.55 +349.309998,412.74,343.95,275.16 +349.809998,413.52,344.6,275.68 +351.959991,414.17,345.14,276.12 +352.26001,414.79,345.66,276.53 +351.190002,415.37,346.14,276.91 +353.809998,416.11,346.76,277.4 +349.98999,416.65,347.21,277.77 +362.579987,417.94,348.28,278.63 +363.730011,419.16,349.3,279.44 +358.019989,419.92,349.93,279.95 +356.980011,420.73,350.61,280.49 +358.350006,421.76,351.47,281.18 +358.480011,422.62,352.18,281.74 +354.5,423.17,352.64,282.11 +354.109985,423.87,353.22,282.58 +353.190002,424.26,353.55,282.84 +352.559998,424.67,353.89,283.11 +352.089996,424.82,354.01,283.21 +350.570007,425,354.16,283.33 +354.26001,425.26,354.39,283.51 +354.299988,425.56,354.64,283.71 +355.929993,425.93,354.94,283.95 +355.549988,426.15,355.12,284.1 +358.290009,426.51,355.42,284.34 +361.059998,427.1,355.92,284.73 +360.200012,427.48,356.24,284.99 +362.459991,428.23,356.86,285.49 +360.470001,428.11,356.76,285.4 +361.670013,427.98,356.65,285.32 +361.799988,428.21,356.84,285.47 +363.149994,428.58,357.15,285.72 +365.519989,429.01,357.51,286.01 +367.779999,429.57,357.97,286.38 +367.820007,430.37,358.64,286.91 +369.5,431.29,359.41,287.53 +367.859985,432.17,360.14,288.11 +370.429993,433.24,361.04,288.83 +370.480011,434.35,361.95,289.56 +366.820007,435.32,362.77,290.21 +363.279999,435.86,363.22,290.57 +360.160004,436.21,363.51,290.81 +361.709991,436.56,363.8,291.04 +359.420013,436.79,363.99,291.2 +357.779999,436.76,363.97,291.17 +357.059998,436.52,363.77,291.01 +350.299988,435.93,363.27,290.62 +348.079987,435.07,362.55,290.04 +343.040009,434.02,361.68,289.35 +343.690002,432.94,360.78,288.63 +345.059998,431.94,359.95,287.96 +346.339996,430.93,359.11,287.29 +345.450012,429.72,358.1,286.48 +348.559998,428.57,357.14,285.71 +348.429993,427.41,356.17,284.94 +345.660004,425.98,354.98,283.98 +345.089996,424.61,353.84,283.07 +346.230011,423.16,352.63,282.11 +345.390015,421.65,351.38,281.1 +340.890015,420.1,350.08,280.06 +338.660004,418.62,348.85,279.08 +335.859985,417.16,347.64,278.11 +336.839996,415.67,346.39,277.11 +338.630005,414.42,345.35,276.28 +336.899994,413.17,344.31,275.45 +336.160004,411.92,343.26,274.61 +331.709991,410.8,342.33,273.87 +337.410004,410.16,341.8,273.44 +341.329987,410.06,341.71,273.37 +343.75,410.06,341.72,273.37 +349.019989,410.3,341.92,273.53 +351.809998,410.63,342.19,273.75 +346.630005,410.7,342.25,273.8 +346.170013,410.55,342.13,273.7 +346.299988,410.43,342.02,273.62 +348.179993,410.58,342.15,273.72 +350.559998,410.91,342.42,273.94 +350.01001,411.13,342.61,274.09 +354.25,411.66,343.05,274.44 +356.790009,412.62,343.85,275.08 +359.859985,413.89,344.91,275.93 +358.929993,415.27,346.06,276.85 +361.329987,416.74,347.29,277.83 +361,418.09,348.4,278.72 +361.799988,419.58,349.65,279.72 +362.679993,421.17,350.98,280.78 +361.339996,422.95,352.46,281.97 +360.049988,424.31,353.59,282.87 +358.690002,425.35,354.46,283.57