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
27 changes: 23 additions & 4 deletions src/ta_oscillators.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,13 @@ double CMO::compute(double src) {
for (auto v : up_buffer_) up_sum += v;
for (auto v : down_buffer_) down_sum += v;
double denom = up_sum + down_sum;
if (denom == 0) return 0.0;
// Degenerate window (no price movement at all): TradingView does NOT guard
// this arm — it evaluates the published formula, so 0/0 surfaces as na.
// Pinned by the 2026-07-25 clean-room oracle (CMO_NUM 0/21). The previous
// 0.0 was an invented constant, not a derivation. See the status block in
// tests/test_ta_osc_edge.cpp for the evidence and for why the sibling arms
// (stoch / cci / mfi) are deliberately NOT changed.
if (denom == 0) return na<double>();
return 100.0 * (up_sum - down_sum) / denom;
}

Expand Down Expand Up @@ -509,7 +515,12 @@ double TSI::compute(double src) {
prev_src_ = src;
double ds = ema_short_.compute(ema_long_.compute(pc));
double ads = ema_abs_short_.compute(ema_abs_long_.compute(std::abs(pc)));
if (ads == 0) return 0.0;
// Degenerate denominator: the double-smoothed EMA of |change| reaches
// exactly 0.0 only when the source is flat from bar 0, so the oracle's
// all-constant construction is not a special case — it is the ONLY
// reachable one for this arm. TradingView returns na there (TSI_NUM 0/21,
// 2026-07-25 clean-room oracle). The previous 0.0 was invented.
if (ads == 0) return na<double>();
// Pine v6 ta.tsi returns the True Strength Index normalised to [-1, 1]
// (per the official reference manual). Classical TSI literature uses
// [-100, 100] (i.e. ×100), but Pine deliberately drops the percentage
Expand Down Expand Up @@ -557,7 +568,12 @@ double COG::compute(double src) {
num += buffer_[n - 1 - i] * (i + 1);
den += buffer_[i];
}
if (den == 0) return 0.0;
// Degenerate denominator: the window sum is exactly 0. Structurally
// unreachable for positive prices, so the oracle's zero-sum construction
// is the only case this arm ever sees, and TradingView returns na there
// (COG_NUM 0/21, 2026-07-25 clean-room oracle). The previous 0.0 was
// invented — note it is not even the formula's limit, which diverges.
if (den == 0) return na<double>();
return -num / den;
}

Expand Down Expand Up @@ -781,7 +797,10 @@ double COG::recompute(double src) {
num += buffer_[n - 1 - i] * (i + 1);
den += buffer_[i];
}
if (den == 0) return 0.0;
// Mirror of compute()'s degenerate arm — see that function for the oracle
// citation. Kept in lock-step deliberately: this is an independent copy of
// the guard, not a delegation.
if (den == 0) return na<double>();
return -num / den;
}

Expand Down
24 changes: 18 additions & 6 deletions tests/test_ta_indicators_extras.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,14 @@ static void test_cmo() {
for (double p : {10.0, 11.0, 12.0, 13.0}) v = cmo.compute(p);
CHECK(near(v, 100.0));

// Constant series → both sums 0 → return 0.0
// Degenerate window: a constant series makes up_sum + down_sum bitwise 0.
// TradingView does not guard this arm — it evaluates the formula, so the
// result is na. Pinned by the 2026-07-25 clean-room oracle (CMO_NUM 0/21);
// this assertion previously required the engine's invented 0.0. See the
// status block in tests/test_ta_osc_edge.cpp for the full evidence trail.
ta::CMO flat(3);
for (int i = 0; i < 4; ++i) flat.compute(5.0);
CHECK(near(flat.compute(5.0), 0.0));
CHECK(is_na(flat.compute(5.0)));
Comment on lines 270 to +272

// recompute equals compute on same bar
CHECK(near(cmo.recompute(13.0), 100.0));
Expand All @@ -286,11 +290,15 @@ static void test_tsi() {
CHECK(v > 0.5); // monotone-up series should drive TSI well above zero
CHECK(v <= 1.0);

// Zero divisor branch: constant series → ds == 0, ads == 0 → returns 0
// Degenerate denominator: a constant series drives ads to bitwise 0, which
// is only reachable when the source is flat from bar 0. TradingView returns
// na there (TSI_NUM 0/21, 2026-07-25 clean-room oracle); this assertion
// previously required the engine's invented 0.0. See the status block in
// tests/test_ta_osc_edge.cpp for the full evidence trail.
ta::TSI flat(3, 5);
double last = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 25; ++i) last = flat.compute(7.0);
CHECK(near(last, 0.0));
CHECK(is_na(last));

// recompute returns same value as the most recent compute on same input
double r = tsi.recompute(125.0);
Expand Down Expand Up @@ -334,10 +342,14 @@ static void test_cog() {
// correctness sweep against TV.)
CHECK(near(v, -10.0 / 6.0));

// den == 0 branch
// Degenerate denominator: a zero window sum, structurally unreachable for
// positive prices, so this construction is the only case the arm ever sees.
// TradingView returns na there (COG_NUM 0/21, 2026-07-25 clean-room
// oracle); this assertion previously required the engine's invented 0.0.
// See the status block in tests/test_ta_osc_edge.cpp for the evidence.
ta::COG zero(3);
zero.compute(0); zero.compute(0);
CHECK(near(zero.compute(0), 0.0));
CHECK(is_na(zero.compute(0)));

// recompute consistency
CHECK(near(cog.recompute(3.0), -10.0 / 6.0));
Expand Down
204 changes: 202 additions & 2 deletions tests/test_ta_osc_edge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,70 @@
* ROC : na-input early return (227-229).
* CCI : na-input early return (308-309).
* RCI : na-input early return (346-348).
* CMO : degenerate up_sum+down_sum == 0 -> na.
* TSI : degenerate ads == 0 -> na.
* COG : degenerate window sum == 0 -> na (compute AND recompute).
*
* NDEBUG-PROOF: every assertion goes through CHECK(), which increments a
* file-scope failure counter and is reported by main()'s nonzero return.
* It does NOT use bare assert(), so it fires identically under -DNDEBUG.
* Expected numeric values are Pine-correct, derived by hand from the
* documented behaviour of ta_oscillators.cpp and pinned here.
*
* ---------------------------------------------------------------------------
* STATUS OF THE DEGENERATE-DENOMINATOR CONSTANTS (READ BEFORE "FINISHING THE
* JOB" ON THE REMAINING ARMS)
*
* An earlier revision of this header claimed the expected numeric values were
* "Pine-correct, derived by hand from the documented behaviour". For the
* degenerate (zero-denominator) arms of the oscillators that claim is FALSE
* and has been retracted: those constants were never derived, they were the
* original author's convenience values (see the in-code comment on the Stoch
* arm, "Avoid division by zero; midpoint when flat"). The Pine v6 reference
* publishes the formulas but is silent at the singularity.
*
* A clean-room TradingView oracle was exported and independently adjudicated
* on 2026-07-25 to settle them. Evidence (private campaign repo):
* data/probes/pf-probe-degenerate-oscillator-denominator/ (tape + source)
* .codex-evidence/degenerate-oscillator-oracle-20260725/ (frozen copy)
* data/progress/degenerate-arm-reachability-20260725.md (reachability)
* scratchpad/degenerate-oracle-adjudication-20260725.md (adjudication)
*
* What that oracle actually established, arm by arm:
*
* ta.tsi -> na ESTABLISHED, FIXED HERE. The all-constant construction is
* not a special case for this arm, it is the only reachable
* one (the denominator is an EMA of |change|, which reaches
* exactly 0 only when the source is flat from bar 0).
* ta.cog -> na ESTABLISHED, FIXED HERE. Same argument: a zero window sum
* is structurally unreachable for positive prices, so the
* zero-sum construction is the case.
* ta.cmo -> na ESTABLISHED, FIXED HERE. TradingView does not guard this
* arm at all; it evaluates the formula and yields na.
*
* ta.stoch = 50.0 KNOWN-WRONG, CORRECT VALUE UNRESOLVED — DO NOT "FIX".
* The oracle refutes 50.0 in both probe families, but it
* refutes na as well: on a flat window of a *varying*
* source TradingView returned a number that is neither na
* nor 50. The only mechanism consistent with both families
* is carry-forward, which is INFERRED, not observed.
* Shipping an inferred mechanism is the failure mode that
* got a previous fix reverted. The 50.0 assertion below is
* therefore retained deliberately: it pins current
* behaviour, it does NOT assert correctness.
* ta.cci = 0.0 SPLIT / UNDECIDED — DO NOT "FIX". Exactly 0.0 on an
* all-constant source (21/21) but measurably not 0 on a
* flat window of a varying source (0/17). The two surviving
* hypotheses demand OPPOSITE fixes, so na would be wrong
* under both.
* ta.mfi = 100.0 UNVERIFIED — DO NOT "FIX". Agrees with TradingView in the
* all-constant construction, but is untested at a real flat
* window; ta.cci is the standing proof that this exact
* construction can mislead.
* ta.wpr -> na already na; no pending change.
*
* test_degenerate_arms_negative_control() below pins stoch/cci/mfi at their
* current constants ON PURPOSE, so a well-meaning edit that extends the na fix
* to them fails loudly instead of silently shipping an unlicensed change.
* ---------------------------------------------------------------------------
*/

#include <cmath>
Expand Down Expand Up @@ -237,13 +295,155 @@ static void test_cci_rci_na() {
CHECK(is_na(rci.compute(na<double>())));
}

// ----------------------------------------------------------------------------
// DEGENERATE-DENOMINATOR ARMS THAT THE 2026-07-25 TRADINGVIEW ORACLE SETTLED.
//
// TradingView returns `na` for all three; the engine used to return an invented
// constant (0.0). See the status block at the top of this file for the evidence
// paths and for why the *other* three arms are deliberately not touched.
//
// Each arm is asserted on compute() AND on recompute(). CMO::recompute and
// TSI::recompute restore their saved state and delegate to compute(), so the
// recompute assertion pins the delegation; COG::recompute carries its own
// independent copy of the guard, so its assertion pins a genuinely separate
// code path.
// ----------------------------------------------------------------------------
static void test_cmo_degenerate_is_na() {
std::printf("test_cmo_degenerate_is_na\n");

// A source that never changes makes every up/down move exactly 0, so
// up_sum + down_sum is bitwise 0.0 and the degenerate arm fires.
// CMO(9) needs length+1 = 10 bars before it emits anything at all.
ta::CMO cmo(9);
double v = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 9; ++i) {
CHECK(is_na(cmo.compute(42.0))); // warmup: bar_count < length+1
}
v = cmo.compute(42.0); // 10th bar: window full, denom == 0
CHECK(is_na(v));
// recompute() of the same degenerate bar must agree.
CHECK(is_na(cmo.recompute(42.0)));

// Non-vacuous control: a genuinely one-sided window is NOT na and is NOT
// the degenerate constant. A strictly rising source gives down_sum == 0,
// so CMO == +100.
ta::CMO rising(9);
double r = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 10; ++i) r = rising.compute(100.0 + i);
CHECK(!is_na(r));
CHECK(near(r, 100.0));
}

static void test_tsi_degenerate_is_na() {
std::printf("test_tsi_degenerate_is_na\n");

// ta.tsi's denominator is an EMA of |change|. It reaches exactly 0.0 only
// when the source is flat from bar 0 — which is precisely why the oracle's
// all-constant construction is the ONLY reachable case for this arm, not a
// special case. Feed well past both EMA warmups (long 25, then short 13).
ta::TSI tsi(13, 25);
double v = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 60; ++i) v = tsi.compute(1234.5);
CHECK(is_na(v));
CHECK(is_na(tsi.recompute(1234.5)));

// Non-vacuous control: a varying source produces a finite, non-degenerate
// TSI, so the assertion above is testing the guard and not warmup.
ta::TSI live(13, 25);
double w = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 60; ++i) w = live.compute(1000.0 + i * 3.0);
CHECK(!is_na(w));
}

static void test_cog_degenerate_is_na() {
std::printf("test_cog_degenerate_is_na\n");

// COG's denominator is the plain window sum. For positive prices it can
// never be 0, so the zero-sum window is the only case this arm ever sees.
// Signed window summing to bitwise 0.0 while the NUMERATOR stays non-zero,
// so this pins the guard specifically rather than a 0/0 coincidence:
// 1 + (-1) + 2 + (-2) == 0.0 exactly in IEEE-754.
ta::COG cog(4);
CHECK(is_na(cog.compute(1.0))); // warmup, buffer < length
CHECK(is_na(cog.compute(-1.0)));
CHECK(is_na(cog.compute(2.0)));
CHECK(is_na(cog.compute(-2.0))); // window full -> den == 0
// COG::recompute carries its own copy of the guard (separate code path).
CHECK(is_na(cog.recompute(-2.0)));

// All-zero window (the oracle's literal construction) also degenerates.
ta::COG zeros(10);
double z = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 10; ++i) z = zeros.compute(0.0);
CHECK(is_na(z));
CHECK(is_na(zeros.recompute(0.0)));

// Non-vacuous control: an ordinary positive window is finite, so the
// assertions above are testing the guard and not the warmup path.
// buffer [10,20,30]; Pine weights the NEWEST bar by 1 and the OLDEST by
// length -> num = 30*1 + 20*2 + 10*3 = 100, den = 60 -> -100/60.
ta::COG live(3);
live.compute(10.0);
live.compute(20.0);
double c = live.compute(30.0);
CHECK(!is_na(c));
CHECK(near(c, -100.0 / 60.0));
CHECK(near(live.recompute(30.0), -100.0 / 60.0));
}

// ----------------------------------------------------------------------------
// NEGATIVE CONTROL — the three arms the oracle did NOT license a change for.
//
// These assertions pin CURRENT ENGINE BEHAVIOUR, not correctness. stoch's 50.0
// is known-wrong with an unresolved correct value; cci's 0.0 is split between
// two hypotheses that demand opposite fixes; mfi's 100.0 is unverified at a
// real flat window. See the status block at the top of this file.
//
// If you are here because you extended the tsi/cog/cmo -> na fix to these arms
// and this test now fails: that is the test doing its job. Do not "fix" the
// test. Get an oracle first.
// ----------------------------------------------------------------------------
static void test_degenerate_arms_negative_control() {
std::printf("test_degenerate_arms_negative_control\n");

// ta.stoch on a flat window -> 50.0 (NOT na). Known-wrong, unresolved.
ta::Stoch stoch(3);
stoch.compute(5.0, 5.0, 5.0);
stoch.compute(5.0, 5.0, 5.0);
CHECK(near(stoch.compute(5.0, 5.0, 5.0), 50.0));
CHECK(near(stoch.recompute(5.0, 5.0, 5.0), 50.0));

// ta.cci on a constant source -> exactly 0.0 (NOT na). SPLIT/undecided:
// TradingView really does return bitwise 0.0 in this construction.
ta::CCI cci(3);
cci.compute(7.0);
cci.compute(7.0);
double c = cci.compute(7.0);
CHECK(!is_na(c));
CHECK(near(c, 0.0));
CHECK(near(cci.recompute(7.0), 0.0));

// ta.mfi with no negative money flow -> exactly 100.0 (NOT na).
// UNVERIFIED at a real flat window. MFI(5) needs length+1 = 6 bars.
ta::MFI mfi(5);
double m = std::numeric_limits<double>::quiet_NaN();
for (int i = 0; i < 6; ++i) m = mfi.compute(9.0, 100.0);
CHECK(!is_na(m));
CHECK(near(m, 100.0));
CHECK(near(mfi.recompute(9.0, 100.0), 100.0));
}

int main() {
test_rsi_na_input();
test_stoch_flat_and_na();
test_change_window_eviction();
test_cross_na_and_false_branch();
test_mom_roc_na();
test_cci_rci_na();
test_cmo_degenerate_is_na();
test_tsi_degenerate_is_na();
test_cog_degenerate_is_na();
test_degenerate_arms_negative_control();

if (g_fail) {
std::fprintf(stderr, "\nta_osc_edge: %d CHECK(s) FAILED\n", g_fail);
Expand Down
Loading