Files
ferro-ta/tests/integration/test_wasm_node_conformance.py
T
Pratik Bhadane 71b6343e92 feat: refresh benchmark coverage and harden CI tooling
Refresh the benchmark and performance surface across the repo. This updates the benchmark wrappers and helper scripts, regenerates the checked-in benchmark and perf-contract artifacts, and folds in the related roadmap, compatibility, and example notebook changes that belong with this performance-focused pass.

Harden the Python CI and local pre-push flow so the same checks pass reliably in both places. The workflow and pre-push script now use module-safe uv typecheck invocations, the Python test environment installs the optional MCP dependency needed by the MCP server tests, and one-off root benchmark outputs are ignored to keep the repo clean.

Align local tooling with the current project configuration by updating the Ruff pre-commit hook, tightening the API typing and MCP server helpers, and refreshing the lockfile to pick up the audited PyJWT fix while preserving the rest of the staged source changes.
2026-03-24 14:52:20 +05:30

185 lines
4.6 KiB
Python

from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import numpy as np
import pytest
import ferro_ta
ROOT = Path(__file__).resolve().parents[2]
WASM_DIR = ROOT / "wasm"
PKG_JS = WASM_DIR / "pkg" / "ferro_ta_wasm.js"
SCRIPT = WASM_DIR / "conformance_node.js"
def _write_node_conformance_script(path: Path) -> None:
path.write_text(
"""
const wasm = require("./pkg/ferro_ta_wasm.js");
function toArray(x) {
return Array.from(x, (v) => (Number.isNaN(v) ? null : Number(v)));
}
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.1, 45.42, 45.84, 46.08, 45.89, 46.03, 46.21, 46.02, 45.78]);
const high = new Float64Array([44.71, 44.5, 44.6, 44.09, 44.79, 45.2, 45.44, 45.73, 46.01, 46.44, 46.21, 46.39, 46.53, 46.3, 46.12]);
const low = new Float64Array([43.9, 43.8, 43.9, 43.2, 43.9, 44.2, 44.6, 44.8, 45.2, 45.5, 45.4, 45.5, 45.7, 45.6, 45.4]);
const volume = new Float64Array([1200, 1320, 1250, 1460, 1500, 1670, 1720, 1810, 1900, 2020, 1980, 2100, 2170, 2140, 2080]);
const payload = {
sma: toArray(wasm.sma(close, 5)),
ema: toArray(wasm.ema(close, 5)),
wma: toArray(wasm.wma(close, 5)),
rsi: toArray(wasm.rsi(close, 5)),
adx: toArray(wasm.adx(high, low, close, 5)),
mfi: toArray(wasm.mfi(high, low, close, volume, 5)),
};
process.stdout.write(JSON.stringify(payload));
""".strip()
+ "\n",
encoding="utf-8",
)
def _run_node_conformance() -> dict[str, list[float | None]]:
if shutil.which("node") is None:
pytest.skip("node is required for wasm/node conformance test")
if not PKG_JS.exists():
pytest.skip(
"wasm/pkg not found; run `wasm-pack build --target nodejs --out-dir pkg`"
)
_write_node_conformance_script(SCRIPT)
try:
out = subprocess.check_output(
["node", str(SCRIPT)],
cwd=WASM_DIR,
text=True,
)
finally:
if SCRIPT.exists():
SCRIPT.unlink()
return json.loads(out)
def _to_jsonable(arr: np.ndarray) -> list[float | None]:
vals = np.asarray(arr, dtype=np.float64)
return [None if np.isnan(x) else float(x) for x in vals]
def _assert_close_with_null_nan(
actual: list[float | None],
expected: list[float | None],
*,
atol: float,
) -> None:
assert len(actual) == len(expected)
a = np.array([np.nan if v is None else float(v) for v in actual], dtype=np.float64)
e = np.array(
[np.nan if v is None else float(v) for v in expected], dtype=np.float64
)
np.testing.assert_allclose(a, e, atol=atol, rtol=0.0, equal_nan=True)
def test_wasm_node_matches_python_core_indicators() -> None:
close = np.array(
[
44.34,
44.09,
44.15,
43.61,
44.33,
44.83,
45.10,
45.42,
45.84,
46.08,
45.89,
46.03,
46.21,
46.02,
45.78,
],
dtype=np.float64,
)
high = np.array(
[
44.71,
44.50,
44.60,
44.09,
44.79,
45.20,
45.44,
45.73,
46.01,
46.44,
46.21,
46.39,
46.53,
46.30,
46.12,
],
dtype=np.float64,
)
low = np.array(
[
43.90,
43.80,
43.90,
43.20,
43.90,
44.20,
44.60,
44.80,
45.20,
45.50,
45.40,
45.50,
45.70,
45.60,
45.40,
],
dtype=np.float64,
)
volume = np.array(
[
1200.0,
1320.0,
1250.0,
1460.0,
1500.0,
1670.0,
1720.0,
1810.0,
1900.0,
2020.0,
1980.0,
2100.0,
2170.0,
2140.0,
2080.0,
],
dtype=np.float64,
)
node_payload = _run_node_conformance()
py_expected = {
"sma": _to_jsonable(ferro_ta.SMA(close, 5)),
"ema": _to_jsonable(ferro_ta.EMA(close, 5)),
"wma": _to_jsonable(ferro_ta.WMA(close, 5)),
"rsi": _to_jsonable(ferro_ta.RSI(close, 5)),
"adx": _to_jsonable(ferro_ta.ADX(high, low, close, 5)),
"mfi": _to_jsonable(ferro_ta.MFI(high, low, close, volume, 5)),
}
for name, expected in py_expected.items():
assert name in node_payload
_assert_close_with_null_nan(node_payload[name], expected, atol=1e-9)