Skip to content

Commit

Permalink
update Stochastic RSI with more parameters (#68)
Browse files Browse the repository at this point in the history
* update Stochastic Oscillator unit tests and history requirements
* disuse float due to rounding errors in several indicators
* update Stochastic RSI to use more parameters
  • Loading branch information
DaveSkender authored Jul 10, 2020
1 parent 6962bcc commit 903c739
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 128 deletions.
2 changes: 1 addition & 1 deletion GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Historical quotes should be of consistent time frequency (e.g. per minute, hour,
| `Close` | decimal | Close price
| `Volume` | long | Volume

There is also a public read-only `Index` property in this class that is set internally, so **do not try to set the Index value**. We set this `Index` property to `public` visibility in case you want to use it in your own wrapper code. See [Cleaning History](#cleaning-history)) section below if you want to pre-clean the history and get `Index` values in your `IEnumerable<Quote> history` data (optional). You can also derive and extend classes (optional), see the [Using Derived Classes](#using-derived-classes) section below.
There is also a public read-only `Index` property in this class that is set internally, so **do not try to set the Index value**. We set this `Index` property to `public` visibility in case you want to use it in your own wrapper code. See [Cleaning History](#cleaning-history) section below if you want to pre-clean the history and get `Index` values in your `IEnumerable<Quote> history` data (optional). You can also derive and extend classes (optional), see the [Using Derived Classes](#using-derived-classes) section below.

## Cleaning history

Expand Down
12 changes: 6 additions & 6 deletions Indicators/ConnorsRsi/ConnorsRsi.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

public class ConnorsRsiResult : ResultBase
{
public float? RsiClose { get; set; }
public float? RsiStreak { get; set; }
public float? PercentRank { get; set; }
public float? ConnorsRsi { get; set; }
public decimal? RsiClose { get; set; }
public decimal? RsiStreak { get; set; }
public decimal? PercentRank { get; set; }
public decimal? ConnorsRsi { get; set; }

// internal use only
internal float? Streak { get; set; }
internal float? PeriodGain { get; set; }
internal decimal? Streak { get; set; }
internal decimal? PeriodGain { get; set; }
}

}
6 changes: 3 additions & 3 deletions Indicators/ConnorsRsi/ConnorsRsi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static IEnumerable<ConnorsRsiResult> GetConnorsRsi(
int startPeriod = Math.Max(rsiPeriod, Math.Max(streakPeriod, rankPeriod)) + 2;

decimal? lastClose = null;
float streak = 0;
decimal streak = 0;

// compose interim results
foreach (BasicData h in bd)
Expand Down Expand Up @@ -74,14 +74,14 @@ public static IEnumerable<ConnorsRsiResult> GetConnorsRsi(
result.Streak = streak;

// percentile rank
result.PeriodGain = (float)((lastClose <= 0) ? null : (h.Value - lastClose) / lastClose);
result.PeriodGain = (decimal)((lastClose <= 0) ? null : (h.Value - lastClose) / lastClose);

if (h.Index > rankPeriod)
{
IEnumerable<ConnorsRsiResult> period = results
.Where(x => x.Index >= (h.Index - rankPeriod) && x.Index < h.Index);

result.PercentRank = (float)100 * period
result.PercentRank = (decimal)100 * period
.Where(x => x.PeriodGain < result.PeriodGain).Count() / rankPeriod;
}

Expand Down
8 changes: 4 additions & 4 deletions Indicators/ConnorsRsi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ The first `N-1` periods will have `null` values since there's not enough data to
| -- |-- |--
| `Index` | int | Sequence of dates
| `Date` | DateTime | Date
| `RsiClose` | float | RSI(`R`) of the Close price.
| `RsiStreak` | float | RSI(`S`) of the Streak.
| `PercentRank` | float | Percentile rank of the period gain value.
| `ConnorsRsi` | float | ConnorsRSI
| `RsiClose` | decimal | RSI(`R`) of the Close price.
| `RsiStreak` | decimal | RSI(`S`) of the Streak.
| `PercentRank` | decimal | Percentile rank of the period gain value.
| `ConnorsRsi` | decimal | ConnorsRSI

## Example

Expand Down
4 changes: 0 additions & 4 deletions Indicators/Indicators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ Please contribute to help us get to v1.0.0. Feedback appreciated.
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GitVersionTask" Version="5.3.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
2 changes: 1 addition & 1 deletion Indicators/Rsi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The first `N-1` periods will have `null` values since there's not enough data to
| -- |-- |--
| `Index` | int | Sequence of dates
| `Date` | DateTime | Date
| `Rsi` | float | RSI over prior `N` lookback periods
| `Rsi` | decimal | RSI over prior `N` lookback periods
| `IsIncreasing` | bool | Direction since last period (e.g. up or down). Persists for no change.

## Example
Expand Down
6 changes: 3 additions & 3 deletions Indicators/Rsi/Rsi.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

public class RsiResult : ResultBase
{
public float? Rsi { get; set; }
public decimal? Rsi { get; set; }
public bool? IsIncreasing { get; set; }

// internal use only
internal float Gain { get; set; } = 0;
internal float Loss { get; set; } = 0;
internal decimal Gain { get; set; } = 0;
internal decimal Loss { get; set; } = 0;
}

}
21 changes: 12 additions & 9 deletions Indicators/Rsi/Rsi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,34 @@ private static IEnumerable<RsiResult> CalcRsi(IEnumerable<BasicData> basicData,
{
Index = (int)h.Index,
Date = h.Date,
Gain = (lastValue < h.Value) ? (float)(h.Value - lastValue) : 0,
Loss = (lastValue > h.Value) ? (float)(lastValue - h.Value) : 0
Gain = (h.Value > lastValue) ? h.Value - lastValue : 0,
Loss = (h.Value < lastValue) ? lastValue - h.Value : 0
};
results.Add(result);

lastValue = h.Value;
}

// initialize average gain
float avgGain = results.Where(x => x.Index <= lookbackPeriod).Select(g => g.Gain).Average();
float avgLoss = results.Where(x => x.Index <= lookbackPeriod).Select(g => g.Loss).Average();
decimal avgGain = results.Where(x => x.Index <= lookbackPeriod).Select(g => g.Gain).Average();
decimal avgLoss = results.Where(x => x.Index <= lookbackPeriod).Select(g => g.Loss).Average();

// initial RSI for trend analysis
float lastRSI = (avgLoss > 0) ? 100 - (100 / (1 + (avgGain / avgLoss))) : 100;
// initial first record
decimal lastRSI = (avgLoss > 0) ? 100 - (100 / (1 + (avgGain / avgLoss))) : 100;
bool? lastIsIncreasing = null;

RsiResult first = results.Where(x => x.Index == lookbackPeriod + 1).FirstOrDefault();
first.Rsi = lastRSI;

// calculate RSI
foreach (RsiResult r in results.Where(x => x.Index >= lookbackPeriod).OrderBy(d => d.Index))
foreach (RsiResult r in results.Where(x => x.Index > (lookbackPeriod + 1)).OrderBy(d => d.Index))
{
avgGain = (avgGain * (lookbackPeriod - 1) + r.Gain) / lookbackPeriod;
avgLoss = (avgLoss * (lookbackPeriod - 1) + r.Loss) / lookbackPeriod;

if (avgLoss > 0)
{
float rs = avgGain / avgLoss;
decimal rs = avgGain / avgLoss;
r.Rsi = 100 - (100 / (1 + rs));
}
else
Expand All @@ -84,7 +87,7 @@ private static IEnumerable<RsiResult> CalcRsi(IEnumerable<BasicData> basicData,
r.IsIncreasing = lastIsIncreasing;
}

lastRSI = (float)r.Rsi;
lastRSI = (decimal)r.Rsi;
lastIsIncreasing = r.IsIncreasing;
}

Expand Down
10 changes: 5 additions & 5 deletions Indicators/Stochastic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ IEnumerable<StochResult> results = Indicator.GetStoch(history, lookbackPeriod, s

| name | type | notes
| -- |-- |--
| `history` | IEnumerable\<[Quote](/GUIDE.md#Quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc). You must supply at least `N` periods of `history`.
| `history` | IEnumerable\<[Quote](/GUIDE.md#Quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc). You must supply at least `N+S` periods of `history`.
| `lookbackPeriod` | int | Lookback period (`N`) for the oscillator (%K). Must be greater than 0. Default is 14.
| `signalPeriod` | int | Lookback period for the signal (%D). Must be greater than 0. Default is 3.
| `smoothingPeriod` | int | Smoothes the Oscillator (%K). "Slow" stochastic uses 3, "Fast" stochastic uses 1. You can specify as needed here. Must be greater than or equal to 1. Default is 3.
| `smoothingPeriod` | int | Smoothing period `S` for the Oscillator (%K). "Slow" stochastic uses 3, "Fast" stochastic uses 1. You can specify as needed here. Must be greater than or equal to 1. Default is 3.

## Response

```csharp
IEnumerable<StochResult>
```

The first `N-1` periods will have `null` Oscillator values since there's not enough data to calculate. We always return the same number of elements as there are in the historical quotes.
The first `N+S-1` periods will have `null` Oscillator values since there's not enough data to calculate. We always return the same number of elements as there are in the historical quotes.

### StochResult

| name | type | notes
| -- |-- |--
| `Index` | int | Sequence of dates
| `Date` | DateTime | Date
| `Oscillator` | float | %K Oscillator over prior `N` lookback periods
| `Signal` | float | %D Simple moving average of Oscillator
| `Oscillator` | decimal | %K Oscillator over prior `N` lookback periods
| `Signal` | decimal | %D Simple moving average of Oscillator
| `IsIncreasing` | bool | Direction since last period (e.g. up or down). Persists for no change.

## Example
Expand Down
6 changes: 3 additions & 3 deletions Indicators/Stochastic/Stoch.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

public class StochResult : ResultBase
{
public float? Oscillator { get; set; }
public float? Signal { get; set; }
public decimal? Oscillator { get; set; }
public decimal? Signal { get; set; }
public bool? IsIncreasing { get; set; }

// internal use only
internal float? Smooth { get; set; }
internal decimal? Smooth { get; set; }
}

}
30 changes: 18 additions & 12 deletions Indicators/Stochastic/Stoch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static IEnumerable<StochResult> GetStoch(IEnumerable<Quote> history, int

if (lowLow != highHigh)
{
result.Oscillator = 100 * (float)((h.Close - lowLow) / (highHigh - lowLow));
result.Oscillator = 100 * ((h.Close - lowLow) / (highHigh - lowLow));
}
else
{
Expand All @@ -58,17 +58,23 @@ public static IEnumerable<StochResult> GetStoch(IEnumerable<Quote> history, int
}


// new signal and trend info
float lastOsc = 0;
// signal and period direction info
decimal? lastOsc = null;
bool? lastIsIncreasing = null;
foreach (StochResult r in results
.Where(x => x.Index >= (lookbackPeriod + signalPeriod + smoothPeriod) && x.Oscillator != null))
.Where(x => x.Index >= (lookbackPeriod + smoothPeriod - 1))
.OrderBy(x => x.Index))
{
r.Signal = results.Where(x => x.Index > (r.Index - signalPeriod) && x.Index <= r.Index)
.Select(v => v.Oscillator)
.Average();
// add signal
if (r.Index >= lookbackPeriod + smoothPeriod + signalPeriod - 2)
{
r.Signal = results.Where(x => x.Index > (r.Index - signalPeriod) && x.Index <= r.Index)
.Select(v => v.Oscillator)
.Average();
}

if (r.Index >= (lookbackPeriod + signalPeriod + smoothPeriod) + 1)
// add direction
if (lastOsc != null)
{
if (r.Oscillator > lastOsc)
{
Expand All @@ -85,7 +91,7 @@ public static IEnumerable<StochResult> GetStoch(IEnumerable<Quote> history, int
}
}

lastOsc = (float)r.Oscillator;
lastOsc = (decimal)r.Oscillator;
lastIsIncreasing = r.IsIncreasing;
}

Expand All @@ -97,7 +103,7 @@ private static List<StochResult> SmoothOscillator(List<StochResult> results, int
{

// temporarily store interim smoothed oscillator
foreach (StochResult r in results.Where(x => x.Index >= (lookbackPeriod + smoothPeriod)))
foreach (StochResult r in results.Where(x => x.Index >= (lookbackPeriod + smoothPeriod - 1)))
{
r.Smooth = results.Where(x => x.Index > (r.Index - smoothPeriod) && x.Index <= r.Index)
.Select(v => v.Oscillator)
Expand All @@ -109,7 +115,7 @@ private static List<StochResult> SmoothOscillator(List<StochResult> results, int
{
if (r.Smooth != null)
{
r.Oscillator = (float)r.Smooth;
r.Oscillator = (decimal)r.Smooth;
}
else
{
Expand Down Expand Up @@ -142,7 +148,7 @@ private static void ValidateStoch(IEnumerable<Quote> history, int lookbackPeriod

// check history
int qtyHistory = history.Count();
int minHistory = lookbackPeriod;
int minHistory = lookbackPeriod + smoothPeriod;
if (qtyHistory < minHistory)
{
throw new BadHistoryException("Insufficient history provided for Stochastic. " +
Expand Down
18 changes: 12 additions & 6 deletions Indicators/StochasticRsi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,37 @@ It is different from, and often confused with the more traditional [Stochastic O

```csharp
// usage
IEnumerable<StochRsiResult> results = Indicator.GetStochRsi(history, lookbackPeriod);
IEnumerable<StochRsiResult> results = Indicator.GetStochRsi(history, rsiPeriod, stochPeriod, signalPeriod, smoothPeriod);
```

## Parameters

| name | type | notes
| -- |-- |--
| `history` | IEnumerable\<[Quote](/GUIDE.md#Quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc). You must supply at least 2×`N` periods of `history`. Since this uses a smoothing technique in the underlying RSI value, we recommend you use at least 250 data points prior to the intended usage date for maximum precision.
| `lookbackPeriod` | int | Number of periods (`N`) in the lookback period. Must be greater than 0. Default is 14.
| `history` | IEnumerable\<[Quote](/GUIDE.md#Quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc). You must supply at least `R+S` periods of `history`. Since this uses a smoothing technique in the underlying RSI value, we recommend you use at least 250 data points prior to the intended usage date for maximum precision.
| `rsiPeriod` | int | Number of periods (`R`) in the lookback period. Must be greater than 0. Standard is 14.
| `stochPeriod` | int | Number of periods (`S`) in the lookback period. Must be greater than 0. Typically the same value as `rsiPeriod`.
| `signalPeriod` | int | Number of periods (`G`) in the signal line (SMA of the StochRSI). Must be greater than 0. Typically 3-5.
| `smoothPeriod` | int | Smoothing periods (`M`) for the Stochastic. Must be greater than 0. Default is 1 (Fast variant).

The original Stochasic RSI formula uses a the Fast variant of the Stochastic calculation (`smoothPeriod=1`). For a standard period of 14, the original formula would be `GetStochRSI(history,14,14,3,1)`; though, the "3" here is just for the Signal, which is not present in the original formula, but useful for additional smoothing of the Stochastic RSI.

## Response

```csharp
IEnumerable<StochRsiResult>
```

The first `2×N-1` periods will have `null` values since there's not enough data to calculate. We always return the same number of elements as there are in the historical quotes.
The first `R+S-1` periods will have `null` values for `StochRsi` since there's not enough data to calculate. We always return the same number of elements as there are in the historical quotes.

### StochRsiResult

| name | type | notes
| -- |-- |--
| `Index` | int | Sequence of dates
| `Date` | DateTime | Date
| `StochRsi` | float | StochRSI over prior `N` lookback periods
| `StochRsi` | decimal | %K Oscillator = Stochastic RSI = Stoch(`S`,`G`,`M`) of RSI(`R`) of Close price
| `Signal` | decimal | %D Signal Line = Simple moving average of %K based on `G` periods
| `IsIncreasing` | bool | Direction since last period (e.g. up or down). Persists for no change.

## Example
Expand All @@ -40,7 +46,7 @@ The first `2×N-1` periods will have `null` values since there's not enough data
IEnumerable<Quote> history = GetHistoryFromFeed("SPY");

// calculate StochRSI(14)
IEnumerable<StochRsiResult> results = Indicator.GetStochRsi(history,14);
IEnumerable<StochRsiResult> results = Indicator.GetStochRsi(history,14,14,1,1);

// use results as needed
DateTime evalDate = DateTime.Parse("12/31/2018");
Expand Down
3 changes: 2 additions & 1 deletion Indicators/StochasticRsi/StochRsi.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

public class StochRsiResult : ResultBase
{
public float? StochRsi { get; set; }
public decimal? StochRsi { get; set; }
public decimal? Signal { get; set; }
public bool? IsIncreasing { get; set; }
}

Expand Down
Loading

0 comments on commit 903c739

Please sign in to comment.