Skip to content

Commit 6a78e52

Browse files
CopilotDaveSkender
andauthored
feat: StdDev StreamHub implemented (#1690)
Signed-off-by: Dave Skender <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: DaveSkender <[email protected]>
1 parent 7a60257 commit 6a78e52

File tree

9 files changed

+395
-8
lines changed

9 files changed

+395
-8
lines changed

docs/_indicators/StdDev.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ layout: indicator
1515
![chart for {{page.title}}]({{site.baseurl}}{{page.image}})
1616

1717
```csharp
18-
// C# usage syntax
18+
// C# usage syntax (series)
1919
IReadOnlyList<StdDevResult> results =
2020
quotes.ToStdDev(lookbackPeriods);
2121

22-
// usage with optional SMA of SD (shown above)
23-
IReadOnlyList<StdDevResult> results =
24-
quotes.ToStdDev(lookbackPeriods, smaPeriods);
22+
// usage with streaming quotes
23+
StdDevHub hub = quotes.ToStdDevHub(lookbackPeriods);
2524
```
2625

2726
## Parameters

src/_common/Catalog/Catalog.Listings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,9 @@ private static void PopulateCatalog()
361361
_listings.Add(Stc.StreamListing);
362362

363363
// Standard Deviation
364+
_listings.Add(StdDev.BufferListing);
364365
_listings.Add(StdDev.SeriesListing);
366+
_listings.Add(StdDev.StreamListing);
365367

366368
// Standard Deviation Channels
367369
_listings.Add(StdDevChannels.BufferListing);

src/s-z/StdDev/IStdDev.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Skender.Stock.Indicators;
2+
3+
/// <summary>
4+
/// Interface for Standard Deviation (StdDev) calculations.
5+
/// </summary>
6+
public interface IStdDev
7+
{
8+
/// <summary>
9+
/// Gets the number of periods to look back for the calculation.
10+
/// </summary>
11+
int LookbackPeriods { get; }
12+
}

src/s-z/StdDev/StdDev.Catalog.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ public static partial class StdDev
2323
.WithMethodName("ToStdDev")
2424
.Build();
2525

26-
// No StreamListing for Standard Deviation.
27-
// No BufferListing for Standard Deviation.
26+
/// <summary>
27+
/// Standard Deviation Stream Listing
28+
/// </summary>
29+
internal static readonly IndicatorListing StreamListing =
30+
new CatalogListingBuilder(CommonListing)
31+
.WithStyle(Style.Stream)
32+
.WithMethodName("ToStdDevHub")
33+
.Build();
34+
35+
/// <summary>
36+
/// Standard Deviation Buffer Listing
37+
/// </summary>
38+
internal static readonly IndicatorListing BufferListing =
39+
new CatalogListingBuilder(CommonListing)
40+
.WithStyle(Style.Buffer)
41+
.WithMethodName("ToStdDevList")
42+
.Build();
2843
}

src/s-z/StdDev/StdDev.StreamHub.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
namespace Skender.Stock.Indicators;
2+
3+
// STANDARD DEVIATION (STREAM HUB)
4+
5+
/// <summary>
6+
/// Represents a Standard Deviation stream hub.
7+
/// </summary>
8+
public class StdDevHub
9+
: ChainProvider<IReusable, StdDevResult>, IStdDev
10+
{
11+
#region fields
12+
13+
private readonly string hubName;
14+
15+
#endregion fields
16+
17+
#region constructors
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="StdDevHub"/> class.
21+
/// </summary>
22+
/// <param name="provider">The chain provider.</param>
23+
/// <param name="lookbackPeriods">Quantity of periods in lookback window.</param>
24+
internal StdDevHub(
25+
IChainProvider<IReusable> provider,
26+
int lookbackPeriods) : base(provider)
27+
{
28+
StdDev.Validate(lookbackPeriods);
29+
LookbackPeriods = lookbackPeriods;
30+
hubName = $"STDDEV({lookbackPeriods})";
31+
32+
Reinitialize();
33+
}
34+
35+
#endregion constructors
36+
37+
#region properties
38+
39+
/// <summary>
40+
/// Gets the number of lookback periods.
41+
/// </summary>
42+
public int LookbackPeriods { get; init; }
43+
44+
#endregion properties
45+
46+
#region methods
47+
48+
/// <inheritdoc/>
49+
public override string ToString() => hubName;
50+
51+
/// <inheritdoc/>
52+
protected override (StdDevResult result, int index)
53+
ToIndicator(IReusable item, int? indexHint)
54+
{
55+
ArgumentNullException.ThrowIfNull(item);
56+
57+
int i = indexHint ?? ProviderCache.IndexOf(item, true);
58+
59+
// Calculate StdDev using two-pass algorithm over ProviderCache
60+
// This is O(lookbackPeriods) complexity (linear in lookback period)
61+
// Two-pass method is necessary for numerical stability and exact Series precision
62+
double? stdDev = null;
63+
double? mean = null;
64+
double? zScore = null;
65+
66+
if (i >= LookbackPeriods - 1)
67+
{
68+
double sum = 0;
69+
bool hasNaN = false;
70+
71+
// Calculate sum first and check for NaN values
72+
for (int p = i - LookbackPeriods + 1; p <= i; p++)
73+
{
74+
double value = ProviderCache[p].Value;
75+
if (double.IsNaN(value))
76+
{
77+
hasNaN = true;
78+
break;
79+
}
80+
81+
sum += value;
82+
}
83+
84+
if (!hasNaN)
85+
{
86+
// Calculate mean
87+
mean = sum / LookbackPeriods;
88+
89+
// Calculate sum of squared deviations (numerically stable method)
90+
double sumSqDev = 0;
91+
for (int p = i - LookbackPeriods + 1; p <= i; p++)
92+
{
93+
double value = ProviderCache[p].Value;
94+
double deviation = value - mean.Value;
95+
sumSqDev += deviation * deviation;
96+
}
97+
98+
// Calculate standard deviation
99+
stdDev = Math.Sqrt(sumSqDev / LookbackPeriods);
100+
101+
// Calculate z-score
102+
zScore = stdDev == 0 ? double.NaN : (item.Value - mean.Value) / stdDev.Value;
103+
}
104+
}
105+
106+
// candidate result
107+
StdDevResult r = new(
108+
Timestamp: item.Timestamp,
109+
StdDev: stdDev,
110+
Mean: mean,
111+
ZScore: zScore.NaN2Null());
112+
113+
return (r, i);
114+
}
115+
116+
#endregion methods
117+
}
118+
119+
/// <summary>
120+
/// Provides methods for creating StdDev hubs.
121+
/// </summary>
122+
public static partial class StdDev
123+
{
124+
/// <summary>
125+
/// Converts the chain provider to a StdDev hub.
126+
/// </summary>
127+
/// <param name="chainProvider">The chain provider.</param>
128+
/// <param name="lookbackPeriods">Quantity of periods in lookback window.</param>
129+
/// <returns>A StdDev hub.</returns>
130+
/// <exception cref="ArgumentNullException">Thrown when the chain provider is null.</exception>
131+
/// <exception cref="ArgumentOutOfRangeException">Thrown when the lookback periods are invalid.</exception>
132+
public static StdDevHub ToStdDevHub(
133+
this IChainProvider<IReusable> chainProvider,
134+
int lookbackPeriods = 14)
135+
=> new(chainProvider, lookbackPeriods);
136+
137+
/// <summary>
138+
/// Creates a StdDev hub from a collection of quotes.
139+
/// </summary>
140+
/// <param name="quotes">Aggregate OHLCV quote bars, time sorted.</param>
141+
/// <param name="lookbackPeriods">Quantity of periods in lookback window.</param>
142+
/// <returns>An instance of <see cref="StdDevHub"/>.</returns>
143+
public static StdDevHub ToStdDevHub(
144+
this IReadOnlyList<IQuote> quotes,
145+
int lookbackPeriods = 14)
146+
{
147+
QuoteHub quoteHub = new();
148+
quoteHub.Add(quotes);
149+
return quoteHub.ToStdDevHub(lookbackPeriods);
150+
}
151+
}

tests/indicators/s-z/StdDev/StdDev.Catalog.Tests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,62 @@ public void StdDevSeriesListing()
3434
stddevResult?.DisplayName.Should().Be("Standard Deviation");
3535
stddevResult.IsReusable.Should().Be(true);
3636
}
37+
38+
[TestMethod]
39+
public void StdDevStreamListing()
40+
{
41+
// Act
42+
IndicatorListing listing = StdDev.StreamListing;
43+
44+
// Assert
45+
listing.Should().NotBeNull();
46+
listing.Name.Should().Be("Standard Deviation");
47+
listing.Uiid.Should().Be("STDEV");
48+
listing.Style.Should().Be(Style.Stream);
49+
listing.Category.Should().Be(Category.PriceCharacteristic);
50+
listing.MethodName.Should().Be("ToStdDevHub");
51+
52+
listing.Parameters.Should().NotBeNull();
53+
listing.Parameters.Should().HaveCount(1);
54+
55+
IndicatorParam lookbackPeriodsParam = listing.Parameters.SingleOrDefault(static p => p.ParameterName == "lookbackPeriods");
56+
lookbackPeriodsParam.Should().NotBeNull();
57+
58+
listing.Results.Should().NotBeNull();
59+
listing.Results.Should().HaveCount(1);
60+
61+
IndicatorResult stddevResult = listing.Results.SingleOrDefault(static r => r.DataName == "StdDev");
62+
stddevResult.Should().NotBeNull();
63+
stddevResult!.DisplayName.Should().Be("Standard Deviation");
64+
stddevResult.IsReusable.Should().Be(true);
65+
}
66+
67+
[TestMethod]
68+
public void StdDevBufferListing()
69+
{
70+
// Act
71+
IndicatorListing listing = StdDev.BufferListing;
72+
73+
// Assert
74+
listing.Should().NotBeNull();
75+
listing.Name.Should().Be("Standard Deviation");
76+
listing.Uiid.Should().Be("STDEV");
77+
listing.Style.Should().Be(Style.Buffer);
78+
listing.Category.Should().Be(Category.PriceCharacteristic);
79+
listing.MethodName.Should().Be("ToStdDevList");
80+
81+
listing.Parameters.Should().NotBeNull();
82+
listing.Parameters.Should().HaveCount(1);
83+
84+
IndicatorParam lookbackPeriodsParam = listing.Parameters.SingleOrDefault(static p => p.ParameterName == "lookbackPeriods");
85+
lookbackPeriodsParam.Should().NotBeNull();
86+
87+
listing.Results.Should().NotBeNull();
88+
listing.Results.Should().HaveCount(1);
89+
90+
IndicatorResult stddevResult = listing.Results.SingleOrDefault(static r => r.DataName == "StdDev");
91+
stddevResult.Should().NotBeNull();
92+
stddevResult!.DisplayName.Should().Be("Standard Deviation");
93+
stddevResult.IsReusable.Should().Be(true);
94+
}
3795
}

tests/indicators/s-z/StdDev/StdDev.Regression.Tests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public StdDevTests() : base("stdev.standard.json") { }
99
public override void Series() => Quotes.ToStdDev().AssertEquals(Expected);
1010

1111
[TestMethod]
12-
public override void Buffer() => Assert.Inconclusive("Buffer implementation not yet available");
12+
public override void Buffer() => Quotes.ToStdDevList(14).AssertEquals(Expected);
1313

1414
[TestMethod]
15-
public override void Stream() => Assert.Inconclusive("Stream implementation not yet available");
15+
public override void Stream() => Quotes.ToStdDevHub(14).Results.AssertEquals(Expected);
1616
}

0 commit comments

Comments
 (0)