Skip to content

Commit

Permalink
add Detrended Price Oscillator (#552)
Browse files Browse the repository at this point in the history
+semver: minor
  • Loading branch information
DaveSkender authored Sep 6, 2021
1 parent 8fbafa6 commit 870ee07
Show file tree
Hide file tree
Showing 11 changed files with 797 additions and 1 deletion.
67 changes: 67 additions & 0 deletions docs/_indicators/Dpo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Detrended Price Oscillator (DPO)
permalink: /indicators/Dpo/
layout: default
---

# {{ page.title }}

[Detrended Price Oscillator](https://en.wikipedia.org/wiki/Detrended_price_oscillator) depicts the difference between price and an offset simple moving average. It is used to identify trend cycles and duration.
[[Discuss] :speech_balloon:]({{site.github.repository_url}}/discussions/551 "Community discussion about this indicator")

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

```csharp
// usage
IEnumerable<DpoResult> results =
quotes.GetDpo(lookbackPeriods);
```

## Parameters

| name | type | notes
| -- |-- |--
| `lookbackPeriods` | int | Number of periods (`N`) in the moving average. Must be greater than 0.

### Historical quotes requirements

You must have at least `N` historical quotes.

`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<DpoResult>
```

- 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 `N/2-2` and last `N/2+1` periods will be `null` since they cannot be calculated.

### DpoResult

| name | type | notes
| -- |-- |--
| `Date` | DateTime | Date
| `Sma` | decimal | Simple moving average offset by `N/2+1` periods
| `Dpo` | decimal | Detrended Price Oscillator (DPO)

### Utilities

- [.ConvertToQuotes()]({{site.baseurl}}/utilities#convert-to-quotes)
- [.Find(lookupDate)]({{site.baseurl}}/utilities#find-indicator-result-by-date)
- [.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
IEnumerable<DpoResult> results = quotes.GetDpo(14);
```
Binary file added docs/assets/charts/Dpo.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 @@ -46,6 +46,7 @@ redirect_from:

- [ConnorsRSI](../indicators/ConnorsRsi/#content)
- [Commodity Channel Index](../indicators/Cci/#content)
- [Detrended Price Oscillator (DPO)](../indicators/Dpo/#content)
- [Relative Strength Index (RSI)](../indicators/Rsi/#content)
- [ROC with Bands](../indicators/Roc/#roc-with-bands)
- [Stochastic Oscillator](../indicators/Stoch/#content) and [KDJ Index](../indicators/Stoch/#content)
Expand Down
11 changes: 11 additions & 0 deletions src/A-D/Dpo/Dpo.Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace Skender.Stock.Indicators
{
[Serializable]
public class DpoResult : ResultBase
{
public decimal? Sma { get; set; }
public decimal? Dpo { get; set; }
}
}
102 changes: 102 additions & 0 deletions src/A-D/Dpo/Dpo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Skender.Stock.Indicators
{
public static partial class Indicator
{
// DETRENDED PRICE OSCILLATOR (DPO)
/// <include file='./info.xml' path='indicator/*' />
///
public static IEnumerable<DpoResult> GetDpo<TQuote>(
this IEnumerable<TQuote> quotes,
int lookbackPeriods)
where TQuote : IQuote
{

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

// check parameter arguments
ValidateDpo(quotes, lookbackPeriods);

// initialize
int size = quotesList.Count;
int offset = lookbackPeriods / 2 + 1;
List<SmaResult> sma = quotes.GetSma(lookbackPeriods).ToList();
List<DpoResult> results = new(size);

// roll through quotes
for (int i = 0; i < size; i++)
{
TQuote q = quotesList[i];

DpoResult r = new()
{
Date = q.Date
};
results.Add(r);

if (i >= lookbackPeriods - offset - 1 && i < size - offset)
{
SmaResult s = sma[i + offset];
r.Sma = s.Sma;
r.Dpo = q.Close - s.Sma;
}
}

return results;
}


// convert to quotes
/// <include file='../../_Common/Results/info.xml' path='info/type[@name="Convert"]/*' />
///
public static IEnumerable<Quote> ConvertToQuotes(
this IEnumerable<DpoResult> results)
{
return results
.Where(x => x.Dpo != null)
.Select(x => new Quote
{
Date = x.Date,
Open = (decimal)x.Dpo,
High = (decimal)x.Dpo,
Low = (decimal)x.Dpo,
Close = (decimal)x.Dpo,
Volume = (decimal)x.Dpo
})
.ToList();
}


// parameter validation
private static void ValidateDpo<TQuote>(
IEnumerable<TQuote> quotes,
int lookbackPeriods)
where TQuote : IQuote
{

// check parameter arguments
if (lookbackPeriods <= 0)
{
throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods,
"Lookback periods must be greater than 0 for DPO.");
}

// check quotes
int qtyHistory = quotes.Count();
int minHistory = lookbackPeriods;
if (qtyHistory < minHistory)
{
string message = string.Format(EnglishCulture,
"Insufficient quotes provided for Detrended Price Oscillator. " +
"You provided {0} periods of quotes when at least {1} are required.",
qtyHistory, minHistory);

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

<indicator>
<summary>
Detrended Price Oscillator (DPO) depicts the difference between price and an offset simple moving average.
See
<see href="https://daveskender.github.io/Stock.Indicators/indicators/Dpo/#content">documentation</see>
for more information.
</summary>
<typeparam name="TQuote">Configurable Quote type. See Guide for more information.</typeparam>
<param name="quotes">Historical price quotes.</param>
<param name="lookbackPeriods">Number of periods in the lookback window.</param>
<returns>Time series of DPO values.</returns>
<exception cref="ArgumentOutOfRangeException">Invalid parameter value provided.</exception>
<exception cref="BadQuotesException">Insufficient quotes provided.</exception>
</indicator>
Binary file added tests/indicators/A-D/Dpo/Dpo.Calc.xlsx
Binary file not shown.
87 changes: 87 additions & 0 deletions tests/indicators/A-D/Dpo/Test.Dpo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Skender.Stock.Indicators;

namespace Internal.Tests
{
[TestClass]
public class Dpo : TestBase
{
[TestMethod]
public void Standard()
{
List<DpoResult> act = quotes.GetDpo(14)
.ToList();

// get test data
List<DpoResult> exp = File.ReadAllLines("A-D/Dpo/data.csv")
.Skip(1)
.Select(t =>
{
string[] csv = t.Split(",");
return new DpoResult
{
Date = Convert.ToDateTime(csv[1], englishCulture),
Sma = decimal.TryParse(csv[6], out decimal sma) ? sma : null,
Dpo = decimal.TryParse(csv[7], out decimal dpo) ? dpo : null
};
})
.ToList();

// assertions
Assert.AreEqual(exp.Count, act.Count);

// compare all values
for (int i = 0; i < exp.Count; i++)
{
DpoResult e = exp[i];
DpoResult a = act[i];

Assert.AreEqual(e.Date, a.Date);
Assert.AreEqual(e.Sma, a.Sma == null
? a.Sma
: Math.Round((decimal)a.Sma, 5),
$"at index {i}");
Assert.AreEqual(e.Dpo, a.Dpo == null
? a.Dpo
: Math.Round((decimal)a.Dpo, 5),
$"at index {i}");
}
}

[TestMethod]
public void ConvertToQuotes()
{
List<Quote> newQuotes = quotes.GetDpo(14)
.ConvertToQuotes()
.ToList();

Assert.AreEqual(489, newQuotes.Count);

Quote q = newQuotes.LastOrDefault();
Assert.AreEqual(2.18214m, Math.Round(q.Close, 5));
}

[TestMethod]
public void BadData()
{
IEnumerable<DpoResult> r = badQuotes.GetDpo(5);
Assert.AreEqual(502, r.Count());
}

[TestMethod]
public void Exceptions()
{
// bad SMA period
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
quotes.GetDpo(0));

// insufficient quotes
Assert.ThrowsException<BadQuotesException>(() =>
Indicator.GetDpo(TestData.GetDefault(10), 11));
}
}
}
Loading

0 comments on commit 870ee07

Please sign in to comment.