Skip to content

Commit

Permalink
add ConnorsRSI indicator (#66)
Browse files Browse the repository at this point in the history
* add Connor RSI indicator
  • Loading branch information
DaveSkender authored Jul 10, 2020
1 parent 9dffc40 commit 80a41bf
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 12 deletions.
16 changes: 16 additions & 0 deletions Indicators/ConnorsRsi/ConnorsRsi.Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Skender.Stock.Indicators
{

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

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

}
153 changes: 153 additions & 0 deletions Indicators/ConnorsRsi/ConnorsRsi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Skender.Stock.Indicators
{
public static partial class Indicator
{
// CONNORS RSI
public static IEnumerable<ConnorsRsiResult> GetConnorsRsi(
IEnumerable<Quote> history, int rsiPeriod = 3, int streakPeriod = 2, int rankPeriod = 100)
{

// convert history to basic format
IEnumerable<BasicData> bd = Cleaners.ConvertHistoryToBasic(history, "C");

// check parameters
ValidateConnorsRsi(bd, rsiPeriod, streakPeriod, rankPeriod);

// initialize
List<ConnorsRsiResult> results = new List<ConnorsRsiResult>();
IEnumerable<RsiResult> rsiResults = CalcRsi(bd, rsiPeriod);
int startPeriod = Math.Max(rsiPeriod, Math.Max(streakPeriod, rankPeriod)) + 2;

decimal? lastClose = null;
float streak = 0;

// compose interim results
foreach (BasicData h in bd)
{
ConnorsRsiResult result = new ConnorsRsiResult
{
Index = (int)h.Index,
Date = h.Date,
RsiClose = rsiResults.Where(x => x.Index == h.Index).FirstOrDefault().Rsi
};

// bypass for first record
if (lastClose == null)
{
lastClose = h.Value;
results.Add(result);
continue;
}

// streak of up or down
if (h.Value == lastClose)
{
streak = 0;
}
else if (h.Value > lastClose)
{
if (streak >= 0)
{
streak++;
}
else
{
streak = 1;
}
}
else // h.Value < lastClose
{
if (streak <= 0)
{
streak--;
}
else
{
streak = -1;
}
}

result.Streak = streak;

// percentile rank
result.PeriodGain = (float)((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
.Where(x => x.PeriodGain < result.PeriodGain).Count() / rankPeriod;
}

results.Add(result);
lastClose = h.Value;
}

// RSI of streak
List<BasicData> bdStreak = results
.Where(x => x.Streak != null)
.Select(x => new BasicData { Index = null, Date = x.Date, Value = (decimal)x.Streak })
.ToList();

IEnumerable<RsiResult> rsiStreakResults = CalcRsi(bdStreak, streakPeriod);

// compose final results
foreach (ConnorsRsiResult r in results.Where(x => x.Index >= streakPeriod + 2))
{
r.RsiStreak = rsiStreakResults
.Where(x => x.Index == r.Index - 1)
.FirstOrDefault()
.Rsi;

if (r.Index >= startPeriod)
{
r.ConnorsRsi = (r.RsiClose + r.RsiStreak + r.PercentRank) / 3;
}
}

return results;
}


private static void ValidateConnorsRsi(
IEnumerable<BasicData> basicData, int rsiPeriod, int streakPeriod, int rankPeriod)
{

// check parameters
if (rsiPeriod <= 1)
{
throw new BadParameterException("RSI period for Close price must be greater than 1 for ConnorsRsi.");
}

if (streakPeriod <= 1)
{
throw new BadParameterException("RSI period for Streak must be greater than 1 for ConnorsRsi.");
}

if (rankPeriod <= 1)
{
throw new BadParameterException("Percent Rank period must be greater than 1 for ConnorsRsi.");
}


// check history
int qtyHistory = basicData.Count();
int minHistory = Math.Max(rsiPeriod, Math.Max(streakPeriod, rankPeriod + 2));
if (qtyHistory < minHistory)
{
throw new BadHistoryException("Insufficient history provided for ConnorsRsi. " +
string.Format("You provided {0} periods of history when at least {1} is required. "
+ "Since this uses a smoothing technique, "
+ "we recommend you use at least 250 data points prior to the intended "
+ "usage date for maximum precision.", qtyHistory, minHistory));
}
}
}

}
60 changes: 60 additions & 0 deletions Indicators/ConnorsRsi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ConnorsRSI

A composite oscillator that incorporates RSI, winning/losing streaks, and percentile gain metrics on scale of 0 to 100.
[More info ...](https://alvarezquanttrading.com/wp-content/uploads/2016/05/ConnorsRSIGuidebook.pdf)

```csharp
// usage
IEnumerable<ConnorsRsiResult> results = Indicator.GetConnorsRsi(history, rsiPeriod, streakPeriod, rankPeriod);
```

## Parameters

| name | type | notes
| -- |-- |--
| `history` | IEnumerable\<[Quote](/GUIDE.md#Quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc).
| `rsiPeriod` | int | Lookback period (`R`) for the close price RSI. Must be greater than 1. Default is 3.
| `streakPeriod` | int | Lookback period (`S`) for the streak RSI. Must be greater than 1. Default is 2.
| `rankPeriod` | int | Lookback period (`P`) for the Percentile Rank. Must be greater than 1. Default is 100.

### History requirements

`N` is the greater of `R` and `S`, and `P`. You must supply at least `N+2` periods of `history`. Since this uses a smoothing technique, we recommend you use at least `N+250` data points prior to the intended usage date for maximum precision.

## Response

```csharp
IEnumerable<ConnorsRsiResult>
```

The first `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.

### ConnorsRsiResult

| name | type | notes
| -- |-- |--
| `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

## Example

```csharp
// fetch historical quotes from your favorite feed, in Quote format
IEnumerable<Quote> history = GetHistoryFromFeed("SPY");

// calculate ConnorsRsi(3,2.100)
IEnumerable<ConnorsRsiResult> results = Indicator.GetConnorsRsi(history,3,2,100);

// use results as needed
DateTime evalDate = DateTime.Parse("12/31/2018");
ConnorsRsiResult result = results.Where(x=>x.Date==evalDate).FirstOrDefault();
Console.WriteLine("ConnorsRSI on {0} was {1}", result.Date, result.ConnorsRsi);
```

```bash
ConnorsRSI on 12/31/2018 was 74.77
```
6 changes: 4 additions & 2 deletions Indicators/Rsi/Rsi.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

public class RsiResult : ResultBase
{
internal float Gain { get; set; } = 0;
internal float Loss { get; set; } = 0;
public float? Rsi { get; set; }
public bool? IsIncreasing { get; set; }

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

}
31 changes: 21 additions & 10 deletions Indicators/Rsi/Rsi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,41 @@ public static partial class Indicator
public static IEnumerable<RsiResult> GetRsi(IEnumerable<Quote> history, int lookbackPeriod = 14)
{

// clean quotes
history = Cleaners.PrepareHistory(history);
// convert history to basic format
IEnumerable<BasicData> bd = Cleaners.ConvertHistoryToBasic(history, "C");

// calculate
return CalcRsi(bd, lookbackPeriod);
}


private static IEnumerable<RsiResult> CalcRsi(IEnumerable<BasicData> basicData, int lookbackPeriod = 14)
{

// clean data
basicData = Cleaners.PrepareBasicData(basicData);

// check parameters
ValidateRsi(history, lookbackPeriod);
ValidateRsi(basicData, lookbackPeriod);

// initialize
decimal lastClose = history.First().Close;
decimal lastValue = basicData.First().Value;
List<RsiResult> results = new List<RsiResult>();

// load gain data
foreach (Quote h in history)
foreach (BasicData h in basicData)
{

RsiResult result = new RsiResult
{
Index = (int)h.Index,
Date = h.Date,
Gain = (lastClose < h.Close) ? (float)(h.Close - lastClose) : 0,
Loss = (lastClose > h.Close) ? (float)(lastClose - h.Close) : 0
Gain = (lastValue < h.Value) ? (float)(h.Value - lastValue) : 0,
Loss = (lastValue > h.Value) ? (float)(lastValue - h.Value) : 0
};
results.Add(result);

lastClose = h.Close;
lastValue = h.Value;
}

// initialize average gain
Expand Down Expand Up @@ -81,7 +92,7 @@ public static IEnumerable<RsiResult> GetRsi(IEnumerable<Quote> history, int look
}


private static void ValidateRsi(IEnumerable<Quote> history, int lookbackPeriod)
private static void ValidateRsi(IEnumerable<BasicData> basicData, int lookbackPeriod)
{

// check parameters
Expand All @@ -91,7 +102,7 @@ private static void ValidateRsi(IEnumerable<Quote> history, int lookbackPeriod)
}

// check history
int qtyHistory = history.Count();
int qtyHistory = basicData.Count();
int minHistory = lookbackPeriod;
if (qtyHistory < minHistory)
{
Expand Down
Binary file modified IndicatorsTests/Test Data/History.xlsx
Binary file not shown.
79 changes: 79 additions & 0 deletions IndicatorsTests/Test.ConnorsRsi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Skender.Stock.Indicators;
using System;
using System.Collections.Generic;
using System.Linq;

namespace StockIndicators.Tests
{
[TestClass]
public class ConnorsRsiTests : TestBase
{

[TestMethod()]
public void GetConnorsRsiTest()
{
int rsiPeriod = 3;
int streakPeriod = 2;
int rankPeriod = 100;
int startPeriod = Math.Max(rsiPeriod, Math.Max(streakPeriod, rankPeriod)) + 2;

IEnumerable<ConnorsRsiResult> results1 = Indicator.GetConnorsRsi(history, rsiPeriod, streakPeriod, rankPeriod);

// assertions

// proper quantities
// should always be the same number of results as there is history
Assert.AreEqual(502, results1.Count());
Assert.AreEqual(502 - startPeriod + 1, results1.Where(x => x.ConnorsRsi != null).Count());

// sample value
ConnorsRsiResult r1 = results1.Where(x => x.Date == DateTime.Parse("12/31/2018")).FirstOrDefault();
Assert.AreEqual((decimal)68.8087, Math.Round((decimal)r1.RsiClose, 4));
Assert.AreEqual((decimal)67.4899, Math.Round((decimal)r1.RsiStreak, 4));
Assert.AreEqual((decimal)88.0000, Math.Round((decimal)r1.PercentRank, 4));
Assert.AreEqual((decimal)74.7662, Math.Round((decimal)r1.ConnorsRsi, 4));

// different parameters
IEnumerable<ConnorsRsiResult> results2 = Indicator.GetConnorsRsi(history, 14, 20, 10);
ConnorsRsiResult r2 = results2.Where(x => x.Date == DateTime.Parse("12/31/2018")).FirstOrDefault();
Assert.AreEqual((decimal)42.0773, Math.Round((decimal)r2.RsiClose, 4));
Assert.AreEqual((decimal)52.7386, Math.Round((decimal)r2.RsiStreak, 4));
Assert.AreEqual((decimal)90.0000, Math.Round((decimal)r2.PercentRank, 4));
Assert.AreEqual((decimal)61.6053, Math.Round((decimal)r2.ConnorsRsi, 4));

}


/* EXCEPTIONS */

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad RSI period.")]
public void BadRsiPeriod()
{
Indicator.GetConnorsRsi(history, 1, 2, 100);
}

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad Streak period.")]
public void BadStreakPeriod()
{
Indicator.GetConnorsRsi(history, 3, 1, 100);
}

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad Rank period.")]
public void BadPctRankPeriods()
{
Indicator.GetConnorsRsi(history, 3, 2, 1);
}

[TestMethod()]
[ExpectedException(typeof(BadHistoryException), "Insufficient history.")]
public void InsufficientHistory()
{
Indicator.GetConnorsRsi(history.Where(x => x.Index < 102), 3, 2, 100);
}

}
}

0 comments on commit 80a41bf

Please sign in to comment.