Skip to content

Commit

Permalink
add Schaff Trend Cycle (#585)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender authored Oct 10, 2021
1 parent bed49a8 commit 716e60b
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 1 deletion.
70 changes: 70 additions & 0 deletions docs/_indicators/Stc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Schaff Trend Cycle
permalink: /indicators/Stc/
layout: default
---

# {{ page.title }}

Created by Doug Schaff, [Schaff Trend Cycle](https://www.investopedia.com/articles/forex/10/schaff-trend-cycle-indicator.asp) is a stochastic oscillator view of two converging/diverging exponential moving averages (a.k.a MACD).
[[Discuss] :speech_balloon:]({{site.github.repository_url}}/discussions/570 "Community discussion about this indicator")

![image]({{site.baseurl}}/assets/charts/Stc.png)

```csharp
// usage
IEnumerable<StcResult> results =
quotes.GetStc(cyclePeriods, fastPeriods, slowPeriods);
```

## Parameters

| name | type | notes
| -- |-- |--
| `cyclePeriods` | int | Number of periods (`C`) for the Trend Cycle. Must be greater than or equal to 0. Default is 10.
| `fastPeriods` | int | Number of periods (`F`) for the faster moving average. Must be greater than 0. Default is 23.
| `slowPeriods` | int | Number of periods (`S`) for the slower moving average. Must be greater than `fastPeriods`. Default is 50.

### Historical quotes requirements

You must have at least `2×(S+C)` or `S+C+100` worth of `quotes`, whichever is more. Since this uses a smoothing technique, we recommend you use at least `S+C+250` data points prior to the intended usage date for better precision.

`quotes` is an `IEnumerable<TQuote>` collection of historical price quotes. It should have a consistent frequency (day, hour, minute, etc). See [the Guide]({{site.baseurl}}/guide/#historical-quotes) for more information.

## Response

```csharp
IEnumerable<StcResult>
```

- This method returns a time series of all available indicator values for the `quotes` provided.
- It always returns the same number of elements as there are in the historical quotes.
- It does not return a single incremental indicator value.
- The first `S+C` slow periods will have `null` values since there's not enough data to calculate.

:hourglass: **Convergence Warning**: The first `S+C+250` periods will have decreasing magnitude, convergence-related precision errors that can be as high as ~5% deviation in indicator values for earlier periods.

### StcResult

| name | type | notes
| -- |-- |--
| `Date` | DateTime | Date
| `Stc` | decimal | Schaff Trend Cycle

### Utilities

- [.Find(lookupDate)]({{site.baseurl}}/utilities#find-indicator-result-by-date)
- [.RemoveWarmupPeriods()]({{site.baseurl}}/utilities#remove-warmup-periods)
- [.RemoveWarmupPeriods(qty)]({{site.baseurl}}/utilities#remove-warmup-periods)

See [Utilities and Helpers]({{site.baseurl}}/utilities#utilities-for-indicator-results) for more information.

## Example

```csharp
// fetch historical quotes from your feed (your method)
IEnumerable<Quote> quotes = GetHistoryFromFeed("SPY");

// calculate STC(12,26,9)
IEnumerable<StcResult> results = quotes.GetStc(10,23,50);
```
Binary file added docs/assets/charts/Stc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/indicators.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ redirect_from:
- [Detrended Price Oscillator (DPO)](../indicators/Dpo/#content)
- [Relative Strength Index (RSI)](../indicators/Rsi/#content)
- [ROC with Bands](../indicators/Roc/#roc-with-bands)
- [Schaff Trend Cycle](../indicators/Stc/#content)
- [Stochastic Oscillator](../indicators/Stoch/#content) and [KDJ Index](../indicators/Stoch/#content)
- [Stochastic RSI](../indicators/StochRsi/#content)
- [Triple EMA Oscillator (TRIX)](../indicators/Trix/#content)
Expand Down
10 changes: 10 additions & 0 deletions src/s-z/Stc/Stc.Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace Skender.Stock.Indicators
{
[Serializable]
public class StcResult : ResultBase
{
public decimal? Stc { get; set; }
}
}
121 changes: 121 additions & 0 deletions src/s-z/Stc/Stc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Skender.Stock.Indicators
{
public static partial class Indicator
{
// SCHAFF TREND CYCLE (STC)
/// <include file='./info.xml' path='indicator/*' />
///
public static IEnumerable<StcResult> GetStc<TQuote>(
this IEnumerable<TQuote> quotes,
int cyclePeriods = 10,
int fastPeriods = 23,
int slowPeriods = 50)
where TQuote : IQuote
{

// sort quotes
List<TQuote> quotesList = quotes.Sort();

// check parameter arguments
ValidateStc(quotes, cyclePeriods, fastPeriods, slowPeriods);

// get stochastic of macd
IEnumerable<StochResult> stochMacd = quotes
.GetMacd(fastPeriods, slowPeriods, 1)
.Where(x => x.Macd != null)
.Select(x => new Quote
{
Date = x.Date,
High = (decimal)x.Macd,
Low = (decimal)x.Macd,
Close = (decimal)x.Macd
})
.GetStoch(cyclePeriods, 1, 3);

// initialize results
// to ensure same length as original quotes
List<StcResult> results = new(quotesList.Count);

for (int i = 0; i < slowPeriods - 1; i++)
{
TQuote q = quotesList[i];
results.Add(new StcResult() { Date = q.Date });
}

// add stoch results
// TODO: see if List Add works faster
results.AddRange(
stochMacd
.Select(x => new StcResult
{
Date = x.Date,
Stc = x.Oscillator
}));

return results;
}


// remove recommended periods
/// <include file='../../_common/Results/info.xml' path='info/type[@name="Prune"]/*' />
///
public static IEnumerable<StcResult> RemoveWarmupPeriods(
this IEnumerable<StcResult> results)
{
int n = results
.ToList()
.FindIndex(x => x.Stc != null);

return results.Remove(n + 250);
}


// parameter validation
private static void ValidateStc<TQuote>(
IEnumerable<TQuote> quotes,
int cyclePeriods,
int fastPeriods,
int slowPeriods)
where TQuote : IQuote
{

// check parameter arguments
if (cyclePeriods < 0)
{
throw new ArgumentOutOfRangeException(nameof(cyclePeriods), cyclePeriods,
"Trend Cycle periods must be greater than or equal to 0 for STC.");
}

if (fastPeriods <= 0)
{
throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods,
"Fast periods must be greater than 0 for STC.");
}

if (slowPeriods <= fastPeriods)
{
throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods,
"Slow periods must be greater than the fast period for STC.");
}

// check quotes
int qtyHistory = quotes.Count();
int minHistory = Math.Max(2 * (slowPeriods + cyclePeriods), slowPeriods + cyclePeriods + 100);
if (qtyHistory < minHistory)
{
string message = "Insufficient quotes provided for STC. " +
string.Format(EnglishCulture,
"You provided {0} periods of quotes when at least {1} are required. "
+ "Since this uses a smoothing technique, "
+ "we recommend you use at least {2} data points prior to the intended "
+ "usage date for better precision.", qtyHistory, minHistory, slowPeriods + 250);

throw new BadQuotesException(nameof(quotes), message);
}
}
}
}
20 changes: 20 additions & 0 deletions src/s-z/Stc/info.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>

<indicator>
<summary>
Schaff Trend Cycle is a stochastic oscillator view of two converging/diverging exponential moving averages.
<para>
See
<see href="https://daveskender.github.io/Stock.Indicators/indicators/Stc/#content">documentation</see>
for more information.
</para>
</summary>
<typeparam name="TQuote">Configurable Quote type. See Guide for more information.</typeparam>
<param name="quotes">Historical price quotes.</param>
<param name="cyclePeriods">Number of periods for the Trend Cycle.</param>
<param name="fastPeriods">Number of periods in the Fast EMA.</param>
<param name="slowPeriods">Number of periods in the Slow EMA.</param>
<returns>Time series of MACD values, including MACD, Signal, and Histogram.</returns>
<exception cref="ArgumentOutOfRangeException">Invalid parameter value provided.</exception>
<exception cref="BadQuotesException">Insufficient quotes provided.</exception>
</indicator>
2 changes: 1 addition & 1 deletion src/s-z/Stoch/Stoch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static IEnumerable<StochResult> GetStoch<TQuote>(

if (index >= lookbackPeriods)
{
decimal highHigh = 0;
decimal highHigh = decimal.MinValue;
decimal lowLow = decimal.MaxValue;

for (int p = index - lookbackPeriods; p < index; p++)
Expand Down
Binary file added tests/indicators/s-z/Stc/Stc.Calc.xlsx
Binary file not shown.
103 changes: 103 additions & 0 deletions tests/indicators/s-z/Stc/Stc.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Skender.Stock.Indicators;

namespace Internal.Tests
{
[TestClass]
public class Stc : TestBase
{

[TestMethod]
public void Standard()
{
int cyclePeriods = 9;
int fastPeriods = 12;
int slowPeriods = 26;

List<StcResult> results =
quotes.GetStc(cyclePeriods, fastPeriods, slowPeriods)
.ToList();

foreach (StcResult r in results)
{
Console.WriteLine($"{r.Date:d},{r.Stc:N4}");
}

// assertions

// proper quantities
// should always be the same number of results as there is quotes
Assert.AreEqual(502, results.Count);
Assert.AreEqual(467, results.Where(x => x.Stc != null).Count());

// sample values
StcResult r34 = results[34];
Assert.IsNull(r34.Stc);

StcResult r35 = results[35];
Assert.AreEqual(100m, r35.Stc);

StcResult r49 = results[49];
Assert.AreEqual(0.8370m, Math.Round((decimal)r49.Stc, 4));

StcResult r249 = results[249];
Assert.AreEqual(27.7340m, Math.Round((decimal)r249.Stc, 4));

StcResult last = results.LastOrDefault();
Assert.AreEqual(19.2544m, Math.Round((decimal)last.Stc, 4));
}

[TestMethod]
public void BadData()
{
IEnumerable<StcResult> r = badQuotes.GetStc(10, 23, 50);
Assert.AreEqual(502, r.Count());
}

[TestMethod]
public void Removed()
{
int cyclePeriods = 9;
int fastPeriods = 12;
int slowPeriods = 26;

List<StcResult> results =
quotes.GetStc(cyclePeriods, fastPeriods, slowPeriods)
.RemoveWarmupPeriods()
.ToList();

// assertions
Assert.AreEqual(502 - (slowPeriods + cyclePeriods + 250), results.Count);

StcResult last = results.LastOrDefault();
Assert.AreEqual(19.2544m, Math.Round((decimal)last.Stc, 4));
}

[TestMethod]
public void Exceptions()
{
// bad fast period
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
Indicator.GetStc(quotes, 9, 0, 26));

// bad slow periods must be larger than faster period
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
Indicator.GetStc(quotes, 9, 12, 12));

// bad signal period
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
Indicator.GetStc(quotes, -1, 12, 26));

// insufficient quotes 2×(S+P)
Assert.ThrowsException<BadQuotesException>(() =>
Indicator.GetStc(TestData.GetDefault(409), 5, 12, 200));

// insufficient quotes S+P+100
Assert.ThrowsException<BadQuotesException>(() =>
Indicator.GetStc(TestData.GetDefault(134), 9, 12, 26));
}
}
}
Binary file modified tests/indicators/s-z/Stoch/Stoch.Calc.xlsx
Binary file not shown.
6 changes: 6 additions & 0 deletions tests/performance/Perf.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ public object GetStarcBands()
return h.GetStarcBands();
}

[Benchmark]
public object GetStc()
{
return h.GetStc();
}

[Benchmark]
public object GetStdDev()
{
Expand Down

0 comments on commit 716e60b

Please sign in to comment.