-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
7 changed files
with
333 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} | ||
} |