Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 14 additions & 48 deletions src/s-z/WilliamsR/WilliamsR.BufferList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Skender.Stock.Indicators;
/// </summary>
public class WilliamsRList : BufferList<WilliamsResult>, IIncrementFromQuote, IWilliamsR
{
private readonly Queue<(double High, double Low)> _buffer;
private readonly StochList _stochList;

/// <summary>
/// Initializes a new instance of the <see cref="WilliamsRList"/> class.
Expand All @@ -17,7 +17,9 @@ public WilliamsRList(
WilliamsR.Validate(lookbackPeriods);
LookbackPeriods = lookbackPeriods;

_buffer = new Queue<(double, double)>(lookbackPeriods);
// Williams %R is Fast Stochastic (K) - 100
// Fast Stochastic parameters: lookback, signal=1, smooth=1
_stochList = new StochList(lookbackPeriods, 1, 1, 3, 2, MaType.SMA);
}

/// <summary>
Expand All @@ -39,7 +41,15 @@ public WilliamsRList(
public void Add(IQuote quote)
{
ArgumentNullException.ThrowIfNull(quote);
Add(quote.Timestamp, (double)quote.High, (double)quote.Low, (double)quote.Close);
_stochList.Add(quote);

// Convert Stochastic result to Williams %R
StochResult stochResult = _stochList[^1];
WilliamsResult williamsResult = new(
Timestamp: stochResult.Timestamp,
WilliamsR: stochResult.Oscillator - 100d);

AddInternal(williamsResult);
}

/// <inheritdoc />
Expand All @@ -53,54 +63,10 @@ public void Add(IReadOnlyList<IQuote> quotes)
}
}

/// <summary>
/// Adds a new quote data point for Williams %R calculation.
/// </summary>
/// <param name="timestamp">The timestamp of the data point.</param>
/// <param name="high">The high price.</param>
/// <param name="low">The low price.</param>
/// <param name="close">The close price.</param>
public void Add(DateTime timestamp, double high, double low, double close)
{
// Update rolling buffer using BufferListUtilities with consolidated tuple
_buffer.Update(LookbackPeriods, (high, low));

// Calculate Williams %R when we have enough data
double? williamsR = null;
if (_buffer.Count == LookbackPeriods)
{
double highHigh = double.MinValue;
double lowLow = double.MaxValue;

foreach ((double High, double Low) in _buffer)
{
if (High > highHigh)
{
highHigh = High;
}

if (Low < lowLow)
{
lowLow = Low;
}
}

// Williams %R is Fast Stochastic - 100
williamsR = highHigh - lowLow != 0
? 100d * (close - lowLow) / (highHigh - lowLow) - 100d
: double.NaN;
}

// Add result to the list
AddInternal(new WilliamsResult(
Timestamp: timestamp,
WilliamsR: williamsR.Nan2Null()));
}

/// <inheritdoc />
public override void Clear()
{
base.Clear();
_buffer.Clear();
_stochList.Clear();
}
}
27 changes: 13 additions & 14 deletions src/s-z/WilliamsR/WilliamsR.StreamHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ namespace Skender.Stock.Indicators;
public class WilliamsRHub
: StreamHub<IQuote, WilliamsResult>, IWilliamsR
{

private readonly string hubName;
private readonly RollingWindowMax<double> _highWindow;
private readonly RollingWindowMin<double> _lowWindow;
Expand All @@ -23,8 +22,8 @@ internal WilliamsRHub(
WilliamsR.Validate(lookbackPeriods);

LookbackPeriods = lookbackPeriods;
_highWindow = new RollingWindowMax<decimal>(lookbackPeriods);
_lowWindow = new RollingWindowMin<decimal>(lookbackPeriods);
_highWindow = new RollingWindowMax<double>(lookbackPeriods);
_lowWindow = new RollingWindowMin<double>(lookbackPeriods);

hubName = $"WILLR({lookbackPeriods})";

Expand All @@ -46,26 +45,27 @@ protected override (WilliamsResult result, int index)
ArgumentNullException.ThrowIfNull(item);
int i = indexHint ?? ProviderCache.IndexOf(item, true);

double o = (double)item.Open;
double h = (double)item.High;
double c = (double)item.Close;
double high = (double)item.High;
double low = (double)item.Low;
double close = (double)item.Close;

_highWindow.Add(h);
_lowWindow.Add(l);
// Update rolling windows for O(1) amortized max/min tracking
_highWindow.Add(high);
_lowWindow.Add(low);

// Calculate Williams %R
// Williams %R is Fast Stochastic - 100
// Williams %R is Fast Stochastic (K) - 100
double williamsR = double.NaN;
if (i >= LookbackPeriods - 1)
{
// Get highest high and lowest low from rolling windows (O(1))
double highHigh = _highWindow.GetMax();
double lowLow = _lowWindow.GetMin();

// Return NaN when range is zero (undefined %R)
williamsR = highHigh - lowLow != 0
? 100d * (c - lowLow) / (highHigh - lowLow) - 100d
: double.NaN;
// Williams %R formula matches Stochastic %K - 100
williamsR = highHigh == lowLow
? double.NaN
: (100d * (close - lowLow) / (highHigh - lowLow)) - 100d;
}

WilliamsResult result = new(
Expand Down Expand Up @@ -111,7 +111,6 @@ protected override void RollbackState(DateTime timestamp)
_lowWindow.Add((double)quote.Low);
}
}

}

public static partial class WilliamsR
Expand Down
52 changes: 36 additions & 16 deletions tests/indicators/s-z/WilliamsR/WilliamsR.StreamHub.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,53 @@ public class WilliamsR : StreamHubTestBase, ITestQuoteObserver
[TestMethod]
public void QuoteObserver_WithWarmupLateArrivalAndRemoval_MatchesSeriesExactly()
{
List<Quote> quotesList = Quotes.ToList();
const int lookbackPeriods = 14;
int length = Quotes.Count;

// setup quote provider hub and observer BEFORE adding data
// setup quote provider hub
QuoteHub quoteHub = new();
WilliamsRHub observer = quoteHub.ToWilliamsRHub(14);

// add base quotes (batch)
quoteHub.Add(quotesList.Take(200));
// prefill quotes at provider (warmup coverage)
quoteHub.Add(Quotes.Take(20));

// initialize observer
WilliamsRHub observer = quoteHub.ToWilliamsRHub(lookbackPeriods);

// add incremental quotes
for (int i = 200; i < length; i++)
// fetch initial results (early)
IReadOnlyList<WilliamsResult> actuals = observer.Results;

// emulate adding quotes to provider hub
for (int i = 20; i < length; i++)
{
Quote q = quotesList[i];
// skip one (add later)
if (i == 80) { continue; }

Quote q = Quotes[i];
quoteHub.Add(q);

// resend duplicate quotes
if (i is > 100 and < 105) { quoteHub.Add(q); }
}

// close observations
quoteHub.EndTransmission();
// late arrival, should equal series
quoteHub.Insert(Quotes[80]);

// assert results
observer.Cache.Should().HaveCount(length);
IReadOnlyList<WilliamsResult> expectedOriginal = Quotes.ToWilliamsR(lookbackPeriods);

// verify against static series calculation
IReadOnlyList<WilliamsResult> expected = Quotes.ToWilliamsR(14);
observer.Cache.Should().HaveCount(expected.Count);
observer.Cache.Should().BeEquivalentTo(expected);
actuals.Should().HaveCount(length);
actuals.Should().BeEquivalentTo(expectedOriginal, static options => options.WithStrictOrdering());

// delete, should equal series (revised)
quoteHub.Remove(Quotes[removeAtIndex]);

IReadOnlyList<WilliamsResult> expectedRevised = RevisedQuotes.ToWilliamsR(lookbackPeriods);

actuals.Should().HaveCount(501);
actuals.Should().BeEquivalentTo(expectedRevised, static options => options.WithStrictOrdering());

// cleanup
observer.Unsubscribe();
quoteHub.EndTransmission();
}

[TestMethod]
Expand Down
Loading