diff --git a/README.md b/README.md index 3f6dcc9..df01617 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ The following list of strategies are currently supported by this package: - [Triangular Moving Average (TRIMA) Strategy](strategy/trend/README.md#type-trimastrategy) - [Triple Exponential Average (TRIX) Strategy](strategy/trend/README.md#type-trixstrategy) - [Triple Moving Average Crossover Strategy](strategy/trend/README.md#type-triplemovingaveragecrossoverstrategy) +- [True Strength Index (TSI) Strategy](strategy/trend/README.md#type-tsistrategy) - [Volume Weighted Moving Average (VWMA) Strategy](strategy/trend/README.md#type-vwmastrategy) ### 🚀 Momentum Strategies diff --git a/strategy/trend/README.md b/strategy/trend/README.md index cde0f85..fceb6d8 100644 --- a/strategy/trend/README.md +++ b/strategy/trend/README.md @@ -95,6 +95,13 @@ The information provided on this project is strictly for informational purposes - [func \(t \*TrixStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#TrixStrategy.Compute>) - [func \(\*TrixStrategy\) Name\(\) string](<#TrixStrategy.Name>) - [func \(t \*TrixStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#TrixStrategy.Report>) +- [type TsiStrategy](<#TsiStrategy>) + - [func NewTsiStrategy\(\) \*TsiStrategy](<#NewTsiStrategy>) + - [func NewTsiStrategyWith\(firstSmoothingPeriod, secondSmoothingPeriod, signalPeriod int\) \*TsiStrategy](<#NewTsiStrategyWith>) + - [func \(t \*TsiStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#TsiStrategy.Compute>) + - [func \(t \*TsiStrategy\) IdlePeriod\(\) int](<#TsiStrategy.IdlePeriod>) + - [func \(t \*TsiStrategy\) Name\(\) string](<#TsiStrategy.Name>) + - [func \(t \*TsiStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#TsiStrategy.Report>) - [type VwmaStrategy](<#VwmaStrategy>) - [func NewVwmaStrategy\(\) \*VwmaStrategy](<#NewVwmaStrategy>) - [func \(v \*VwmaStrategy\) Compute\(c \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#VwmaStrategy.Compute>) @@ -155,6 +162,15 @@ const ( ) ``` + + +```go +const ( + // DefaultTsiStrategySignalPeriod is the default signal line period of 12. + DefaultTsiStrategySignalPeriod = 12 +) +``` + ```go @@ -875,6 +891,81 @@ func (t *TrixStrategy) Report(c <-chan *asset.Snapshot) *helper.Report Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + +## type [TsiStrategy]() + +TsiStrategy represents the configuration parameters for calculating the TSI strategy. When the TSI is above zero and crossing above the signal line suggests a bullish trend, while TSI being below zero and crossing below the signal line indicates a bearish trend. + +``` +Signal Line = Ema(12, TSI) +When TSI > 0, TSI > Signal Line, Buy. +When TSI < 0, TSI < Signal Line, Sell.const +``` + +```go +type TsiStrategy struct { + // Tsi represents the configuration parameters for calculating the True Strength Index (TSI). + Tsi *trend.Tsi[float64] + + // Signal line is the moving average of the TSI. + Signal trend.Ma[float64] +} +``` + + +### func [NewTsiStrategy]() + +```go +func NewTsiStrategy() *TsiStrategy +``` + +NewTsiStrategy function initializes a new TSI strategy instance. + + +### func [NewTsiStrategyWith]() + +```go +func NewTsiStrategyWith(firstSmoothingPeriod, secondSmoothingPeriod, signalPeriod int) *TsiStrategy +``` + +NewTsiStrategyWith function initializes a new TSI strategy instance with the given parameters. + + +### func \(\*TsiStrategy\) [Compute]() + +```go +func (t *TsiStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*TsiStrategy\) [IdlePeriod]() + +```go +func (t *TsiStrategy) IdlePeriod() int +``` + +IdlePeriod is the initial period that TSI strategy yield any results. + + +### func \(\*TsiStrategy\) [Name]() + +```go +func (t *TsiStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*TsiStrategy\) [Report]() + +```go +func (t *TsiStrategy) Report(c <-chan *asset.Snapshot) *helper.Report +``` + +Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + ## type [VwmaStrategy]() diff --git a/strategy/trend/testdata/tsi_strategy.csv b/strategy/trend/testdata/tsi_strategy.csv new file mode 100644 index 0000000..28e7727 --- /dev/null +++ b/strategy/trend/testdata/tsi_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 +1 +0 +0 +0 +-1 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +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 +-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 +1 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/strategy/trend/trend.go b/strategy/trend/trend.go index a90b35e..cb977db 100644 --- a/strategy/trend/trend.go +++ b/strategy/trend/trend.go @@ -35,6 +35,7 @@ func AllStrategies() []strategy.Strategy { NewQstickStrategy(), NewTrimaStrategy(), NewTripleMovingAverageCrossoverStrategy(), + NewTsiStrategy(), NewVwmaStrategy(), } } diff --git a/strategy/trend/tsi_strategy.go b/strategy/trend/tsi_strategy.go new file mode 100644 index 0000000..202d08d --- /dev/null +++ b/strategy/trend/tsi_strategy.go @@ -0,0 +1,146 @@ +// 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" +) + +const ( + // DefaultTsiStrategySignalPeriod is the default signal line period of 12. + DefaultTsiStrategySignalPeriod = 12 +) + +// TsiStrategy represents the configuration parameters for calculating the TSI strategy. When the TSI is above zero and +// crossing above the signal line suggests a bullish trend, while TSI being below zero and crossing below the signal +// line indicates a bearish trend. +// +// Signal Line = Ema(12, TSI) +// When TSI > 0, TSI > Signal Line, Buy. +// When TSI < 0, TSI < Signal Line, Sell.const +type TsiStrategy struct { + // Tsi represents the configuration parameters for calculating the True Strength Index (TSI). + Tsi *trend.Tsi[float64] + + // Signal line is the moving average of the TSI. + Signal trend.Ma[float64] +} + +// NewTsiStrategy function initializes a new TSI strategy instance. +func NewTsiStrategy() *TsiStrategy { + return NewTsiStrategyWith( + trend.DefaultTsiFirstSmoothingPeriod, + trend.DefaultTsiSecondSmoothingPeriod, + DefaultTsiStrategySignalPeriod, + ) +} + +// NewTsiStrategyWith function initializes a new TSI strategy instance with the given parameters. +func NewTsiStrategyWith(firstSmoothingPeriod, secondSmoothingPeriod, signalPeriod int) *TsiStrategy { + return &TsiStrategy{ + Tsi: trend.NewTsiWith[float64]( + firstSmoothingPeriod, + secondSmoothingPeriod, + ), + + Signal: trend.NewEmaWithPeriod[float64](signalPeriod), + } +} + +// Name returns the name of the strategy. +func (t *TsiStrategy) Name() string { + return fmt.Sprintf("Tsi Strategy (%s,%s)", + t.Tsi.String(), + t.Signal.String(), + ) +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (t *TsiStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + closings := asset.SnapshotsAsClosings(snapshots) + + tsisSplice := helper.Duplicate(t.Tsi.Compute(closings), 2) + + tsisSplice[0] = helper.Skip(tsisSplice[0], t.Signal.IdlePeriod()) + signals := t.Signal.Compute(tsisSplice[1]) + + actions := helper.Operate(tsisSplice[0], signals, func(tsi, signal float64) strategy.Action { + // When the TSI is above zero and crossing above the signal line suggests a bullish trend. + if (tsi > 0) && (tsi > signal) { + return strategy.Buy + } + + // While TSI being below zero and crossing below the signal line indicates a bearish trend. + if (tsi < 0) && (tsi < signal) { + return strategy.Sell + } + + return strategy.Hold + }) + + // TSI and signal line start only after a full period. + actions = helper.Shift(actions, t.IdlePeriod(), strategy.Hold) + actions = strategy.NormalizeActions(actions) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (t *TsiStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> closings[0] -> closings + // closings[1] -> tsi[0] -> tsi + // -> tsi[1] -> signal + // snapshots[2] -> actions -> annotations + // -> outcomes + // + snapshotsSplice := helper.Duplicate(c, 3) + + dates := helper.Skip( + asset.SnapshotsAsDates(snapshotsSplice[0]), + t.IdlePeriod(), + ) + + closingsSplice := helper.Duplicate(asset.SnapshotsAsClosings(snapshotsSplice[1]), 2) + closingsSplice[1] = helper.Skip(closingsSplice[1], t.IdlePeriod()) + + tsisSplice := helper.Duplicate(t.Tsi.Compute(closingsSplice[0]), 2) + tsisSplice[0] = helper.Skip(tsisSplice[0], t.Signal.IdlePeriod()) + + signals := t.Signal.Compute(tsisSplice[1]) + + actions, outcomes := strategy.ComputeWithOutcome(t, snapshotsSplice[2]) + actions = helper.Skip(actions, t.IdlePeriod()) + outcomes = helper.Skip(outcomes, t.IdlePeriod()) + + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(t.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[1])) + + report.AddColumn(helper.NewNumericReportColumn("TSI", tsisSplice[0]), 1) + report.AddColumn(helper.NewNumericReportColumn("Signal", signals), 1) + + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} + +// IdlePeriod is the initial period that TSI strategy yield any results. +func (t *TsiStrategy) IdlePeriod() int { + return t.Tsi.IdlePeriod() + t.Signal.IdlePeriod() +} diff --git a/strategy/trend/tsi_strategy_test.go b/strategy/trend/tsi_strategy_test.go new file mode 100644 index 0000000..be1837a --- /dev/null +++ b/strategy/trend/tsi_strategy_test.go @@ -0,0 +1,56 @@ +// 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 TestTsiStrategy(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/tsi_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + tsi := trend.NewTsiStrategy() + actual := tsi.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestTsiStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + tsi := trend.NewTsiStrategy() + + report := tsi.Report(snapshots) + + fileName := "tsi_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +}