High-precision technical indicators with a growing streaming and performance-focused toolchain. This fork removes rounding, restores mathematical constants, and adds modern streaming, stateful indicators, and benchmark coverage while preserving the familiar API.
- Precision first: no
Math.Round, real constants (Math.PI,Math.Sqrt(2)), and full double precision output. - Streaming-ready: trade/quote/bar ingestion, timeframes, and stateful indicators that update incrementally.
- Multi-series streaming: register indicators that consume multiple symbols/timeframes with alignment policies.
- Performance focus: ongoing algorithmic and allocation optimizations with benchmarks to validate changes.
- Targets:
net461,net10.0.
See the full list in INDICATORS.md.
using OoplesFinance.StockIndicators.Models;
using OoplesFinance.StockIndicators;
var data = new List<TickerData>
{
new() { Date = DateTime.UtcNow, Open = 100, High = 101, Low = 99, Close = 100.5, Volume = 1000 },
new() { Date = DateTime.UtcNow.AddMinutes(1), Open = 100.5, High = 102, Low = 100, Close = 101.8, Volume = 900 }
};
var stockData = new StockData(data);
var sma = stockData.CalculateSimpleMovingAverage(20).CustomValuesList;using OoplesFinance.StockIndicators.Streaming;
var engine = new StreamingIndicatorEngine(new StreamingIndicatorEngineOptions
{
EmitUpdates = false
});
engine.RegisterStatefulIndicator(
"AAPL",
BarTimeframe.Tick,
new SimpleMovingAverageState(5),
update => Console.WriteLine($"SMA(5) = {update.Value:F4}"),
new IndicatorSubscriptionOptions { IncludeUpdates = false });
engine.OnTrade(new StreamTrade("AAPL", DateTime.UtcNow, 100, 1));
engine.OnTrade(new StreamTrade("AAPL", DateTime.UtcNow.AddSeconds(1), 101, 1));using OoplesFinance.StockIndicators.Enums;
using OoplesFinance.StockIndicators.Streaming;
var engine = new StreamingIndicatorEngine(new StreamingIndicatorEngineOptions
{
EmitUpdates = false
});
var primary = new SeriesKey("AAPL", BarTimeframe.Minutes(1));
var secondary = new SeriesKey("MSFT", BarTimeframe.Minutes(1));
engine.RegisterMultiSeriesIndicator(
primary,
new[] { secondary },
new SpreadState(primary, secondary),
update => Console.WriteLine($"Spread = {update.Value:F4}"),
new IndicatorSubscriptionOptions
{
IncludeUpdates = false,
SeriesAlignmentPolicy = SeriesAlignmentPolicy.Strict
});
// Custom multi-series example state
sealed class SpreadState : IMultiSeriesIndicatorState
{
private readonly SeriesKey _left;
private readonly SeriesKey _right;
public SpreadState(SeriesKey left, SeriesKey right)
{
_left = left;
_right = right;
}
public IndicatorName Name => IndicatorName.None;
public void Reset() { }
public MultiSeriesIndicatorStateResult Update(MultiSeriesContext context, SeriesKey series, OhlcvBar bar,
bool isFinal, bool includeOutputs)
{
if (!context.TryGetLatest(_left, out var left) || !context.TryGetLatest(_right, out var right))
{
return new MultiSeriesIndicatorStateResult(false, 0d, null);
}
var value = left.Close - right.Close;
return new MultiSeriesIndicatorStateResult(true, value, null);
}
}Notes:
- Default alignment is
SeriesAlignmentPolicy.LastKnown(emit using most recent bars). SeriesAlignmentPolicy.Strictrequires all series to share the sameEndTime. WithIncludeUpdates = false, alignment uses final bars only.
The v2.0 builder API provides zero-allocation indicator computation with fluent configuration:
using OoplesFinance.StockIndicators.Builder;
// Create data source
var stockData = new StockData(opens, highs, lows, closes, volumes, dates);
var source = IndicatorDataSource.FromBatch(stockData);
// Declare handles at outer scope for cross-lambda access
SeriesHandle sma = default, rsi = default;
SignalHandle overboughtSignal = default;
// Configure indicators with the fluent builder
var builder = new StockIndicatorBuilder(source)
.ConfigureIndicators(indicators =>
{
sma = indicators.Sma(20);
rsi = indicators.Rsi(14);
var bb = indicators.BollingerBands(20, 2);
var macd = indicators.Macd(12, 26, 9);
})
.ConfigureSignals(signals =>
{
// Define trading signals
overboughtSignal = signals.When(rsi).CrossesAbove(70).Emit("overbought");
signals.When(rsi).CrossesBelow(30).Emit("oversold");
// Group conditions
signals.Group(
SignalCondition.Above(rsi, 50),
SignalCondition.Above(sma, 100))
.All()
.ForBars(3)
.Emit("bullish");
})
.ConfigureNotifications(notify =>
{
notify.Console();
notify.Email(new EmailOptions { To = "alerts@example.com" });
notify.Telegram(new TelegramOptions { ChatId = "123456" });
})
.ConfigureAutoTrading(trade =>
{
trade.Alpaca(new AlpacaOptions { UsePaper = true })
.OnSignal(overboughtSignal)
.MarketSell();
});
// Build and use
using var runtime = builder.Build();
var smaBuffer = runtime.GetSeries(sma);
var values = smaBuffer.AsSpan(); // Zero-allocation access- 750+ Indicators: Access all indicators via
indicators.Calculate(IndicatorName, params)or typed methods - Zero Allocations:
IndicatorBuffer<T>usesArrayPool<T>for zero-allocation hot paths - SIMD Optimized: Math operations use loop unrolling for improved performance
- Lazy Evaluation: Only compute indicators referenced by signals
- Multi-Symbol Support:
indicators.For(symbol, timeframe)for cross-symbol analysis
| Adapter | Configuration |
|---|---|
| Console | notify.Console() |
notify.Email(new EmailOptions { SmtpHost, To }) |
|
| SMS | notify.Sms(new SmsOptions { AccountSid, ToNumber }) |
| Webhook | notify.Webhook(new WebhookOptions { Url }) |
| Telegram | notify.Telegram(new TelegramOptions { BotToken, ChatId }) |
| Discord | notify.Discord(new DiscordOptions { WebhookUrl }) |
| Adapter | Configuration |
|---|---|
| Console (dry-run) | trade.ConsoleAdapter() |
| Alpaca | trade.Alpaca(new AlpacaOptions { UsePaper = true }) |
For migration from v1.x, see MIGRATION.md.
The v2.0 Builder API represents a fundamental shift in how you work with indicators. Here's why you should consider upgrading:
When processing real-time data with incremental updates, v2.0's streaming engine significantly outperforms v1.0's batch recomputation approach:
| Scenario | v1.0 (us) | v2.0 (us) | Improvement |
|---|---|---|---|
| Streaming (10 updates, 3 indicators) | 27,136 | 14,987 | 45% faster |
Benchmark: 10,000 data points, SMA(14) + RSI(14) + Bollinger Bands(20,2). AMD Ryzen 9 3950X, .NET 10.0.
Why v2.0 wins at streaming: v1.0 must recompute the entire history for each new bar. v2.0's streaming engine processes data incrementally, only computing the new values.
When v1.0 is faster: For one-time batch calculations on static data, v1.0's direct Calculate* methods have less overhead since there's no builder setup cost. If you're doing a single calculation and never updating, v1.0 remains efficient.
Write your indicator logic once, use it for both historical analysis and live trading:
// Same builder configuration works for both modes
var builder = new StockIndicatorBuilder(source)
.ConfigureIndicators(ind => { sma = ind.Sma(20); rsi = ind.Rsi(14); })
.ConfigureSignals(sig => sig.When(rsi).CrossesAbove(70).Emit("overbought"));
// Batch mode: source = IndicatorDataSource.FromBatch(stockData)
// Streaming mode: source = IndicatorDataSource.FromStreaming(options)Compose indicators intuitively without manual data extraction:
// v1.0: Manual and error-prone
var smaResult = stockData.CalculateSimpleMovingAverage(20);
// Now manually extract values and feed to RSI... complex!
// v2.0: Natural composition
indicators.Then(sma).Rsi(14); // RSI of SMA - automatic!Detect trading signals declaratively:
.ConfigureSignals(signals =>
{
// Crossover detection
signals.When(fastMa).CrossesAbove(slowMa).Emit("golden_cross");
signals.When(rsi).CrossesBelow(30).Emit("oversold");
// Multi-condition groups
signals.Group(
SignalCondition.Above(rsi, 50),
SignalCondition.Above(price, sma200))
.All()
.ForBars(3)
.Emit("bullish_confirmation");
})Get alerted when signals fire:
.ConfigureNotifications(notify =>
{
notify.Console(); // Debug output
notify.Email(new EmailOptions { To = "..." }); // SMTP email
notify.Sms(new SmsOptions { ToNumber = "..." }); // Twilio SMS
notify.Telegram(new TelegramOptions { ChatId = "..." }); // Telegram bot
notify.Discord(new DiscordOptions { WebhookUrl = "..." }); // Discord
notify.Webhook(new WebhookOptions { Url = "..." }); // Custom webhook
})Execute trades automatically when signals fire:
.ConfigureAutoTrading(trade =>
{
trade.Alpaca(new AlpacaOptions { UsePaper = true })
.OnSignal(buySignal)
.MarketBuy(quantity: 10);
})| Use Case | Recommended API |
|---|---|
| One-time batch calculation | v1.0 Calculate* methods |
| Real-time streaming data | v2.0 Builder API |
| Multiple indicators together | v2.0 Builder API |
| Signal detection | v2.0 Builder API |
| Notifications/alerts | v2.0 Builder API |
| Auto-trading | v2.0 Builder API |
| Indicator chaining (RSI of SMA) | v2.0 Builder API |
Full benchmark results comparing v1.0 and v2.0 APIs (10,000 data points):
| Category | v1.0 (us) | v2.0 (us) | Notes |
|---|---|---|---|
| SMA single | 326 | 8,896 | v2.0 has builder setup overhead |
| EMA single | 306 | 9,958 | v2.0 has builder setup overhead |
| RSI single | 555 | 9,380 | v2.0 has builder setup overhead |
| Bollinger single | 1,091 | 12,699 | v2.0 has builder setup overhead |
| Streaming (10 updates) | 27,136 | 14,987 | v2.0 45% faster |
| Multi-indicator (4 ind.) | 3,258 | 15,491 | Setup overhead amortizes with more indicators |
The v2.0 benchmarks create a new builder for each call to measure full setup cost. In real applications, you create the builder once and reuse it, eliminating repeated setup overhead.
A developer console is available to run batch, streaming, and multi-series examples locally.
dotnet run --project examples/OoplesFinance.StockIndicators.DevConsole/OoplesFinance.StockIndicators.DevConsole.csprojNon-interactive:
dotnet run --project examples/OoplesFinance.StockIndicators.DevConsole/OoplesFinance.StockIndicators.DevConsole.csproj -- --run-all --no-pauseSample BenchmarkDotNet results (Count=10000, net10.0). Optimized = this fork, Baseline = original library.
Length = 14
| Indicator | Optimized (us) | Baseline (us) | Speedup |
|---|---|---|---|
| SMA | 319.6 | 13636.9 | 42.7x |
| EMA | 323.0 | 814.4 | 2.5x |
| RSI | 889.0 | 18767.6 | 21.1x |
| MACD | 549.2 | 8872.7 | 16.2x |
| Bollinger Bands | 1292.2 | 33604.7 | 26.0x |
| ATR | 412.9 | 13023.5 | 31.5x |
| Chande CMO | 679.5 | 27632.6 | 40.7x |
| Ulcer Index | 2620.4 | 30663.0 | 11.7x |
Length = 50
| Indicator | Optimized (us) | Baseline (us) | Speedup |
|---|---|---|---|
| SMA | 321.8 | 28647.1 | 89.0x |
| EMA | 304.9 | 783.4 | 2.6x |
| RSI | 879.0 | 18738.7 | 21.3x |
| MACD | 551.2 | 8919.8 | 16.2x |
| Bollinger Bands | 1299.6 | 23400.2 | 18.0x |
| ATR | 375.1 | 13073.6 | 34.9x |
| Chande CMO | 644.5 | 11835.8 | 18.4x |
| Ulcer Index | 2756.0 | 14288.4 | 5.2x |
Length = 200
| Indicator | Optimized (us) | Baseline (us) | Speedup |
|---|---|---|---|
| SMA | 307.8 | 14493.6 | 47.1x |
| EMA | 304.2 | 950.9 | 3.1x |
| RSI | 925.7 | 18540.0 | 20.0x |
| MACD | 543.8 | 8948.7 | 16.5x |
| Bollinger Bands | 1820.9 | 67349.6 | 37.0x |
| ATR | 394.2 | 13098.8 | 33.2x |
| Chande CMO | 630.4 | 33713.7 | 53.5x |
| Ulcer Index | 2749.9 | 72270.8 | 26.3x |
Streaming fanout (100k ticks, 5 timeframes):
| Scenario | Indicators | Outputs | Throughput (ticks/sec) | p95 latency (ms) | Updates |
|---|---|---|---|---|---|
| Core | 10 | off | 207,346 | 0.01 | 6,001,190 |
| Core + outputs | 10 | on | 109,301 | 0.02 | 6,001,190 |
| Extended | 15 | off | 140,680 | 0.01 | 9,001,785 |
Streaming scaling (100k ticks, outputs off):
| Timeframes | Indicators | Throughput (ticks/sec) | p95 latency (ms) | Updates |
|---|---|---|---|---|
| 1 | 5 | 616,430 | 0.00 | 1,000,000 |
| 3 | 5 | 404,420 | 0.00 | 2,000,590 |
| 5 | 5 | 263,733 | 0.01 | 3,000,595 |
| 5 | 10 | 207,346 | 0.01 | 6,001,190 |
| 5 | 15 | 140,680 | 0.01 | 9,001,785 |
Full reports: BenchmarkDotNet.Artifacts/results/OoplesFinance.StockIndicators.Benchmarks.IndicatorBenchmarks-report-github.md
Benchmarks live in benchmarks/ and include batch and streaming performance harnesses.
dotnet run --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -c Release
Run streaming fanout throughput/latency:
dotnet run --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -c Release -- --streaming-perf --ticks 100000
Compare optimized vs baseline:
.\benchmarks\setup-baseline.ps1 -Ref master
dotnet build -c Release -p:AssemblyName=OoplesFinance.StockIndicators.Original -p:TargetFramework=net10.0 benchmarks/.baseline/src/OoplesFinance.StockIndicators.csproj
dotnet run --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -c Release -- --filter *IndicatorBenchmarks*
Results are written to BenchmarkDotNet.Artifacts/.
src/core librarytests/unit testsbenchmarks/BenchmarkDotNet suiteexamples/developer console and examples
INDICATORS.mdlist of indicatorsOPTIMIZATIONS.mdoptimization backlog and notesMODERNIZATION_PLAN.mdroadmap for refactors and performance work
Apache 2.0. See LICENSE.txt.