Skip to content

fix: correct the directional movement seeding off-by-one - #111

Open
phmatray wants to merge 1 commit into
devfrom
fix/directional-movement-offbyone
Open

fix: correct the directional movement seeding off-by-one#111
phmatray wants to merge 1 commit into
devfrom
fix/directional-movement-offbyone

Conversation

@phmatray

Copy link
Copy Markdown
Owner

Split out of #110 so a semantic change to shipping indicators gets reviewed on its own, with tests, rather than riding inside a fluent-API PR.

Adx, Dx, PlusDI and MinusDI seeded one term short — the same shape as the EMA seed corrected in #105. The C reference uses while (i-- > 0), which runs its body i times; the C# transcription was while (true) { i--; if (i <= 0) break; }, which runs it i - 1 times.

Two measured symptoms

Both on a 100-bar linear uptrend (high = 100 + 2i, low = 99 + 2i, close = 99.5 + 2i):

1. The output claimed bars that don't exist.

BegIdx NBElement BegIdx+NBElement bars
Adx 27 75 → 73 102 → 100 100
Dx 14 88 → 86 102 → 100 100
PlusDI 14 88 → 86 102 → 100 100
MinusDI 14 88 → 86 102 → 100 100

Two values claimed for bars 100 and 101. Any consumer mapping an output element onto a bar — which is the documented contract, and what the samples in #104 do — was misaligned by two, silently.

2. The ADX seed averaged 13 values while dividing by 14.

On this series DX is a constant 100, and Wilder's average of a constant is that constant, so ADX must be exactly 100 at every bar. It read 92.857 — which is 100 × 13/14 — then crept toward 100 without arriving:

before:  first=92.8571  last=99.9703
after:   first=100      last=100

Adxr is affected transitively. +DI, -DI and DX values were already correct and are unchanged — only their alignment metadata was wrong.

Tests

The existing AdxTests, DxTests, PlusDITests, MinusDITests assert only RetCode.Success, so none of this was detectable by the suite. The four fixtures here assert numbers, and I verified how each behaves against the old code:

Fixture vs. old code
OutputNeverClaimsMoreBarsThanTheInputHas fails (102 ≠ 100)
AdxOnAConstantTrendIsExactlyOneHundredFromItsFirstValue fails (92.857 ≠ 100)
DirectionalIndicatorsSeparateAPureUptrend passes — control
DirectionalIndicatorsSeparateAPureDowntrend passes — control

The two controls are deliberate: they pin +DI = 80, -DI = 0, DX = 100 (all exact and hand-derivable on a linear trend) to prove the fix leaves the already-correct values alone rather than trading one error for another.

without the fix:  Failed: 2, Passed: 2
with the fix:     Failed: 0, Passed: 4

Verification

dotnet build TaLibStandard.sln -c Release  ->  0 errors
dotnet test  TaLibStandard.sln -c Release  ->  1129 passed, 0 failed
  Candles 673 · Functions 236 · Backtesting 220

Release note

This changes the numeric output of Adx, Adxr, Dx, PlusDI and MinusDI in a published package. Callers who compensated for the two-bar misalignment will need to stop. Worth calling out in the v4 notes alongside the ATR/EMA/RSI fixes from #105.

Once this merges, #110 (fluent API) should be rebased on top — it currently carries these same four files, and its strict alignment guard is what surfaced the defect in the first place.

🤖 Generated with Claude Code

Adx, Dx, PlusDI and MinusDI seeded one term short, in the same shape as the EMA
seed corrected in #105. The C reference uses `while (i-- > 0)`, which runs its
body i times; the C# transcription was `while (true) { i--; if (i <= 0) break; }`,
which runs it i - 1 times.

It showed up two ways, both measured on a 100-bar linear uptrend.

The undercount left the cursor short of startIdx, so BegIdx + NBElement came back
as 102 over 100 bars - two values claimed for bars that do not exist. Every
consumer that maps an output element onto a bar was therefore misaligned by two,
silently. All four functions were affected.

The ADX seed averaged 13 values while dividing by 14. On a series whose DX is a
constant 100 - a perfectly linear trend - ADX must be exactly 100 at every bar,
because Wilder's average of a constant is that constant. It read 92.857, which is
100 * 13 / 14, and crept towards 100 without arriving.

Adxr is affected transitively; +DI, -DI and DX values were already correct and are
unchanged, only their alignment metadata was wrong.

The existing AdxTests, DxTests, PlusDITests and MinusDITests assert only
RetCode.Success, so none of this was detectable. The four fixtures added here
assert numbers: two of them fail against the previous implementation - the
alignment invariant and the ADX seed - and two pass either way, held as controls
proving the fix leaves the already-correct +DI/-DI/DX values alone.

Verified: 0 errors, 1129 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant