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)
+ }
+}