Fix 3 engine bugs: swing-point NaN false positives, ROC div-by-zero, ADX epsilon

Ported from feature/eurusd-support (e6e09c5), pair-neutral fixes:
- _find_swing_points: all(<empty generator>) is vacuously True, so a value
  surrounded by NaNs was reported as a swing point; now requires at least
  one valid neighbor on each side
- ROC: guard against zero close at the lookback positions (inf scores)
- ADX: epsilon denominator instead of NaN-replace to avoid NaN cascade

Adds 8 regression tests (Module 9). 150/150 pass.
This commit is contained in:
Harwinder Sekhon
2026-07-10 20:48:56 -04:00
parent ea306df484
commit f3b90d3862
2 changed files with 92 additions and 7 deletions
+13 -7
View File
@@ -492,7 +492,7 @@ class TechnicalEngine:
plus_di = 100 * (plus_dm.ewm(alpha=1/c.adx_period, min_periods=c.adx_period, adjust=False).mean() / atr)
minus_di = 100 * (minus_dm.ewm(alpha=1/c.adx_period, min_periods=c.adx_period, adjust=False).mean() / atr)
dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)
dx = 100 * (plus_di - minus_di).abs() / np.maximum(plus_di + minus_di, 1e-10)
adx = dx.ewm(alpha=1/c.adx_period, min_periods=c.adx_period, adjust=False).mean()
adx_val = adx.iloc[-1] if not adx.empty and not pd.isna(adx.iloc[-1]) else 0.0
@@ -602,8 +602,12 @@ class TechnicalEngine:
if len(close) < n + 5:
return
roc_val = ((close.iloc[-1] - close.iloc[-n - 1]) / close.iloc[-n - 1]) * 100
roc_prev = ((close.iloc[-2] - close.iloc[-n - 2]) / close.iloc[-n - 2]) * 100
denom1 = close.iloc[-n - 1]
denom2 = close.iloc[-n - 2]
if denom1 == 0 or denom2 == 0:
return
roc_val = ((close.iloc[-1] - denom1) / denom1) * 100
roc_prev = ((close.iloc[-2] - denom2) / denom2) * 100
if pd.isna(roc_val) or pd.isna(roc_prev):
return
@@ -1154,12 +1158,14 @@ class TechnicalEngine:
for i in range(window, len(vals) - window):
if np.isnan(vals[i]):
continue
left_vals = [vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])]
right_vals = [vals[i+j] for j in range(1, window+1) if i+j < len(vals) and not np.isnan(vals[i+j])]
if not left_vals or not right_vals:
continue
if mode == "low":
if all(vals[i] <= vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])) and \
all(vals[i] <= vals[i+j] for j in range(1, window+1) if i+j < len(vals) and not np.isnan(vals[i+j])):
if all(vals[i] <= v for v in left_vals) and all(vals[i] <= v for v in right_vals):
points.append((i, vals[i]))
else:
if all(vals[i] >= vals[i-j] for j in range(1, window+1) if not np.isnan(vals[i-j])) and \
all(vals[i] >= vals[i+j] for j in range(1, window+1) if i+j < len(vals) and not np.isnan(vals[i+j])):
if all(vals[i] >= v for v in left_vals) and all(vals[i] >= v for v in right_vals):
points.append((i, vals[i]))
return points