53566b9d82
Move several hot Python analysis paths to Rust-backed helpers. This adds Rust implementations for backtest strategy signal generation and the core portfolio loop, options and futures payoff aggregation, Greeks aggregation, ratio calculation, trade extraction, chunked close-only indicator runs, and forward-fill helpers. Wire the Python analysis and data modules to prefer these paths, and add coverage for the new batch fast path. Expand the WASM package to export WMA, ADX, and MFI from ferro_ta_core, refresh the Node examples, benchmarks, and README, and add a Node-vs-Python conformance test so the browser and node surface stays aligned with the main Python package. Introduce a generated cross-surface API manifest in docs/, along with scripts to rebuild and verify it from source exports. Enforce manifest freshness in the Python and WASM CI workflows so release candidates catch surface drift before push.
238 lines
5.9 KiB
Python
238 lines
5.9 KiB
Python
"""
|
|
ferro_ta.cross_asset — Cross-asset and relative strength analytics.
|
|
|
|
Provides helpers for relative value and pair-trading workflows:
|
|
- relative_strength(asset_returns, benchmark_returns)
|
|
- spread(a, b, hedge=1.0)
|
|
- ratio(a, b)
|
|
- zscore(x, window)
|
|
- rolling_beta(a, b, window)
|
|
|
|
Compute-intensive work delegates to Rust (via ferro_ta._ferro_ta).
|
|
|
|
Functions
|
|
---------
|
|
relative_strength(asset_returns, benchmark_returns)
|
|
Cumulative-return ratio (asset / benchmark), starting at 1.
|
|
|
|
spread(a, b, hedge=1.0)
|
|
Spread series: a - hedge * b.
|
|
|
|
ratio(a, b)
|
|
Ratio series: a / b.
|
|
|
|
zscore(x, window)
|
|
Rolling Z-score of series *x* over a sliding window.
|
|
|
|
rolling_beta(a, b, window)
|
|
Rolling beta (hedge ratio) of series *a* vs *b*.
|
|
|
|
Rust backend
|
|
------------
|
|
ferro_ta._ferro_ta.relative_strength
|
|
ferro_ta._ferro_ta.spread
|
|
ferro_ta._ferro_ta.zscore_series
|
|
ferro_ta._ferro_ta.rolling_beta
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
from numpy.typing import ArrayLike, NDArray
|
|
|
|
from ferro_ta._ferro_ta import ratio as _rust_ratio
|
|
from ferro_ta._ferro_ta import relative_strength as _rust_rel_strength
|
|
from ferro_ta._ferro_ta import rolling_beta as _rust_rolling_beta
|
|
from ferro_ta._ferro_ta import spread as _rust_spread
|
|
from ferro_ta._ferro_ta import zscore_series as _rust_zscore
|
|
from ferro_ta._utils import _to_f64
|
|
|
|
__all__ = [
|
|
"relative_strength",
|
|
"spread",
|
|
"ratio",
|
|
"zscore",
|
|
"rolling_beta",
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# relative_strength
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def relative_strength(
|
|
asset_returns: ArrayLike,
|
|
benchmark_returns: ArrayLike,
|
|
) -> NDArray[np.float64]:
|
|
"""Compute relative strength of an asset versus a benchmark.
|
|
|
|
Returns the ratio of cumulative returns::
|
|
|
|
RS[i] = (1 + r_asset[0]) * … * (1 + r_asset[i]) /
|
|
((1 + r_bench[0]) * … * (1 + r_bench[i]))
|
|
|
|
starting from RS[0] ≈ 1.
|
|
|
|
Parameters
|
|
----------
|
|
asset_returns, benchmark_returns : array-like
|
|
Fractional returns per bar (e.g. 0.01 for +1%). Equal length.
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray of same length — relative strength series.
|
|
|
|
Examples
|
|
--------
|
|
>>> import numpy as np
|
|
>>> from ferro_ta.analysis.cross_asset import relative_strength
|
|
>>> r_a = np.array([0.01, 0.02, -0.01, 0.005])
|
|
>>> r_b = np.array([0.005, 0.01, -0.005, 0.002])
|
|
>>> rs = relative_strength(r_a, r_b)
|
|
>>> rs[0] > 1 # asset outperformed at bar 0
|
|
True
|
|
"""
|
|
a = _to_f64(asset_returns)
|
|
b = _to_f64(benchmark_returns)
|
|
return _rust_rel_strength(a, b)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# spread
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def spread(
|
|
a: ArrayLike,
|
|
b: ArrayLike,
|
|
hedge: float = 1.0,
|
|
) -> NDArray[np.float64]:
|
|
"""Compute the spread between two series.
|
|
|
|
``spread[i] = a[i] - hedge * b[i]``
|
|
|
|
Parameters
|
|
----------
|
|
a, b : array-like (equal length)
|
|
hedge : float — hedge ratio (default 1.0)
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray
|
|
|
|
Examples
|
|
--------
|
|
>>> import numpy as np
|
|
>>> from ferro_ta.analysis.cross_asset import spread
|
|
>>> a = np.array([10.0, 11.0, 12.0])
|
|
>>> b = np.array([9.0, 10.0, 11.0])
|
|
>>> list(spread(a, b))
|
|
[1.0, 1.0, 1.0]
|
|
"""
|
|
return _rust_spread(_to_f64(a), _to_f64(b), float(hedge))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ratio
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def ratio(
|
|
a: ArrayLike,
|
|
b: ArrayLike,
|
|
) -> NDArray[np.float64]:
|
|
"""Compute the ratio of two series: a / b.
|
|
|
|
Zeros in *b* produce ``NaN`` in the result.
|
|
|
|
Parameters
|
|
----------
|
|
a, b : array-like (equal length)
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray
|
|
|
|
Examples
|
|
--------
|
|
>>> import numpy as np
|
|
>>> from ferro_ta.analysis.cross_asset import ratio
|
|
>>> a = np.array([10.0, 12.0, 15.0])
|
|
>>> b = np.array([5.0, 4.0, 5.0])
|
|
>>> list(ratio(a, b))
|
|
[2.0, 3.0, 3.0]
|
|
"""
|
|
return _rust_ratio(_to_f64(a), _to_f64(b))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# zscore
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def zscore(
|
|
x: ArrayLike,
|
|
window: int,
|
|
) -> NDArray[np.float64]:
|
|
"""Compute the rolling Z-score of series *x*.
|
|
|
|
``z[i] = (x[i] - mean(x[i-window+1..i])) / std(x[i-window+1..i])``
|
|
|
|
Parameters
|
|
----------
|
|
x : array-like
|
|
window : int — must be >= 2
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray — NaN for first ``window-1`` positions.
|
|
|
|
Examples
|
|
--------
|
|
>>> import numpy as np
|
|
>>> from ferro_ta.analysis.cross_asset import zscore
|
|
>>> x = np.array([1.0, 2.0, 3.0, 2.0, 1.0])
|
|
>>> z = zscore(x, window=3)
|
|
>>> np.isnan(z[0]) and np.isnan(z[1])
|
|
True
|
|
"""
|
|
return _rust_zscore(_to_f64(x), int(window))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# rolling_beta
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def rolling_beta(
|
|
a: ArrayLike,
|
|
b: ArrayLike,
|
|
window: int,
|
|
) -> NDArray[np.float64]:
|
|
"""Compute rolling beta (hedge ratio) of series *a* vs *b*.
|
|
|
|
Parameters
|
|
----------
|
|
a, b : array-like (equal length)
|
|
window : int — rolling window size (must be >= 2)
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray — NaN for first ``window-1`` positions.
|
|
|
|
Examples
|
|
--------
|
|
>>> import numpy as np
|
|
>>> from ferro_ta.analysis.cross_asset import rolling_beta
|
|
>>> rng = np.random.default_rng(42)
|
|
>>> b = rng.normal(0, 1, 50)
|
|
>>> a = 0.8 * b + rng.normal(0, 0.1, 50)
|
|
>>> rb = rolling_beta(a, b, window=20)
|
|
>>> np.isnan(rb[18])
|
|
True
|
|
>>> abs(rb[-1] - 0.8) < 0.3
|
|
True
|
|
"""
|
|
return _rust_rolling_beta(_to_f64(a), _to_f64(b), int(window))
|