feat: fluent high-level API (DRAFT — contains a semantic change to 5 shipping indicators) - #110
Draft
phmatray wants to merge 6 commits into
Draft
feat: fluent high-level API (DRAFT — contains a semantic change to 5 shipping indicators)#110phmatray wants to merge 6 commits into
phmatray wants to merge 6 commits into
Conversation
Adds the empty project skeletons, central package versions and solution entries for the v1.0 launch work tracked in #63. Implementations land in follow-up commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The push trigger and the pre-release branch still named `develop`, a branch this repository does not have, so no push to the default branch has ever run CI. #101 fixed the pull_request trigger only. Also carries over the .gitignore entries from the abandoned WIP-feature-backup-20260218 branch before that branch is deleted. Refs #63 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… docs Delivers the technical track of the launch checklist in #63. Benchmarks (benchmarks/TechnicalAnalysis.Benchmarks) 119 BenchmarkDotNet methods over series of 1k/10k/100k bars, every indicator measured twice: once through TAFunc with caller-supplied buffers (algorithm cost) and once through TAMath (algorithm plus allocation). Candlestick patterns are exercised over double, float and decimal to price the generic-math design. The managed-versus-C comparison is opt-in: a DllImportResolver probes 19 candidate names and paths for the native TA-Lib, and when it finds nothing the suite prints a banner and removes NativeComparisonBenchmarks from the runnable set, so the whole thing works offline with no native dependency. Where the comparison does run, both implementations are asserted equivalent in [GlobalSetup] before anything is timed, so a faster result can never come from computing the wrong thing. Real-time sample (samples/TechnicalAnalysis.Samples.RealTime[.Client]) Deterministic tick feed, bar aggregator, and a rolling indicator engine behind both a SignalR hub and a raw WebSocket endpoint, plus a dependency-free browser dashboard and a console SignalR client. Backtesting sample (samples/TechnicalAnalysis.Samples.Backtesting) Bar-driven engine with commission and slippage, five strategies and a buy-and-hold baseline, and a metrics suite covering CAGR, drawdown, Sharpe, Sortino, Calmar, profit factor and expectancy. 220 unit tests. Both samples route every TA-Lib result through a single alignment-owning type, because output element k describes bar BegIdx + k and treating the two index spaces as interchangeable silently shifts every signal in time. The backtester goes further and clamps that metadata to the visible window, so a strategy cannot read a future bar even through BegIdx and NBElement. Docs Complete indicator catalogue generated from source by tools/generate-indicator-catalog.py (98 function entry points, 61 candlestick patterns), plus getting-started, TradingView integration, benchmarks, real-time and backtesting guides. 0 errors, 1118 tests passing, and 129 warnings against a 132 baseline. Refs #63 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects, all the same species: a faithful-looking port of the TA-Lib C source that dropped exactly one line. Atr never normalised its accumulator. The main output loop divided on the way out but carried the undivided value into the next bar, so the running average was multiplied by (period - 1) every bar. On a series whose true range is a constant 4.0 it returned 4.29, 4.41, 57.6, 749, 9737 - a factor of 13 per bar for period 14 - and reached +Infinity within a few hundred bars. The warm-up loop directly above it and the identical loop in Natr both normalise; only this one did not. TA_INT_EMA seeded itself low. `while (i-- > 0)` upstream became `i--; if (i <= 0) break;`, which accumulates period - 1 values while still dividing by period. EMA(20) over a constant series of 100 returned 95.476190 instead of 100. The seed is shared, so this reached Ema, Macd, MacdExt, MacdFix, Dema, Tema, T3, Apo, Ppo and Trix - and because the fast and slow legs were mis-seeded by different amounts, MACD read 0.53 on a series that never moved. Rsi divided by zero on a flat window. A window with no price change has an average gain and an average loss of exactly zero, so the ratio was 0/0 and every element came back NaN next to RetCode.Success. Upstream guards the same division and yields zero. All three division sites are now routed through one guarded helper, including the Metastock branch. The existing tests could not have caught any of this: they assert only that RetCode is Success. Each fix therefore comes with value-asserting tests verified to fail against the previous implementation - six of the seven new tests fail without these changes. The seventh, RSI over a rising series, is a control: it pins the legitimate zero-loss case at 100 so the new guard cannot quietly swallow it. Refs #63 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s-docs' into feat/fluent-high-level-api
The workflow that produced this branch could not run dotnet - the SDK host was
failing code-signature validation - so nothing here had ever been built or
tested. Two things were wrong.
The branch did not compile. The new public TechnicalAnalysis.Functions.
IndicatorSeries and MacdSeries collide by name with the backtesting sample's
own Engine.IndicatorSeries and MacdSeries, and both namespaces are in the
sample's global usings, giving five CS0104 errors. The sample keeps its own
types - they are bar-window aware and clamp their metadata so a strategy cannot
read a future bar - so they are pinned with global aliases in the sample and its
test project.
MacdLineDecaysAnalyticallyOnceTheRampFlattens asserted a sequence shifted one
bar late. closes[bar] = 1 + bar below 60, so closes[59] is already 60.0 and the
last RISING bar is 59, not 60; bar 60 is the first flat bar and has therefore
already taken one decay step. The analytic anchor is bar 59:
line(59 + m) = 12.5 * (25/27)^m - 5.5 * (11/13)^m
which reproduces all five of the test's own expected values exactly, each one
bar earlier than asserted. The implementation was right and the expectation was
wrong, so the bar indices moved rather than the code.
Verified: 0 errors, 1226 tests pass, and the historical regression finally
executes - Sma30OverClosesOneToOneHundredHasLatest85Point5AndNotZero asserts
Latest == 85.5 and != 0.0 and passes.
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the abandoned
auto-claude/001fluent API, which returned 0.0 from every value accessor because it used a bar index to subscript an array.Draft, deliberately. It builds and all tests pass, but it carries a change I don't think should be merged on my judgment alone — see "The decision you need to make" below.
The design
Three independent proposals were judged and synthesised into one spec. The winning idea, from the composition-first proposal:
IndicatorSeriesowns alignment in exactly one place.ToArrayIndex(bar) => bar - _firstBar, written once. Every facade is built on it. The only array-indexed member isWarmValues, named and documented as such.Latestreturnsdouble?, notdouble. A padding zero isn't addressable by any member returning a baredouble, and(double?)null > 70isfalseand(double?)null < 30isfalse— a not-warm indicator asserts nothing in either direction.AsOf(bar)makes look-ahead unrepresentable rather than merely detected — the future isn't part of the value handed over. Verified:prices.AsOf(50).Sma(30).Latest == prices.Sma(30).AsOf(50).Latestexactly, for all seven indicators.IsOverbought. 70/30 is Wilder's 1978 convention for daily RSI(14) and noise on 5-minute crypto — that threshold belongs in the caller's source under the caller's review.Verification
The workflow that produced this branch could not run
dotnet(SDK host failing code-signature validation), so nothing here had ever been built. I ran it:The historical regression now executes for the first time and passes:
101 test methods, and unlike the abandoned branch they assert computed values.
Two things I fixed to get here
It didn't compile. The new public
TechnicalAnalysis.Functions.IndicatorSeries/MacdSeriescollide with the backtesting sample'sEngine.IndicatorSeries/MacdSeriesfrom #104 — 5 × CS0104. Pinned with global aliases. That name collision is itself worth a design conversation:IndicatorSeriesis a very general name to claim inTechnicalAnalysis.Functions.One test was wrong, not the code.
MacdLineDecaysAnalyticallyOnceTheRampFlattensasserted a sequence shifted one bar late. Sincecloses[bar] = 1 + barbelow 60,closes[59]is already 60.0 — the last rising bar is 59, so bar 60 has already taken one decay step. Anchoring at 59,line(59+m) = 12.5·(25/27)^m − 5.5·(11/13)^mreproduces all five expected values exactly, each one bar earlier. Moved the indices, not the code.This branch is not additive. It modifies four shipping indicators:
These are real off-by-one fixes — the same bug class as the EMA seed in #105. The C reference
while (i-- > 0)runs the bodyitimes; the C# transcriptionwhile (true) { i--; if (i <= 0) break; }runs iti-1. The undercount leftBegIdx + NBElementoverrunning the input, which the new strictIndicatorSeries.Createguard surfaced. So the fluent API exposed a pre-existing defect, and the author fixed the indicator rather than weakening the guard. That's the right instinct.But it changes the numeric output of Adx, Dx, PlusDI, MinusDI and (transitively) Adxr in a published package, and:
AdxTests/DxTests/MinusDITests/PlusDITests/AdxrTestsassert onlyRetCode.ShouldBe(Success)— the exact weak standard this pass existed to end. They cannot detect a regression here.DirectionalMovementTestsassert the alignment invariant (BegIdx + NBElement == BarCount) but not the indicator values.So the fix is plausibly correct and currently unproven numerically.
My recommendation: split those four files into their own bug-fix PR with value-asserting tests (the way #105 was done — every test verified to fail against the old code), then rebase this on top. The two are coupled — the fluent API's guard throws without the fix — so it'd be a stack, not independent PRs.
I've left this as a draft rather than deciding for you.
🤖 Generated with Claude Code