diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 8a8c2b0..9b41c94 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -73,6 +73,10 @@ jobs: - name: Run unit tests with coverage run: pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=xml --cov-report=term-missing --cov-fail-under=65 + - name: Check API manifest is current + if: matrix.python-version == '3.12' + run: python scripts/check_api_manifest.py + - name: Upload coverage report uses: actions/upload-artifact@v7 if: matrix.python-version == '3.12' diff --git a/.github/workflows/ci-wasm.yml b/.github/workflows/ci-wasm.yml index d1636ef..8a26669 100644 --- a/.github/workflows/ci-wasm.yml +++ b/.github/workflows/ci-wasm.yml @@ -35,6 +35,9 @@ jobs: working-directory: wasm run: wasm-pack build --target nodejs --out-dir pkg + - name: Check API manifest is current + run: python3 scripts/check_api_manifest.py + - name: Benchmark WASM package working-directory: wasm run: node bench.js --json ../wasm_benchmark.json diff --git a/docs/api_manifest.json b/docs/api_manifest.json new file mode 100644 index 0000000..86c4b3a --- /dev/null +++ b/docs/api_manifest.json @@ -0,0 +1,5178 @@ +{ + "surfaces": { + "python": { + "indicator_count": 208, + "method_count": 447, + "categories": [ + "aggregation", + "alerts", + "batch", + "crypto", + "cycle", + "extended", + "features", + "math_ops", + "momentum", + "overlap", + "pattern", + "portfolio", + "price_transform", + "regime", + "resampling", + "signals", + "statistic", + "streaming", + "volatility", + "volume" + ], + "indicators": [ + { + "name": "ACOS", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "AD", + "category": "volume", + "module": "ferro_ta.indicators.volume", + "doc": "", + "params": [] + }, + { + "name": "ADD", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ADOSC", + "category": "volume", + "module": "ferro_ta.indicators.volume", + "doc": "", + "params": [] + }, + { + "name": "ADX", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ADXR", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "APO", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "AROON", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "AROONOSC", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ASIN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ATAN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ATR", + "category": "volatility", + "module": "ferro_ta.indicators.volatility", + "doc": "", + "params": [] + }, + { + "name": "AVGPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "AlertEvent", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "AlertManager", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "BBANDS", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "BETA", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "BOP", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "CCI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "CDL2CROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3BLACKCROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3INSIDE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3LINESTRIKE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3OUTSIDE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3STARSINSOUTH", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3WHITESOLDIERS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLABANDONEDBABY", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLADVANCEBLOCK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLBELTHOLD", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLBREAKAWAY", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLCLOSINGMARUBOZU", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLCONCEALBABYSWALL", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLCOUNTERATTACK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDARKCLOUDCOVER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDOJISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDRAGONFLYDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLENGULFING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLEVENINGDOJISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLEVENINGSTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLGAPSIDESIDEWHITE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLGRAVESTONEDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHAMMER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHANGINGMAN", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHARAMI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHARAMICROSS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHIGHWAVE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHIKKAKE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHIKKAKEMOD", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHOMINGPIGEON", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLIDENTICAL3CROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLINNECK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLINVERTEDHAMMER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLKICKING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLKICKINGBYLENGTH", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLLADDERBOTTOM", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLLONGLEGGEDDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLLONGLINE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMARUBOZU", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMATCHINGLOW", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMATHOLD", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMORNINGDOJISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMORNINGSTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLONNECK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLPIERCING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLRICKSHAWMAN", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLRISEFALL3METHODS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSEPARATINGLINES", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSHOOTINGSTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSHORTLINE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSPINNINGTOP", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSTALLEDPATTERN", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSTICKSANDWICH", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTAKURI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTASUKIGAP", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTHRUSTING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTRISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLUNIQUE3RIVER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLUPSIDEGAP2CROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLXSIDEGAP3METHODS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CEIL", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "CHANDELIER_EXIT", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "CHOPPINESS_INDEX", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "CMO", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "CORREL", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "COS", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "COSH", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "DEMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "DIV", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "DONCHIAN", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "DX", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "EMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "EXP", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "FLOOR", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "HT_DCPERIOD", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_DCPHASE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_PHASOR", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_SINE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_TRENDLINE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_TRENDMODE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HULL_MA", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "ICHIMOKU", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "KAMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "KELTNER_CHANNELS", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_ANGLE", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_INTERCEPT", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_SLOPE", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "LOG10", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MACD", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MACDEXT", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MACDFIX", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MAMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MAVP", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MAX", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MAXINDEX", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MEDPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "MFI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MIDPOINT", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MIDPRICE", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MIN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MININDEX", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MINUS_DI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MINUS_DM", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MOM", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MULT", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "NATR", + "category": "volatility", + "module": "ferro_ta.indicators.volatility", + "doc": "", + "params": [] + }, + { + "name": "OBV", + "category": "volume", + "module": "ferro_ta.indicators.volume", + "doc": "", + "params": [] + }, + { + "name": "PIVOT_POINTS", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "PLUS_DI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "PLUS_DM", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "PPO", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROC", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROCP", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROCR", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROCR100", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "RSI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "SAR", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "SAREXT", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "SIN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SINH", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "SQRT", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "STDDEV", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "STOCH", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "STOCHF", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "STOCHRSI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "SUB", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SUM", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SUPERTREND", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "StreamingATR", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingBBands", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingEMA", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingMACD", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingRSI", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingSMA", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingStoch", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingSupertrend", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingVWAP", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "T3", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "TAN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "TANH", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "TEMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "TRANGE", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "TRIMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "TRIX", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "TSF", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "TYPPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "TickAggregator", + "category": "aggregation", + "module": "ferro_ta.data.aggregation", + "doc": "", + "params": [] + }, + { + "name": "ULTOSC", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "VAR", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "VWAP", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "VWMA", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "WCLPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "WILLR", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "WMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "aggregate_ticks", + "category": "aggregation", + "module": "ferro_ta.data.aggregation", + "doc": "", + "params": [] + }, + { + "name": "batch_apply", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "batch_ema", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "batch_rsi", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "batch_sma", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "beta", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "check_cross", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "check_threshold", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "collect_alert_bars", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "compose", + "category": "signals", + "module": "ferro_ta.analysis.signals", + "doc": "", + "params": [] + }, + { + "name": "compute_many", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "continuous_bar_labels", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "correlation_matrix", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "detect_breaks_cusum", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "drawdown", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "feature_matrix", + "category": "features", + "module": "ferro_ta.analysis.features", + "doc": "", + "params": [] + }, + { + "name": "funding_pnl", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "multi_timeframe", + "category": "resampling", + "module": "ferro_ta.data.resampling", + "doc": "", + "params": [] + }, + { + "name": "portfolio_volatility", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "rank_signals", + "category": "signals", + "module": "ferro_ta.analysis.signals", + "doc": "", + "params": [] + }, + { + "name": "regime", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "regime_adx", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "regime_combined", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "resample", + "category": "resampling", + "module": "ferro_ta.data.resampling", + "doc": "", + "params": [] + }, + { + "name": "resample_continuous", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "rolling_variance_break", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "screen", + "category": "signals", + "module": "ferro_ta.analysis.signals", + "doc": "", + "params": [] + }, + { + "name": "session_boundaries", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "structural_breaks", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "volume_bars", + "category": "resampling", + "module": "ferro_ta.data.resampling", + "doc": "", + "params": [] + } + ], + "methods": [ + { + "name": "TickAggregator", + "category": "aggregation", + "module": "ferro_ta.data.aggregation", + "doc": "", + "params": [] + }, + { + "name": "aggregate_ticks", + "category": "aggregation", + "module": "ferro_ta.data.aggregation", + "doc": "", + "params": [] + }, + { + "name": "AlertEvent", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "AlertManager", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "check_cross", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "check_threshold", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "collect_alert_bars", + "category": "alerts", + "module": "ferro_ta.tools.alerts", + "doc": "", + "params": [] + }, + { + "name": "TradeStats", + "category": "attribution", + "module": "ferro_ta.analysis.attribution", + "doc": "", + "params": [] + }, + { + "name": "attribution_by_month", + "category": "attribution", + "module": "ferro_ta.analysis.attribution", + "doc": "", + "params": [] + }, + { + "name": "attribution_by_signal", + "category": "attribution", + "module": "ferro_ta.analysis.attribution", + "doc": "", + "params": [] + }, + { + "name": "from_backtest", + "category": "attribution", + "module": "ferro_ta.analysis.attribution", + "doc": "", + "params": [] + }, + { + "name": "trade_stats", + "category": "attribution", + "module": "ferro_ta.analysis.attribution", + "doc": "", + "params": [] + }, + { + "name": "batch_apply", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "batch_ema", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "batch_rsi", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "batch_sma", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "compute_many", + "category": "batch", + "module": "ferro_ta.data.batch", + "doc": "", + "params": [] + }, + { + "name": "ratio", + "category": "cross_asset", + "module": "ferro_ta.analysis.cross_asset", + "doc": "", + "params": [] + }, + { + "name": "relative_strength", + "category": "cross_asset", + "module": "ferro_ta.analysis.cross_asset", + "doc": "", + "params": [] + }, + { + "name": "rolling_beta", + "category": "cross_asset", + "module": "ferro_ta.analysis.cross_asset", + "doc": "", + "params": [] + }, + { + "name": "spread", + "category": "cross_asset", + "module": "ferro_ta.analysis.cross_asset", + "doc": "", + "params": [] + }, + { + "name": "zscore", + "category": "cross_asset", + "module": "ferro_ta.analysis.cross_asset", + "doc": "", + "params": [] + }, + { + "name": "continuous_bar_labels", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "funding_pnl", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "resample_continuous", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "session_boundaries", + "category": "crypto", + "module": "ferro_ta.analysis.crypto", + "doc": "", + "params": [] + }, + { + "name": "HT_DCPERIOD", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_DCPHASE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_PHASOR", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_SINE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_TRENDLINE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "HT_TRENDMODE", + "category": "cycle", + "module": "ferro_ta.indicators.cycle", + "doc": "", + "params": [] + }, + { + "name": "PayoffLeg", + "category": "derivatives_payoff", + "module": "ferro_ta.analysis.derivatives_payoff", + "doc": "", + "params": [] + }, + { + "name": "aggregate_greeks", + "category": "derivatives_payoff", + "module": "ferro_ta.analysis.derivatives_payoff", + "doc": "", + "params": [] + }, + { + "name": "futures_leg_payoff", + "category": "derivatives_payoff", + "module": "ferro_ta.analysis.derivatives_payoff", + "doc": "", + "params": [] + }, + { + "name": "option_leg_payoff", + "category": "derivatives_payoff", + "module": "ferro_ta.analysis.derivatives_payoff", + "doc": "", + "params": [] + }, + { + "name": "strategy_payoff", + "category": "derivatives_payoff", + "module": "ferro_ta.analysis.derivatives_payoff", + "doc": "", + "params": [] + }, + { + "name": "CHANDELIER_EXIT", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "CHOPPINESS_INDEX", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "DONCHIAN", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "HULL_MA", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "ICHIMOKU", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "KELTNER_CHANNELS", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "PIVOT_POINTS", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "SUPERTREND", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "VWAP", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "VWMA", + "category": "extended", + "module": "ferro_ta.indicators.extended", + "doc": "", + "params": [] + }, + { + "name": "feature_matrix", + "category": "features", + "module": "ferro_ta.analysis.features", + "doc": "", + "params": [] + }, + { + "name": "CurveSummary", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "annualized_basis", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "back_adjusted_continuous_contract", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "basis", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "calendar_spreads", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "carry_spread", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "curve_slope", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "curve_summary", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "implied_carry_rate", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "parity_gap", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "ratio_adjusted_continuous_contract", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "roll_yield", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "synthetic_forward", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "synthetic_spot", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "weighted_continuous_contract", + "category": "futures", + "module": "ferro_ta.analysis.futures", + "doc": "", + "params": [] + }, + { + "name": "ACOS", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ADD", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ASIN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ATAN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "CEIL", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "COS", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "COSH", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "DIV", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "EXP", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "FLOOR", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "LN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "LOG10", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MAX", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MAXINDEX", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MIN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MININDEX", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "MULT", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SIN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SINH", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SQRT", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SUB", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "SUM", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "TAN", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "TANH", + "category": "math_ops", + "module": "ferro_ta.indicators.math_ops", + "doc": "", + "params": [] + }, + { + "name": "ADX", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ADXR", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "APO", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "AROON", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "AROONOSC", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "BOP", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "CCI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "CMO", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "DX", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MFI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MINUS_DI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MINUS_DM", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "MOM", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "PLUS_DI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "PLUS_DM", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "PPO", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROC", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROCP", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROCR", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ROCR100", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "RSI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "STOCH", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "STOCHF", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "STOCHRSI", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "TRANGE", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "TRIX", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "ULTOSC", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "WILLR", + "category": "momentum", + "module": "ferro_ta.indicators.momentum", + "doc": "", + "params": [] + }, + { + "name": "OptionGreeks", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "SmileMetrics", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "black_76_price", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "black_scholes_price", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "greeks", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "implied_volatility", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "iv_percentile", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "iv_rank", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "iv_zscore", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "label_moneyness", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "option_price", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "select_strike", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "smile_metrics", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "term_structure_slope", + "category": "options", + "module": "ferro_ta.analysis.options", + "doc": "", + "params": [] + }, + { + "name": "DerivativesStrategy", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "ExpirySelector", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "ExpirySelectorKind", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "LegPreset", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "RiskControl", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "RiskMode", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "SimulationLimits", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "StrategyLeg", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "StrikeSelector", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "StrikeSelectorKind", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "build_strategy_preset", + "category": "options_strategy", + "module": "ferro_ta.analysis.options_strategy", + "doc": "", + "params": [] + }, + { + "name": "BBANDS", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "DEMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "EMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "KAMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MACD", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MACDEXT", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MACDFIX", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MAMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MAVP", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MIDPOINT", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "MIDPRICE", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "SAR", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "SAREXT", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "SMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "T3", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "TEMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "TRIMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "WMA", + "category": "overlap", + "module": "ferro_ta.indicators.overlap", + "doc": "", + "params": [] + }, + { + "name": "CDL2CROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3BLACKCROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3INSIDE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3LINESTRIKE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3OUTSIDE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3STARSINSOUTH", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDL3WHITESOLDIERS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLABANDONEDBABY", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLADVANCEBLOCK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLBELTHOLD", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLBREAKAWAY", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLCLOSINGMARUBOZU", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLCONCEALBABYSWALL", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLCOUNTERATTACK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDARKCLOUDCOVER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDOJISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLDRAGONFLYDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLENGULFING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLEVENINGDOJISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLEVENINGSTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLGAPSIDESIDEWHITE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLGRAVESTONEDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHAMMER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHANGINGMAN", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHARAMI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHARAMICROSS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHIGHWAVE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHIKKAKE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHIKKAKEMOD", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLHOMINGPIGEON", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLIDENTICAL3CROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLINNECK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLINVERTEDHAMMER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLKICKING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLKICKINGBYLENGTH", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLLADDERBOTTOM", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLLONGLEGGEDDOJI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLLONGLINE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMARUBOZU", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMATCHINGLOW", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMATHOLD", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMORNINGDOJISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLMORNINGSTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLONNECK", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLPIERCING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLRICKSHAWMAN", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLRISEFALL3METHODS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSEPARATINGLINES", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSHOOTINGSTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSHORTLINE", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSPINNINGTOP", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSTALLEDPATTERN", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLSTICKSANDWICH", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTAKURI", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTASUKIGAP", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTHRUSTING", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLTRISTAR", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLUNIQUE3RIVER", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLUPSIDEGAP2CROWS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "CDLXSIDEGAP3METHODS", + "category": "pattern", + "module": "ferro_ta.indicators.pattern", + "doc": "", + "params": [] + }, + { + "name": "beta", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "correlation_matrix", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "drawdown", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "portfolio_volatility", + "category": "portfolio", + "module": "ferro_ta.analysis.portfolio", + "doc": "", + "params": [] + }, + { + "name": "AVGPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "MEDPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "TYPPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "WCLPRICE", + "category": "price_transform", + "module": "ferro_ta.indicators.price_transform", + "doc": "", + "params": [] + }, + { + "name": "detect_breaks_cusum", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "regime", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "regime_adx", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "regime_combined", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "rolling_variance_break", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "structural_breaks", + "category": "regime", + "module": "ferro_ta.analysis.regime", + "doc": "", + "params": [] + }, + { + "name": "multi_timeframe", + "category": "resampling", + "module": "ferro_ta.data.resampling", + "doc": "", + "params": [] + }, + { + "name": "resample", + "category": "resampling", + "module": "ferro_ta.data.resampling", + "doc": "", + "params": [] + }, + { + "name": "volume_bars", + "category": "resampling", + "module": "ferro_ta.data.resampling", + "doc": "", + "params": [] + }, + { + "name": "compose", + "category": "signals", + "module": "ferro_ta.analysis.signals", + "doc": "", + "params": [] + }, + { + "name": "rank_signals", + "category": "signals", + "module": "ferro_ta.analysis.signals", + "doc": "", + "params": [] + }, + { + "name": "screen", + "category": "signals", + "module": "ferro_ta.analysis.signals", + "doc": "", + "params": [] + }, + { + "name": "BETA", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "CORREL", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_ANGLE", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_INTERCEPT", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_SLOPE", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "STDDEV", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "TSF", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "VAR", + "category": "statistic", + "module": "ferro_ta.indicators.statistic", + "doc": "", + "params": [] + }, + { + "name": "StreamingATR", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingBBands", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingEMA", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingMACD", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingRSI", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingSMA", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingStoch", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingSupertrend", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "StreamingVWAP", + "category": "streaming", + "module": "ferro_ta.data.streaming", + "doc": "", + "params": [] + }, + { + "name": "compute_indicator", + "category": "tools", + "module": "ferro_ta.tools.tools", + "doc": "", + "params": [] + }, + { + "name": "describe_indicator", + "category": "tools", + "module": "ferro_ta.tools.tools", + "doc": "", + "params": [] + }, + { + "name": "list_indicators", + "category": "tools", + "module": "ferro_ta.tools.tools", + "doc": "", + "params": [] + }, + { + "name": "run_backtest", + "category": "tools", + "module": "ferro_ta.tools.tools", + "doc": "", + "params": [] + }, + { + "name": "ACOS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "AD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ADD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ADOSC", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ADX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ADXR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "APO", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "AROON", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "AROONOSC", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ASIN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ATAN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ATR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "AVGPRICE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "BBANDS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "BETA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "BOP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CCI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL2CROWS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL3BLACKCROWS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL3INSIDE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL3LINESTRIKE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL3OUTSIDE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL3STARSINSOUTH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDL3WHITESOLDIERS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLABANDONEDBABY", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLADVANCEBLOCK", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLBELTHOLD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLBREAKAWAY", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLCLOSINGMARUBOZU", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLCONCEALBABYSWALL", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLCOUNTERATTACK", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLDARKCLOUDCOVER", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLDOJI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLDOJISTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLDRAGONFLYDOJI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLENGULFING", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLEVENINGDOJISTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLEVENINGSTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLGAPSIDESIDEWHITE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLGRAVESTONEDOJI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHAMMER", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHANGINGMAN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHARAMI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHARAMICROSS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHIGHWAVE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHIKKAKE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHIKKAKEMOD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLHOMINGPIGEON", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLIDENTICAL3CROWS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLINNECK", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLINVERTEDHAMMER", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLKICKING", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLKICKINGBYLENGTH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLLADDERBOTTOM", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLLONGLEGGEDDOJI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLLONGLINE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLMARUBOZU", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLMATCHINGLOW", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLMATHOLD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLMORNINGDOJISTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLMORNINGSTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLONNECK", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLPIERCING", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLRICKSHAWMAN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLRISEFALL3METHODS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLSEPARATINGLINES", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLSHOOTINGSTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLSHORTLINE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLSPINNINGTOP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLSTALLEDPATTERN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLSTICKSANDWICH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLTAKURI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLTASUKIGAP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLTHRUSTING", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLTRISTAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLUNIQUE3RIVER", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLUPSIDEGAP2CROWS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CDLXSIDEGAP3METHODS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CEIL", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CHANDELIER_EXIT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CHOPPINESS_INDEX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CMO", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "CORREL", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "COS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "COSH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "DEMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "DIV", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "DONCHIAN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "DX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "EMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "EXP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "FLOOR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HT_DCPERIOD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HT_DCPHASE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HT_PHASOR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HT_SINE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HT_TRENDLINE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HT_TRENDMODE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "HULL_MA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ICHIMOKU", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "KAMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "KELTNER_CHANNELS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_ANGLE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_INTERCEPT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "LINEARREG_SLOPE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "LN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "LOG10", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MACD", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MACDEXT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MACDFIX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MAMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MAVP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MAX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MAXINDEX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MEDPRICE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MFI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MIDPOINT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MIDPRICE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MIN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MININDEX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MINUS_DI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MINUS_DM", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MOM", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "MULT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "NATR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "OBV", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "PIVOT_POINTS", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "PLUS_DI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "PLUS_DM", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "PPO", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ROC", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ROCP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ROCR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ROCR100", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "RSI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SAREXT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SIN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SINH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SQRT", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "STDDEV", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "STOCH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "STOCHF", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "STOCHRSI", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SUB", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SUM", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "SUPERTREND", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "T3", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TAN", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TANH", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TEMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TRANGE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TRIMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TRIX", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TSF", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "TYPPRICE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "ULTOSC", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "VAR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "VWAP", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "VWMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "WCLPRICE", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "WILLR", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "WMA", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "__version__", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "about", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "benchmark", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "debug_mode", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "disable_debug", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "enable_debug", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "get_logger", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "indicators", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "info", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "log_call", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "methods", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "traced", + "category": "top_level", + "module": "ferro_ta", + "doc": "", + "params": [] + }, + { + "name": "plot", + "category": "viz", + "module": "ferro_ta.tools.viz", + "doc": "", + "params": [] + }, + { + "name": "ATR", + "category": "volatility", + "module": "ferro_ta.indicators.volatility", + "doc": "", + "params": [] + }, + { + "name": "NATR", + "category": "volatility", + "module": "ferro_ta.indicators.volatility", + "doc": "", + "params": [] + }, + { + "name": "TRANGE", + "category": "volatility", + "module": "ferro_ta.indicators.volatility", + "doc": "", + "params": [] + }, + { + "name": "AD", + "category": "volume", + "module": "ferro_ta.indicators.volume", + "doc": "", + "params": [] + }, + { + "name": "ADOSC", + "category": "volume", + "module": "ferro_ta.indicators.volume", + "doc": "", + "params": [] + }, + { + "name": "OBV", + "category": "volume", + "module": "ferro_ta.indicators.volume", + "doc": "", + "params": [] + } + ] + }, + "rust_core": { + "public_function_count": 64, + "functions": [ + { + "module": "futures.basis", + "function": "annualized_basis", + "file": "futures/basis.rs" + }, + { + "module": "futures.basis", + "function": "basis", + "file": "futures/basis.rs" + }, + { + "module": "futures.basis", + "function": "carry_spread", + "file": "futures/basis.rs" + }, + { + "module": "futures.basis", + "function": "implied_carry_rate", + "file": "futures/basis.rs" + }, + { + "module": "futures.curve", + "function": "calendar_spreads", + "file": "futures/curve.rs" + }, + { + "module": "futures.curve", + "function": "curve_slope", + "file": "futures/curve.rs" + }, + { + "module": "futures.curve", + "function": "curve_summary", + "file": "futures/curve.rs" + }, + { + "module": "futures.roll", + "function": "back_adjusted_continuous", + "file": "futures/roll.rs" + }, + { + "module": "futures.roll", + "function": "ratio_adjusted_continuous", + "file": "futures/roll.rs" + }, + { + "module": "futures.roll", + "function": "roll_yield", + "file": "futures/roll.rs" + }, + { + "module": "futures.roll", + "function": "weighted_continuous", + "file": "futures/roll.rs" + }, + { + "module": "futures.synthetic", + "function": "parity_gap", + "file": "futures/synthetic.rs" + }, + { + "module": "futures.synthetic", + "function": "synthetic_forward", + "file": "futures/synthetic.rs" + }, + { + "module": "futures.synthetic", + "function": "synthetic_spot", + "file": "futures/synthetic.rs" + }, + { + "module": "math", + "function": "max", + "file": "math.rs" + }, + { + "module": "math", + "function": "min", + "file": "math.rs" + }, + { + "module": "math", + "function": "sliding_max", + "file": "math.rs" + }, + { + "module": "math", + "function": "sliding_min", + "file": "math.rs" + }, + { + "module": "math", + "function": "sum", + "file": "math.rs" + }, + { + "module": "momentum", + "function": "adx", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "adxr", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "dx", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "minus_di", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "minus_dm", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "mom", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "plus_di", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "plus_dm", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "rsi", + "file": "momentum.rs" + }, + { + "module": "momentum", + "function": "stoch", + "file": "momentum.rs" + }, + { + "module": "options.chain", + "function": "atm_index", + "file": "options/chain.rs" + }, + { + "module": "options.chain", + "function": "label_moneyness", + "file": "options/chain.rs" + }, + { + "module": "options.chain", + "function": "select_strike_by_delta", + "file": "options/chain.rs" + }, + { + "module": "options.chain", + "function": "select_strike_by_offset", + "file": "options/chain.rs" + }, + { + "module": "options.greeks", + "function": "black_76_greeks", + "file": "options/greeks.rs" + }, + { + "module": "options.greeks", + "function": "black_scholes_greeks", + "file": "options/greeks.rs" + }, + { + "module": "options.greeks", + "function": "model_greeks", + "file": "options/greeks.rs" + }, + { + "module": "options.greeks", + "function": "model_theta", + "file": "options/greeks.rs" + }, + { + "module": "options.iv", + "function": "implied_volatility", + "file": "options/iv.rs" + }, + { + "module": "options.iv", + "function": "iv_percentile", + "file": "options/iv.rs" + }, + { + "module": "options.iv", + "function": "iv_rank", + "file": "options/iv.rs" + }, + { + "module": "options.iv", + "function": "iv_zscore", + "file": "options/iv.rs" + }, + { + "module": "options.mod", + "function": "sign", + "file": "options/mod.rs" + }, + { + "module": "options.normal", + "function": "cdf", + "file": "options/normal.rs" + }, + { + "module": "options.normal", + "function": "pdf", + "file": "options/normal.rs" + }, + { + "module": "options.pricing", + "function": "black_76_price", + "file": "options/pricing.rs" + }, + { + "module": "options.pricing", + "function": "black_scholes_price", + "file": "options/pricing.rs" + }, + { + "module": "options.pricing", + "function": "model_price", + "file": "options/pricing.rs" + }, + { + "module": "options.pricing", + "function": "price_lower_bound", + "file": "options/pricing.rs" + }, + { + "module": "options.pricing", + "function": "price_upper_bound", + "file": "options/pricing.rs" + }, + { + "module": "options.surface", + "function": "atm_iv", + "file": "options/surface.rs" + }, + { + "module": "options.surface", + "function": "linear_interpolate", + "file": "options/surface.rs" + }, + { + "module": "options.surface", + "function": "smile_metrics", + "file": "options/surface.rs" + }, + { + "module": "options.surface", + "function": "term_structure_slope", + "file": "options/surface.rs" + }, + { + "module": "overlap", + "function": "bbands", + "file": "overlap.rs" + }, + { + "module": "overlap", + "function": "ema", + "file": "overlap.rs" + }, + { + "module": "overlap", + "function": "macd", + "file": "overlap.rs" + }, + { + "module": "overlap", + "function": "sma", + "file": "overlap.rs" + }, + { + "module": "overlap", + "function": "sma_into", + "file": "overlap.rs" + }, + { + "module": "overlap", + "function": "wma", + "file": "overlap.rs" + }, + { + "module": "statistic", + "function": "stddev", + "file": "statistic.rs" + }, + { + "module": "volatility", + "function": "atr", + "file": "volatility.rs" + }, + { + "module": "volatility", + "function": "trange", + "file": "volatility.rs" + }, + { + "module": "volume", + "function": "mfi", + "file": "volume.rs" + }, + { + "module": "volume", + "function": "obv", + "file": "volume.rs" + } + ] + }, + "wasm_node": { + "export_count": 12, + "exports": [ + "adx", + "atr", + "bbands", + "ema", + "macd", + "mfi", + "mom", + "obv", + "rsi", + "sma", + "stochf", + "wma" + ] + } + }, + "parity_summary": { + "python_indicator_count": 207, + "wasm_export_count": 12, + "common_python_wasm_count": 12, + "common_python_wasm": [ + "adx", + "atr", + "bbands", + "ema", + "macd", + "mfi", + "mom", + "obv", + "rsi", + "sma", + "stochf", + "wma" + ], + "python_only_vs_wasm": [ + "acos", + "ad", + "add", + "adosc", + "adxr", + "aggregate_ticks", + "alertevent", + "alertmanager", + "apo", + "aroon", + "aroonosc", + "asin", + "atan", + "avgprice", + "batch_apply", + "batch_ema", + "batch_rsi", + "batch_sma", + "beta", + "bop", + "cci", + "cdl2crows", + "cdl3blackcrows", + "cdl3inside", + "cdl3linestrike", + "cdl3outside", + "cdl3starsinsouth", + "cdl3whitesoldiers", + "cdlabandonedbaby", + "cdladvanceblock", + "cdlbelthold", + "cdlbreakaway", + "cdlclosingmarubozu", + "cdlconcealbabyswall", + "cdlcounterattack", + "cdldarkcloudcover", + "cdldoji", + "cdldojistar", + "cdldragonflydoji", + "cdlengulfing", + "cdleveningdojistar", + "cdleveningstar", + "cdlgapsidesidewhite", + "cdlgravestonedoji", + "cdlhammer", + "cdlhangingman", + "cdlharami", + "cdlharamicross", + "cdlhighwave", + "cdlhikkake", + "cdlhikkakemod", + "cdlhomingpigeon", + "cdlidentical3crows", + "cdlinneck", + "cdlinvertedhammer", + "cdlkicking", + "cdlkickingbylength", + "cdlladderbottom", + "cdllongleggeddoji", + "cdllongline", + "cdlmarubozu", + "cdlmatchinglow", + "cdlmathold", + "cdlmorningdojistar", + "cdlmorningstar", + "cdlonneck", + "cdlpiercing", + "cdlrickshawman", + "cdlrisefall3methods", + "cdlseparatinglines", + "cdlshootingstar", + "cdlshortline", + "cdlspinningtop", + "cdlstalledpattern", + "cdlsticksandwich", + "cdltakuri", + "cdltasukigap", + "cdlthrusting", + "cdltristar", + "cdlunique3river", + "cdlupsidegap2crows", + "cdlxsidegap3methods", + "ceil", + "chandelier_exit", + "check_cross", + "check_threshold", + "choppiness_index", + "cmo", + "collect_alert_bars", + "compose", + "compute_many", + "continuous_bar_labels", + "correl", + "correlation_matrix", + "cos", + "cosh", + "dema", + "detect_breaks_cusum", + "div", + "donchian", + "drawdown", + "dx", + "exp", + "feature_matrix", + "floor", + "funding_pnl", + "ht_dcperiod", + "ht_dcphase", + "ht_phasor", + "ht_sine", + "ht_trendline", + "ht_trendmode", + "hull_ma", + "ichimoku", + "kama", + "keltner_channels", + "linearreg", + "linearreg_angle", + "linearreg_intercept", + "linearreg_slope", + "ln", + "log10", + "ma", + "macdext", + "macdfix", + "mama", + "mavp", + "max", + "maxindex", + "medprice", + "midpoint", + "midprice", + "min", + "minindex", + "minus_di", + "minus_dm", + "mult", + "multi_timeframe", + "natr", + "pivot_points", + "plus_di", + "plus_dm", + "portfolio_volatility", + "ppo", + "rank_signals", + "regime", + "regime_adx", + "regime_combined", + "resample", + "resample_continuous", + "roc", + "rocp", + "rocr", + "rocr100", + "rolling_variance_break", + "sar", + "sarext", + "screen", + "session_boundaries", + "sin", + "sinh", + "sqrt", + "stddev", + "stoch", + "stochrsi", + "streamingatr", + "streamingbbands", + "streamingema", + "streamingmacd", + "streamingrsi", + "streamingsma", + "streamingstoch", + "streamingsupertrend", + "streamingvwap", + "structural_breaks", + "sub", + "sum", + "supertrend", + "t3", + "tan", + "tanh", + "tema", + "tickaggregator", + "trange", + "trima", + "trix", + "tsf", + "typprice", + "ultosc", + "var", + "volume_bars", + "vwap", + "vwma", + "wclprice", + "willr" + ], + "wasm_only_vs_python": [] + } +} diff --git a/python/ferro_ta/analysis/attribution.py b/python/ferro_ta/analysis/attribution.py index 3e0952c..f131bae 100644 --- a/python/ferro_ta/analysis/attribution.py +++ b/python/ferro_ta/analysis/attribution.py @@ -38,6 +38,9 @@ from typing import Any, Optional import numpy as np from numpy.typing import ArrayLike, NDArray +from ferro_ta._ferro_ta import ( + extract_trades as _rust_extract_trades, +) from ferro_ta._ferro_ta import ( monthly_contribution as _rust_monthly_contribution, ) @@ -199,31 +202,10 @@ def from_backtest(result: Any) -> tuple[NDArray[np.float64], NDArray[np.float64] """ pos = np.asarray(result.positions, dtype=np.float64) ret = np.asarray(result.strategy_returns, dtype=np.float64) - n = len(pos) - - pnl_list: list[float] = [] - hold_list: list[float] = [] - - i = 0 - while i < n: - if pos[i] == 0.0: - i += 1 - continue - # Start of a trade - j = i + 1 - while j < n and pos[j] == pos[i]: - j += 1 - # Trade from i to j-1 - trade_pnl = float(np.sum(ret[i:j])) - pnl_list.append(trade_pnl) - hold_list.append(float(j - i)) - i = j - - if not pnl_list: - return np.empty(0, dtype=np.float64), np.empty(0, dtype=np.float64) + pnl, hold = _rust_extract_trades(pos, ret) return ( - np.array(pnl_list, dtype=np.float64), - np.array(hold_list, dtype=np.float64), + np.asarray(pnl, dtype=np.float64), + np.asarray(hold, dtype=np.float64), ) diff --git a/python/ferro_ta/analysis/backtest.py b/python/ferro_ta/analysis/backtest.py index af4ddc3..a3f59dd 100644 --- a/python/ferro_ta/analysis/backtest.py +++ b/python/ferro_ta/analysis/backtest.py @@ -55,6 +55,10 @@ from typing import Optional, Union import numpy as np from numpy.typing import ArrayLike, NDArray +from ferro_ta._ferro_ta import backtest_core as _rust_backtest_core +from ferro_ta._ferro_ta import macd_crossover_signals as _rust_macd_crossover_signals +from ferro_ta._ferro_ta import rsi_threshold_signals as _rust_rsi_threshold_signals +from ferro_ta._ferro_ta import sma_crossover_signals as _rust_sma_crossover_signals from ferro_ta.core.exceptions import FerroTAInputError, FerroTAValueError # --------------------------------------------------------------------------- @@ -149,16 +153,14 @@ def rsi_strategy( overbought : float RSI level above which a short (-1) signal is generated (default 70). """ - from ferro_ta import RSI # local import to avoid circular dep - if timeperiod < 1: raise FerroTAValueError(f"timeperiod must be >= 1, got {timeperiod}") c = np.asarray(close, dtype=np.float64) - rsi = np.asarray(RSI(c, timeperiod=timeperiod), dtype=np.float64) - signals = np.where(rsi <= oversold, 1.0, np.where(rsi >= overbought, -1.0, 0.0)) - signals[np.isnan(rsi)] = np.nan - return signals + return np.asarray( + _rust_rsi_threshold_signals(c, int(timeperiod), float(oversold), float(overbought)), + dtype=np.float64, + ) def sma_crossover_strategy( @@ -183,8 +185,6 @@ def sma_crossover_strategy( slow : int Slow SMA period (default 30). """ - from ferro_ta import SMA # local import - if fast < 1: raise FerroTAValueError(f"fast must be >= 1, got {fast}") if slow < 1: @@ -193,13 +193,10 @@ def sma_crossover_strategy( raise FerroTAValueError(f"fast ({fast}) must be less than slow ({slow})") c = np.asarray(close, dtype=np.float64) - sma_fast = np.asarray(SMA(c, timeperiod=fast), dtype=np.float64) - sma_slow = np.asarray(SMA(c, timeperiod=slow), dtype=np.float64) - signals = np.where(sma_fast > sma_slow, 1.0, -1.0).astype(np.float64) - # Warm-up: NaN where either MA is NaN - warmup = np.isnan(sma_fast) | np.isnan(sma_slow) - signals[warmup] = np.nan - return signals + return np.asarray( + _rust_sma_crossover_signals(c, int(fast), int(slow)), + dtype=np.float64, + ) def macd_crossover_strategy( @@ -227,8 +224,6 @@ def macd_crossover_strategy( signalperiod : int Signal line EMA period (default 9). """ - from ferro_ta import MACD # local import - if fastperiod < 1 or slowperiod < 1 or signalperiod < 1: raise FerroTAValueError("MACD periods must be >= 1") if fastperiod >= slowperiod: @@ -237,15 +232,12 @@ def macd_crossover_strategy( ) c = np.asarray(close, dtype=np.float64) - macd_line, signal_line, _ = MACD( - c, fastperiod=fastperiod, slowperiod=slowperiod, signalperiod=signalperiod + return np.asarray( + _rust_macd_crossover_signals( + c, int(fastperiod), int(slowperiod), int(signalperiod) + ), + dtype=np.float64, ) - macd_line = np.asarray(macd_line, dtype=np.float64) - signal_line = np.asarray(signal_line, dtype=np.float64) - signals = np.where(macd_line > signal_line, 1.0, -1.0).astype(np.float64) - warmup = np.isnan(macd_line) | np.isnan(signal_line) - signals[warmup] = np.nan - return signals # --------------------------------------------------------------------------- @@ -342,52 +334,14 @@ def backtest( # Compute signals # ------------------------------------------------------------------ signals = np.asarray(strategy_fn(c, **strategy_kwargs), dtype=np.float64) - - # ------------------------------------------------------------------ - # Positions: lag signals by 1 bar to avoid look-ahead bias - # ------------------------------------------------------------------ - positions = np.empty_like(signals) - positions[0] = 0.0 - positions[1:] = signals[:-1] - # Replace NaN in positions with 0 (flat) - positions = np.nan_to_num(positions, nan=0.0) - - # ------------------------------------------------------------------ - # Returns - # ------------------------------------------------------------------ - bar_returns: np.ndarray = np.empty(len(c), dtype=np.float64) - bar_returns[0] = 0.0 - bar_returns[1:] = np.diff(c) / c[:-1] - - strategy_returns = positions * bar_returns - position_changed = np.concatenate([[False], positions[1:] != positions[:-1]]) - - # Slippage: on each position change, reduce return by slippage_bps/10000 (one-way) - if slippage_bps > 0: - strategy_returns = strategy_returns.copy() - strategy_returns[position_changed] -= slippage_bps / 10_000.0 - - # Cumulative equity: with optional commission per trade - if commission_per_trade <= 0: - equity = np.cumprod(1.0 + strategy_returns) - else: - gross_equity = np.cumprod(1.0 + strategy_returns) - if np.any(gross_equity == 0.0): - equity = np.empty(len(c), dtype=np.float64) - equity[0] = 1.0 - for i in range(1, len(c)): - equity[i] = equity[i - 1] * (1.0 + strategy_returns[i]) - if position_changed[i]: - equity[i] -= commission_per_trade - else: - commissions = position_changed.astype(np.float64) * commission_per_trade - discounted_commissions = np.cumsum(commissions / gross_equity) - equity = gross_equity * (1.0 - discounted_commissions) + positions, bar_returns, strategy_returns, equity = _rust_backtest_core( + c, signals, float(commission_per_trade), float(slippage_bps) + ) return BacktestResult( signals=signals, - positions=positions, - bar_returns=bar_returns, - strategy_returns=strategy_returns, + positions=np.asarray(positions, dtype=np.float64), + bar_returns=np.asarray(bar_returns, dtype=np.float64), + strategy_returns=np.asarray(strategy_returns, dtype=np.float64), equity=np.asarray(equity, dtype=np.float64), ) diff --git a/python/ferro_ta/analysis/cross_asset.py b/python/ferro_ta/analysis/cross_asset.py index 190c460..d79bd86 100644 --- a/python/ferro_ta/analysis/cross_asset.py +++ b/python/ferro_ta/analysis/cross_asset.py @@ -40,6 +40,7 @@ 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 @@ -162,11 +163,7 @@ def ratio( >>> list(ratio(a, b)) [2.0, 3.0, 3.0] """ - av = _to_f64(a) - bv = _to_f64(b) - with np.errstate(divide="ignore", invalid="ignore"): - result = np.where(bv == 0, np.nan, av / bv) - return result + return _rust_ratio(_to_f64(a), _to_f64(b)) # --------------------------------------------------------------------------- diff --git a/python/ferro_ta/analysis/derivatives_payoff.py b/python/ferro_ta/analysis/derivatives_payoff.py index 2bda935..7891bca 100644 --- a/python/ferro_ta/analysis/derivatives_payoff.py +++ b/python/ferro_ta/analysis/derivatives_payoff.py @@ -11,10 +11,16 @@ from typing import Any import numpy as np from numpy.typing import ArrayLike, NDArray +from ferro_ta._ferro_ta import aggregate_greeks_legs as _rust_aggregate_greeks_legs +from ferro_ta._ferro_ta import strategy_payoff_dense as _rust_strategy_payoff_dense +from ferro_ta._ferro_ta import strategy_payoff_legs as _rust_strategy_payoff_legs from ferro_ta.analysis.options import OptionGreeks -from ferro_ta.analysis.options import greeks as option_greeks from ferro_ta.analysis.options_strategy import DerivativesStrategy, StrategyLeg -from ferro_ta.core.exceptions import FerroTAInputError, FerroTAValueError +from ferro_ta.core.exceptions import ( + FerroTAInputError, + FerroTAValueError, + _normalize_rust_error, +) __all__ = [ "PayoffLeg", @@ -79,14 +85,23 @@ def option_leg_payoff( ) -> NDArray[np.float64]: """Expiry payoff for a single option leg.""" grid = _coerce_spot_grid(spot_grid) - sign = _side_sign(side) * float(quantity) * float(multiplier) - if option_type == "call": - intrinsic = np.maximum(grid - float(strike), 0.0) - elif option_type == "put": - intrinsic = np.maximum(float(strike) - grid, 0.0) - else: + _side_sign(side) + if option_type not in {"call", "put"}: raise FerroTAValueError("option_type must be 'call' or 'put'.") - return sign * (intrinsic - float(premium)) + return np.asarray( + _rust_strategy_payoff_dense( + grid, + np.array([0], dtype=np.int64), # option + np.array([1 if side == "long" else -1], dtype=np.int64), + np.array([1 if option_type == "call" else -1], dtype=np.int64), + np.array([float(strike)], dtype=np.float64), + np.array([float(premium)], dtype=np.float64), + np.array([0.0], dtype=np.float64), + np.array([float(quantity)], dtype=np.float64), + np.array([float(multiplier)], dtype=np.float64), + ), + dtype=np.float64, + ) def futures_leg_payoff( @@ -99,8 +114,21 @@ def futures_leg_payoff( ) -> NDArray[np.float64]: """P/L profile for a futures leg.""" grid = _coerce_spot_grid(spot_grid) - sign = _side_sign(side) * float(quantity) * float(multiplier) - return sign * (grid - float(entry_price)) + _side_sign(side) + return np.asarray( + _rust_strategy_payoff_dense( + grid, + np.array([1], dtype=np.int64), # future + np.array([1 if side == "long" else -1], dtype=np.int64), + np.array([-1], dtype=np.int64), + np.array([0.0], dtype=np.float64), + np.array([0.0], dtype=np.float64), + np.array([float(entry_price)], dtype=np.float64), + np.array([float(quantity)], dtype=np.float64), + np.array([float(multiplier)], dtype=np.float64), + ), + dtype=np.float64, + ) def _mapping_to_leg(mapping: Mapping[str, Any]) -> PayoffLeg: @@ -141,31 +169,13 @@ def strategy_payoff( """Aggregate expiry payoff across option and futures legs.""" grid = _coerce_spot_grid(spot_grid) normalized = _normalize_legs(legs, strategy=strategy) - total = np.zeros_like(grid) - for leg in normalized: - if leg.instrument == "option": - if leg.strike is None: - raise FerroTAValueError("Option payoff legs require strike.") - total += option_leg_payoff( - grid, - strike=float(leg.strike), - premium=float(leg.premium), - option_type=str(leg.option_type), - side=str(leg.side), - quantity=float(leg.quantity), - multiplier=float(leg.multiplier), - ) - else: - if leg.entry_price is None: - raise FerroTAValueError("Futures payoff legs require entry_price.") - total += futures_leg_payoff( - grid, - entry_price=float(leg.entry_price), - side=str(leg.side), - quantity=float(leg.quantity), - multiplier=float(leg.multiplier), - ) - return total + if len(normalized) == 0: + return np.zeros_like(grid) + + try: + return np.asarray(_rust_strategy_payoff_legs(grid, normalized), dtype=np.float64) + except ValueError as err: + _normalize_rust_error(err) def aggregate_greeks( @@ -176,42 +186,20 @@ def aggregate_greeks( ) -> OptionGreeks: """Aggregate Greeks across option and futures legs.""" normalized = _normalize_legs(legs, strategy=strategy) - totals = { - "delta": 0.0, - "gamma": 0.0, - "vega": 0.0, - "theta": 0.0, - "rho": 0.0, - } - for leg in normalized: - leg_sign = _side_sign(leg.side) * float(leg.quantity) * float(leg.multiplier) - if leg.instrument == "future": - totals["delta"] += leg_sign - continue - if leg.strike is None or leg.volatility is None or leg.time_to_expiry is None: - raise FerroTAValueError( - "Option legs require strike, volatility, and time_to_expiry for Greeks aggregation." - ) - leg_greeks = option_greeks( - float(spot), - float(leg.strike), - float(leg.rate), - float(leg.time_to_expiry), - float(leg.volatility), - option_type=str(leg.option_type), - model="bsm", - carry=float(leg.carry), + if len(normalized) == 0: + return OptionGreeks(0.0, 0.0, 0.0, 0.0, 0.0) + + try: + delta, gamma, vega, theta, rho = _rust_aggregate_greeks_legs( + float(spot), normalized ) - totals["delta"] += leg_sign * float(leg_greeks.delta) - totals["gamma"] += leg_sign * float(leg_greeks.gamma) - totals["vega"] += leg_sign * float(leg_greeks.vega) - totals["theta"] += leg_sign * float(leg_greeks.theta) - totals["rho"] += leg_sign * float(leg_greeks.rho) + except ValueError as err: + _normalize_rust_error(err) return OptionGreeks( - totals["delta"], - totals["gamma"], - totals["vega"], - totals["theta"], - totals["rho"], + float(delta), + float(gamma), + float(vega), + float(theta), + float(rho), ) diff --git a/python/ferro_ta/analysis/features.py b/python/ferro_ta/analysis/features.py index 19e8be7..21629ef 100644 --- a/python/ferro_ta/analysis/features.py +++ b/python/ferro_ta/analysis/features.py @@ -23,6 +23,7 @@ from typing import Any, Optional, Union import numpy as np from numpy.typing import NDArray +from ferro_ta._ferro_ta import forward_fill_nan as _rust_forward_fill_nan from ferro_ta._utils import _to_f64 from ferro_ta.data.batch import compute_many @@ -32,13 +33,7 @@ __all__ = [ def _forward_fill_nan(arr: NDArray[np.float64]) -> NDArray[np.float64]: - mask = np.isnan(arr) - if not mask.any(): - return arr - - last_valid = np.where(~mask, np.arange(len(arr)), 0) - np.maximum.accumulate(last_valid, out=last_valid) - return arr[last_valid] + return np.asarray(_rust_forward_fill_nan(np.ascontiguousarray(arr, dtype=np.float64))) # --------------------------------------------------------------------------- diff --git a/python/ferro_ta/data/batch.py b/python/ferro_ta/data/batch.py index 8e8d6c2..9412f7b 100644 --- a/python/ferro_ta/data/batch.py +++ b/python/ferro_ta/data/batch.py @@ -6,16 +6,16 @@ This module provides a 2-D batch API that accepts a 2-D numpy array a 2-D output array of the same shape. For the most common indicators — SMA, EMA, RSI — the 2-D path is handled -entirely in Rust (a single GIL release for all columns). The generic -``batch_apply`` is available for other indicators that do not have a Rust -batch implementation. +entirely in Rust (a single GIL release for all columns). ``batch_apply`` +also dispatches these indicators to Rust when possible; other indicators +use the generic Python fallback path. Functions --------- batch_sma — SMA on every column of a 2-D array (Rust fast path for 2-D) batch_ema — EMA on every column of a 2-D array (Rust fast path for 2-D) batch_rsi — RSI on every column of a 2-D array (Rust fast path for 2-D) -batch_apply — Generic batch wrapper (Python loop) for any arbitrary indicator +batch_apply — Generic batch wrapper with Rust fast-path for SMA/EMA/RSI Usage ----- @@ -92,6 +92,27 @@ _HLC_FASTPATH_DEFAULTS: dict[str, int] = { "WILLR": 14, } +_BATCH_FASTPATH_DEFAULTS: dict[str, int] = { + "SMA": 30, + "EMA": 30, + "RSI": 14, +} + + +def _resolve_batch_fastpath( + fn: Callable[..., np.ndarray], + kwargs: dict[str, object], +) -> tuple[str, int] | None: + name = getattr(fn, "__name__", "").upper() + if name not in _BATCH_FASTPATH_DEFAULTS: + return None + if set(kwargs) - {"timeperiod"}: + return None + raw = kwargs.get("timeperiod", _BATCH_FASTPATH_DEFAULTS[name]) + if not isinstance(raw, int): + return None + return name, int(raw) + def _normalize_indicator_spec( spec: str | tuple[str, dict[str, object]] | tuple[str, dict[str, object], object], @@ -225,11 +246,9 @@ def batch_apply( ) -> np.ndarray: """Apply any single-series indicator *fn* to every column of *data*. - This is the generic fallback batch executor — it calls *fn* once per - column in a Python loop. For the common indicators SMA, EMA, and RSI - prefer the dedicated :func:`batch_sma`, :func:`batch_ema`, and - :func:`batch_rsi` functions, which use a Rust-side loop and avoid - per-column Python round-trips. + For recognized close-only indicators (SMA/EMA/RSI with default or + ``timeperiod`` argument only), this function dispatches to the Rust + batch kernels. Otherwise it falls back to a Python per-column loop. Parameters ---------- @@ -265,6 +284,16 @@ def batch_apply( if arr.ndim != 2: raise ValueError(f"batch_apply expects 1-D or 2-D input; got {arr.ndim}-D") + fastpath = _resolve_batch_fastpath(fn, kwargs) + if fastpath is not None: + indicator, timeperiod = fastpath + contiguous = np.ascontiguousarray(arr) + if indicator == "SMA": + return np.asarray(_rust_batch_sma(contiguous, timeperiod, True)) + if indicator == "EMA": + return np.asarray(_rust_batch_ema(contiguous, timeperiod, True)) + return np.asarray(_rust_batch_rsi(contiguous, timeperiod, True)) + n_samples, n_series = arr.shape result = np.empty((n_samples, n_series), dtype=np.float64) for j in range(n_series): diff --git a/python/ferro_ta/data/chunked.py b/python/ferro_ta/data/chunked.py index 1ba6501..574aa25 100644 --- a/python/ferro_ta/data/chunked.py +++ b/python/ferro_ta/data/chunked.py @@ -27,6 +27,7 @@ Rust backend ferro_ta._ferro_ta.make_chunk_ranges ferro_ta._ferro_ta.trim_overlap ferro_ta._ferro_ta.stitch_chunks + ferro_ta._ferro_ta.chunk_apply_close_indicator Notes ----- @@ -49,6 +50,9 @@ from typing import Any import numpy as np from numpy.typing import ArrayLike, NDArray +from ferro_ta._ferro_ta import ( + chunk_apply_close_indicator as _rust_chunk_apply_close_indicator, +) from ferro_ta._ferro_ta import ( make_chunk_ranges as _rust_make_chunk_ranges, ) @@ -67,6 +71,26 @@ __all__ = [ "stitch_chunks", ] +_FASTPATH_DEFAULT_PERIODS: dict[str, int] = { + "SMA": 30, + "EMA": 30, + "RSI": 14, +} + + +def _resolve_chunk_fastpath( + fn: Callable[..., Any], fn_kwargs: dict[str, Any] +) -> tuple[str, int] | None: + name = getattr(fn, "__name__", "").upper() + if name not in _FASTPATH_DEFAULT_PERIODS: + return None + if set(fn_kwargs) - {"timeperiod"}: + return None + raw = fn_kwargs.get("timeperiod", _FASTPATH_DEFAULT_PERIODS[name]) + if not isinstance(raw, int): + return None + return name, int(raw) + def make_chunk_ranges( n: int, @@ -190,6 +214,20 @@ def chunk_apply( if n == 0: return np.empty(0, dtype=np.float64) + fastpath = _resolve_chunk_fastpath(fn, fn_kwargs) + if fastpath is not None: + indicator, timeperiod = fastpath + return np.asarray( + _rust_chunk_apply_close_indicator( + np.ascontiguousarray(s), + indicator, + int(timeperiod), + int(chunk_size), + int(overlap), + ), + dtype=np.float64, + ) + ranges = make_chunk_ranges(n, chunk_size, overlap) if len(ranges) == 0: result = fn(s, **fn_kwargs) diff --git a/scripts/build_api_manifest.py b/scripts/build_api_manifest.py new file mode 100644 index 0000000..8bfff33 --- /dev/null +++ b/scripts/build_api_manifest.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +""" +Build a cross-surface API manifest for ferro-ta. + +The generated manifest summarizes: +- Python indicator/method exposure (from ferro_ta.tools.api_info) +- Core Rust crate public functions (ferro_ta_core) +- WASM/Node exported functions (from wasm pkg d.ts) + +Output is written to `docs/api_manifest.json`. +""" + +from __future__ import annotations + +import argparse +import ast +import datetime as _dt +import importlib.util +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def _load_api_info_module(root: Path, module_path: Path): + python_root = str(root / "python") + if python_root not in sys.path: + sys.path.insert(0, python_root) + spec = importlib.util.spec_from_file_location("ferro_ta_tools_api_info", module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load module spec from {module_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) # type: ignore[assignment] + return module + + +def _module_file(root: Path, module_name: str) -> Path | None: + module_rel = module_name.replace(".", "/") + file_path = root / "python" / f"{module_rel}.py" + if file_path.exists(): + return file_path + init_path = root / "python" / module_rel / "__init__.py" + if init_path.exists(): + return init_path + return None + + +def _extract_dunder_all(file_path: Path) -> list[str]: + try: + source = file_path.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(file_path)) + except Exception: + return [] + + exports: list[str] = [] + for node in tree.body: + value_node = None + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "__all__": + value_node = node.value + break + elif isinstance(node, ast.AnnAssign): + target = node.target + if isinstance(target, ast.Name) and target.id == "__all__": + value_node = node.value + if value_node is None: + continue + try: + value = ast.literal_eval(value_node) + except Exception: + continue + if isinstance(value, str): + exports = [value] + elif isinstance(value, (list, tuple)): + exports = [item for item in value if isinstance(item, str)] + return exports + + +def _module_exports(root: Path, module_name: str) -> list[str]: + file_path = _module_file(root, module_name) + if file_path is None: + return [] + return _extract_dunder_all(file_path) + + +def _extract_python_api(root: Path) -> dict[str, Any]: + module_path = root / "python" / "ferro_ta" / "tools" / "api_info.py" + api_info_module = _load_api_info_module(root, module_path) + + category_modules = dict(getattr(api_info_module, "_CATEGORY_MODULES", {})) + method_modules = dict(getattr(api_info_module, "_METHOD_MODULES", {})) + + indicators: list[dict[str, Any]] = [] + seen_indicators: set[str] = set() + for category, module_name in category_modules.items(): + for name in _module_exports(root, module_name): + if name in seen_indicators: + continue + seen_indicators.add(name) + indicators.append( + { + "name": name, + "category": category, + "module": module_name, + "doc": "", + "params": [], + } + ) + + methods: list[dict[str, Any]] = [] + seen_methods: set[tuple[str, str]] = set() + for category, module_name in method_modules.items(): + for name in _module_exports(root, module_name): + key = (module_name, name) + if key in seen_methods: + continue + seen_methods.add(key) + methods.append( + { + "name": name, + "category": category, + "module": module_name, + "doc": "", + "params": [], + } + ) + + indicators.sort(key=lambda entry: entry["name"]) + methods.sort(key=lambda entry: (entry["category"], entry["name"])) + + categories = sorted({entry["category"] for entry in indicators}) + + if not indicators: + raise RuntimeError( + "No Python indicators discovered from source exports. " + "Check `python/ferro_ta/tools/api_info.py` mappings and module __all__ declarations." + ) + + return { + "indicator_count": len(indicators), + "method_count": len(methods), + "categories": categories, + "indicators": indicators, + "methods": methods, + } + + +def _extract_core_exports(root: Path) -> list[dict[str, str]]: + core_src = root / "crates" / "ferro_ta_core" / "src" + entries: list[dict[str, str]] = [] + + for rs_file in sorted(core_src.rglob("*.rs")): + rel = rs_file.relative_to(core_src).as_posix() + module = rel[:-3].replace("/", ".") + text = rs_file.read_text(encoding="utf-8") + for match in re.finditer(r"(?m)^\s*pub\s+fn\s+([A-Za-z0-9_]+)\s*\(", text): + entries.append( + { + "module": module, + "function": match.group(1), + "file": rel, + } + ) + + entries.sort(key=lambda item: (item["module"], item["function"])) + return entries + + +def _extract_wasm_exports(root: Path) -> list[str]: + exports: set[str] = set() + + # Source exports are the canonical declaration of the WASM/Node API and + # avoid drift when a stale wasm/pkg folder is present locally. + wasm_lib = root / "wasm" / "src" / "lib.rs" + if wasm_lib.exists(): + text = wasm_lib.read_text(encoding="utf-8") + for match in re.finditer( + r"(?ms)#\s*\[wasm_bindgen(?:\([^\)]*\))?\]\s*pub\s+fn\s+([A-Za-z0-9_]+)\s*\(", + text, + ): + exports.add(match.group(1)) + if exports: + return sorted(exports) + + # Fallback to generated declarations if source parsing did not find exports. + dts_path = root / "wasm" / "pkg" / "ferro_ta_wasm.d.ts" + if dts_path.exists(): + for line in dts_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line.startswith("export function "): + name = line[len("export function ") :].split("(")[0].strip() + if name: + exports.add(name) + + return sorted(exports) + + +def _safe_git_head(root: Path) -> str | None: + try: + completed = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=root, + capture_output=True, + text=True, + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return None + value = completed.stdout.strip() + return value or None + + +def build_manifest(root: Path, include_runtime_metadata: bool = False) -> dict[str, Any]: + python_api = _extract_python_api(root) + rust_core = _extract_core_exports(root) + wasm_exports = _extract_wasm_exports(root) + + python_indicator_names = {entry["name"] for entry in python_api["indicators"]} + python_indicator_names_lc = {name.lower() for name in python_indicator_names} + wasm_set = set(wasm_exports) + wasm_set_lc = {name.lower() for name in wasm_set} + common_with_wasm = sorted(python_indicator_names_lc.intersection(wasm_set_lc)) + + manifest: dict[str, Any] = { + "surfaces": { + "python": python_api, + "rust_core": { + "public_function_count": len(rust_core), + "functions": rust_core, + }, + "wasm_node": { + "export_count": len(wasm_exports), + "exports": wasm_exports, + }, + }, + "parity_summary": { + "python_indicator_count": len(python_indicator_names_lc), + "wasm_export_count": len(wasm_set), + "common_python_wasm_count": len(common_with_wasm), + "common_python_wasm": common_with_wasm, + "python_only_vs_wasm": sorted(python_indicator_names_lc - wasm_set_lc), + "wasm_only_vs_python": sorted(wasm_set_lc - python_indicator_names_lc), + }, + } + + if include_runtime_metadata: + manifest["generated_at_utc"] = _dt.datetime.now(tz=_dt.UTC).isoformat() + manifest["git_head"] = _safe_git_head(root) + + return manifest + + +def main() -> None: + parser = argparse.ArgumentParser(description="Build cross-surface API manifest") + parser.add_argument( + "--output", + type=Path, + default=Path("docs/api_manifest.json"), + help="Output JSON path relative to repo root (default: docs/api_manifest.json)", + ) + parser.add_argument( + "--include-runtime-metadata", + action="store_true", + help=( + "Include non-deterministic metadata fields (timestamp, git head). " + "Disabled by default to keep manifest reproducible for CI checks." + ), + ) + args = parser.parse_args() + + root = _repo_root() + output_path = (root / args.output).resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + + manifest = build_manifest(root, include_runtime_metadata=args.include_runtime_metadata) + output_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + print(f"Wrote API manifest to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_api_manifest.py b/scripts/check_api_manifest.py new file mode 100644 index 0000000..dd162be --- /dev/null +++ b/scripts/check_api_manifest.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Check that docs/api_manifest.json is up-to-date. + +This script regenerates the deterministic manifest in-memory and compares it to +the committed file. It exits non-zero if drift is detected. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + python_root = str(root / "python") + if python_root not in sys.path: + sys.path.insert(0, python_root) + scripts_root = str(root / "scripts") + if scripts_root not in sys.path: + sys.path.insert(0, scripts_root) + + from build_api_manifest import build_manifest + + manifest_path = root / "docs" / "api_manifest.json" + + if not manifest_path.exists(): + print( + "docs/api_manifest.json is missing. Run:\n" + " python scripts/build_api_manifest.py --output docs/api_manifest.json" + ) + return 1 + + expected = build_manifest(root, include_runtime_metadata=False) + actual = json.loads(manifest_path.read_text(encoding="utf-8")) + + if actual != expected: + print( + "docs/api_manifest.json is out of date.\n" + "Run:\n" + " python scripts/build_api_manifest.py --output docs/api_manifest.json\n" + "and commit the updated file." + ) + return 1 + + print("docs/api_manifest.json is up to date.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/attribution/mod.rs b/src/attribution/mod.rs index e102fb4..0faab89 100644 --- a/src/attribution/mod.rs +++ b/src/attribution/mod.rs @@ -191,6 +191,54 @@ pub fn signal_attribution<'py>( Ok((labels.into_pyarray(py), contributions.into_pyarray(py))) } +// --------------------------------------------------------------------------- +// extract_trades +// --------------------------------------------------------------------------- + +/// Extract trade-level pnl and hold durations from positions and strategy returns. +/// +/// A trade is a maximal contiguous run of non-zero position values. +#[pyfunction] +#[allow(clippy::type_complexity)] +pub fn extract_trades<'py>( + py: Python<'py>, + positions: PyReadonlyArray1<'py, f64>, + strategy_returns: PyReadonlyArray1<'py, f64>, +) -> PyResult<(Bound<'py, PyArray1>, Bound<'py, PyArray1>)> { + let pos = positions.as_slice()?; + let ret = strategy_returns.as_slice()?; + let n = pos.len(); + if n != ret.len() { + return Err(PyValueError::new_err( + "positions and strategy_returns must have the same length", + )); + } + + let mut pnl = Vec::::new(); + let mut hold = Vec::::new(); + + let mut i = 0usize; + while i < n { + if pos[i] == 0.0 { + i += 1; + continue; + } + let mut j = i + 1; + while j < n && pos[j] == pos[i] { + j += 1; + } + let mut trade_pnl = 0.0_f64; + for v in ret.iter().take(j).skip(i) { + trade_pnl += *v; + } + pnl.push(trade_pnl); + hold.push((j - i) as f64); + i = j; + } + + Ok((pnl.into_pyarray(py), hold.into_pyarray(py))) +} + // --------------------------------------------------------------------------- // Register // --------------------------------------------------------------------------- @@ -199,5 +247,6 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(trade_stats, m)?)?; m.add_function(wrap_pyfunction!(monthly_contribution, m)?)?; m.add_function(wrap_pyfunction!(signal_attribution, m)?)?; + m.add_function(wrap_pyfunction!(extract_trades, m)?)?; Ok(()) } diff --git a/src/backtest/mod.rs b/src/backtest/mod.rs new file mode 100644 index 0000000..213b76b --- /dev/null +++ b/src/backtest/mod.rs @@ -0,0 +1,244 @@ +//! Rust-backed strategy signal generation and backtest core. +//! +//! These functions move the hot loops from Python into Rust while preserving +//! the public Python behavior. + +use crate::validation; +use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +fn nan_to_num_with_numpy_defaults(v: f64) -> f64 { + if v.is_nan() { + 0.0 + } else if v.is_infinite() { + if v.is_sign_positive() { + f64::MAX + } else { + -f64::MAX + } + } else { + v + } +} + +// --------------------------------------------------------------------------- +// Strategy signal helpers +// --------------------------------------------------------------------------- + +/// RSI threshold strategy: +/// +1 when RSI <= oversold, -1 when RSI >= overbought, 0 otherwise. +/// Warm-up bars are NaN. +#[pyfunction] +#[pyo3(signature = (close, timeperiod = 14, oversold = 30.0, overbought = 70.0))] +pub fn rsi_threshold_signals<'py>( + py: Python<'py>, + close: PyReadonlyArray1<'py, f64>, + timeperiod: usize, + oversold: f64, + overbought: f64, +) -> PyResult>> { + validation::validate_timeperiod(timeperiod, "timeperiod", 1)?; + let prices = close.as_slice()?; + let rsi = ferro_ta_core::momentum::rsi(prices, timeperiod); + let out: Vec = rsi + .iter() + .map(|&v| { + if v.is_nan() { + f64::NAN + } else if v <= oversold { + 1.0 + } else if v >= overbought { + -1.0 + } else { + 0.0 + } + }) + .collect(); + Ok(out.into_pyarray(py)) +} + +/// SMA crossover strategy: +/// +1 when fast SMA > slow SMA, -1 otherwise. Warm-up bars are NaN. +#[pyfunction] +#[pyo3(signature = (close, fast = 10, slow = 30))] +pub fn sma_crossover_signals<'py>( + py: Python<'py>, + close: PyReadonlyArray1<'py, f64>, + fast: usize, + slow: usize, +) -> PyResult>> { + validation::validate_timeperiod(fast, "fast", 1)?; + validation::validate_timeperiod(slow, "slow", 1)?; + if fast >= slow { + return Err(PyValueError::new_err(format!( + "fast ({fast}) must be less than slow ({slow})" + ))); + } + let prices = close.as_slice()?; + let sma_fast = ferro_ta_core::overlap::sma(prices, fast); + let sma_slow = ferro_ta_core::overlap::sma(prices, slow); + let out: Vec = sma_fast + .iter() + .zip(sma_slow.iter()) + .map(|(&f, &s)| { + if f.is_nan() || s.is_nan() { + f64::NAN + } else if f > s { + 1.0 + } else { + -1.0 + } + }) + .collect(); + Ok(out.into_pyarray(py)) +} + +/// MACD crossover strategy: +/// +1 when MACD line > signal line, -1 otherwise. Warm-up bars are NaN. +#[pyfunction] +#[pyo3(signature = (close, fastperiod = 12, slowperiod = 26, signalperiod = 9))] +pub fn macd_crossover_signals<'py>( + py: Python<'py>, + close: PyReadonlyArray1<'py, f64>, + fastperiod: usize, + slowperiod: usize, + signalperiod: usize, +) -> PyResult>> { + validation::validate_timeperiod(fastperiod, "fastperiod", 1)?; + validation::validate_timeperiod(slowperiod, "slowperiod", 1)?; + validation::validate_timeperiod(signalperiod, "signalperiod", 1)?; + if fastperiod >= slowperiod { + return Err(PyValueError::new_err(format!( + "fastperiod ({fastperiod}) must be less than slowperiod ({slowperiod})" + ))); + } + + let prices = close.as_slice()?; + let (macd_line, signal_line, _) = + ferro_ta_core::overlap::macd(prices, fastperiod, slowperiod, signalperiod); + let out: Vec = macd_line + .iter() + .zip(signal_line.iter()) + .map(|(&m, &s)| { + if m.is_nan() || s.is_nan() { + f64::NAN + } else if m > s { + 1.0 + } else { + -1.0 + } + }) + .collect(); + Ok(out.into_pyarray(py)) +} + +// --------------------------------------------------------------------------- +// Backtest core +// --------------------------------------------------------------------------- + +/// Backtest core loop over close prices and strategy signals. +/// +/// Returns `(positions, bar_returns, strategy_returns, equity)`. +#[pyfunction] +#[pyo3(signature = (close, signals, commission_per_trade = 0.0, slippage_bps = 0.0))] +#[allow(clippy::type_complexity)] +pub fn backtest_core<'py>( + py: Python<'py>, + close: PyReadonlyArray1<'py, f64>, + signals: PyReadonlyArray1<'py, f64>, + commission_per_trade: f64, + slippage_bps: f64, +) -> PyResult<( + Bound<'py, PyArray1>, + Bound<'py, PyArray1>, + Bound<'py, PyArray1>, + Bound<'py, PyArray1>, +)> { + let c = close.as_slice()?; + let s = signals.as_slice()?; + let n = c.len(); + validation::validate_equal_length(&[(n, "close"), (s.len(), "signals")])?; + + let mut positions = vec![0.0_f64; n]; + if n > 1 { + for i in 1..n { + positions[i] = nan_to_num_with_numpy_defaults(s[i - 1]); + } + } + + let mut bar_returns = vec![0.0_f64; n]; + for i in 1..n { + bar_returns[i] = (c[i] - c[i - 1]) / c[i - 1]; + } + + let mut strategy_returns = vec![0.0_f64; n]; + for i in 0..n { + strategy_returns[i] = positions[i] * bar_returns[i]; + } + + let mut position_changed = vec![false; n]; + for i in 1..n { + position_changed[i] = positions[i] != positions[i - 1]; + } + + if slippage_bps > 0.0 { + let slip = slippage_bps / 10_000.0; + for i in 0..n { + if position_changed[i] { + strategy_returns[i] -= slip; + } + } + } + + let mut equity = vec![1.0_f64; n]; + if n > 0 { + if commission_per_trade <= 0.0 { + let mut gross = 1.0_f64; + for i in 0..n { + gross *= 1.0 + strategy_returns[i]; + equity[i] = gross; + } + } else { + let mut gross_equity = vec![1.0_f64; n]; + let mut gross = 1.0_f64; + for i in 0..n { + gross *= 1.0 + strategy_returns[i]; + gross_equity[i] = gross; + } + + if gross_equity.contains(&0.0) { + equity[0] = 1.0; + for i in 1..n { + equity[i] = equity[i - 1] * (1.0 + strategy_returns[i]); + if position_changed[i] { + equity[i] -= commission_per_trade; + } + } + } else { + let mut discounted_commissions = 0.0_f64; + for i in 0..n { + if position_changed[i] { + discounted_commissions += commission_per_trade / gross_equity[i]; + } + equity[i] = gross_equity[i] * (1.0 - discounted_commissions); + } + } + } + } + + Ok(( + positions.into_pyarray(py), + bar_returns.into_pyarray(py), + strategy_returns.into_pyarray(py), + equity.into_pyarray(py), + )) +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(rsi_threshold_signals, m)?)?; + m.add_function(wrap_pyfunction!(sma_crossover_signals, m)?)?; + m.add_function(wrap_pyfunction!(macd_crossover_signals, m)?)?; + m.add_function(wrap_pyfunction!(backtest_core, m)?)?; + Ok(()) +} diff --git a/src/chunked/mod.rs b/src/chunked/mod.rs index 095d803..25e7a1c 100644 --- a/src/chunked/mod.rs +++ b/src/chunked/mod.rs @@ -8,11 +8,15 @@ //! //! Functions //! --------- -//! - `trim_overlap` — remove the first *overlap* elements from an array -//! (to strip the warm-up from a chunk's indicator output). -//! - `stitch_chunks` — concatenate trimmed chunk results into one array. -//! - `make_chunk_ranges` — compute start/end indices for a series given chunk -//! size and overlap, for use by the Python caller. +//! - `trim_overlap` — remove the first *overlap* elements from +//! an array (to strip the warm-up from a chunk's indicator output). +//! - `stitch_chunks` — concatenate trimmed chunk results into one +//! array. +//! - `make_chunk_ranges` — compute start/end indices for a series +//! given chunk size and overlap, for use by the Python caller. +//! - `chunk_apply_close_indicator`— run chunked close-only indicators fully in +//! Rust (SMA/EMA/RSI). +//! - `forward_fill_nan` — forward-fill NaN values in a 1-D array. use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1}; use pyo3::exceptions::PyValueError; @@ -132,6 +136,125 @@ pub fn make_chunk_ranges<'py>( Ok(ranges.into_pyarray(py)) } +// --------------------------------------------------------------------------- +// chunk_apply_close_indicator +// --------------------------------------------------------------------------- + +fn compute_close_indicator( + indicator: &str, + series: &[f64], + timeperiod: usize, +) -> PyResult> { + match indicator { + "SMA" => Ok(ferro_ta_core::overlap::sma(series, timeperiod)), + "EMA" => Ok(ferro_ta_core::overlap::ema(series, timeperiod)), + "RSI" => Ok(ferro_ta_core::momentum::rsi(series, timeperiod)), + _ => Err(PyValueError::new_err(format!( + "chunk_apply_close_indicator does not support indicator '{indicator}'" + ))), + } +} + +/// Run chunked execution for close-only indicators in Rust. +/// +/// Parameters +/// ---------- +/// series : 1-D float64 array +/// indicator : one of {"SMA", "EMA", "RSI"} +/// timeperiod : indicator period (>= 1) +/// chunk_size : output bars per chunk (>= 1) +/// overlap : warm-up bars prepended to each chunk +/// +/// Returns +/// ------- +/// 1-D float64 array with the same length as `series`. +#[pyfunction] +#[pyo3(signature = (series, indicator, timeperiod, chunk_size = 10_000, overlap = 100))] +pub fn chunk_apply_close_indicator<'py>( + py: Python<'py>, + series: PyReadonlyArray1<'py, f64>, + indicator: &str, + timeperiod: usize, + chunk_size: usize, + overlap: usize, +) -> PyResult>> { + if timeperiod == 0 { + return Err(PyValueError::new_err("timeperiod must be >= 1")); + } + if chunk_size == 0 { + return Err(PyValueError::new_err("chunk_size must be >= 1")); + } + + let values = series.as_slice()?; + if values.is_empty() { + return Ok(Vec::::new().into_pyarray(py)); + } + + let name = indicator.to_ascii_uppercase(); + let n = values.len(); + let mut stitched: Vec = Vec::with_capacity(n); + let mut start = 0usize; + let mut chunk_index = 0usize; + + loop { + let end = (start + chunk_size + overlap).min(n); + let chunk = &values[start..end]; + let out = compute_close_indicator(name.as_str(), chunk, timeperiod)?; + + let discard = if chunk_index == 0 { 0 } else { overlap }; + if discard > out.len() { + return Err(PyValueError::new_err(format!( + "overlap ({discard}) must be <= chunk output length ({})", + out.len() + ))); + } + stitched.extend_from_slice(&out[discard..]); + + if end >= n { + break; + } + start = end.saturating_sub(overlap); + chunk_index += 1; + } + + if stitched.len() != n { + return Err(PyValueError::new_err(format!( + "internal chunk stitching error: expected output length {n}, got {}", + stitched.len() + ))); + } + + Ok(stitched.into_pyarray(py)) +} + +// --------------------------------------------------------------------------- +// forward_fill_nan +// --------------------------------------------------------------------------- + +/// Forward-fill NaN values in a 1-D array. +/// +/// Leading NaN values are preserved until the first non-NaN value appears. +#[pyfunction] +pub fn forward_fill_nan<'py>( + py: Python<'py>, + values: PyReadonlyArray1<'py, f64>, +) -> PyResult>> { + let input = values.as_slice()?; + let mut out = Vec::with_capacity(input.len()); + let mut last = f64::NAN; + + for &value in input { + if value.is_nan() { + out.push(last); + } else { + last = value; + out.push(value); + } + } + + Ok(out.into_pyarray(py)) +} + // --------------------------------------------------------------------------- // Register // --------------------------------------------------------------------------- @@ -140,5 +263,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(trim_overlap, m)?)?; m.add_function(wrap_pyfunction!(stitch_chunks, m)?)?; m.add_function(wrap_pyfunction!(make_chunk_ranges, m)?)?; + m.add_function(wrap_pyfunction!(chunk_apply_close_indicator, m)?)?; + m.add_function(wrap_pyfunction!(forward_fill_nan, m)?)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index f7bc361..f8e5eee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod aggregation; pub mod alerts; pub mod attribution; +pub mod backtest; pub mod batch; pub mod chunked; pub mod crypto; @@ -70,5 +71,6 @@ fn _ferro_ta(m: &Bound<'_, PyModule>) -> PyResult<()> { chunked::register(m)?; regime::register(m)?; attribution::register(m)?; + backtest::register(m)?; Ok(()) } diff --git a/src/options/mod.rs b/src/options/mod.rs index bd8fe52..134134b 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -3,6 +3,7 @@ mod chain; mod greeks; mod iv; +mod payoff; mod pricing; mod surface; @@ -63,5 +64,21 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m )?)?; m.add_function(pyo3::wrap_pyfunction!(self::chain::select_strike_delta, m)?)?; + m.add_function(pyo3::wrap_pyfunction!( + self::payoff::strategy_payoff_dense, + m + )?)?; + m.add_function(pyo3::wrap_pyfunction!( + self::payoff::strategy_payoff_legs, + m + )?)?; + m.add_function(pyo3::wrap_pyfunction!( + self::payoff::aggregate_greeks_dense, + m + )?)?; + m.add_function(pyo3::wrap_pyfunction!( + self::payoff::aggregate_greeks_legs, + m + )?)?; Ok(()) } diff --git a/src/options/payoff.rs b/src/options/payoff.rs new file mode 100644 index 0000000..8b9d498 --- /dev/null +++ b/src/options/payoff.rs @@ -0,0 +1,443 @@ +use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyTuple}; + +#[derive(Clone, Copy)] +enum Instrument { + Option, + Future, +} + +#[derive(Clone, Copy)] +enum Side { + Long, + Short, +} + +#[derive(Clone, Copy)] +enum OptionType { + Call, + Put, +} + +impl Side { + fn sign(self) -> f64 { + match self { + Side::Long => 1.0, + Side::Short => -1.0, + } + } +} + +fn parse_instrument(v: i64) -> PyResult { + match v { + 0 => Ok(Instrument::Option), + 1 => Ok(Instrument::Future), + _ => Err(PyValueError::new_err( + "instrument must be 0 (option) or 1 (future)", + )), + } +} + +fn parse_side(v: i64) -> PyResult { + match v { + 1 => Ok(Side::Long), + -1 => Ok(Side::Short), + _ => Err(PyValueError::new_err("side must be 1 (long) or -1 (short)")), + } +} + +fn parse_option_type(v: i64) -> PyResult { + match v { + 1 => Ok(OptionType::Call), + -1 => Ok(OptionType::Put), + _ => Err(PyValueError::new_err( + "option_type must be 1 (call) or -1 (put)", + )), + } +} + +fn parse_instrument_label(v: &str) -> PyResult { + match v.to_ascii_lowercase().as_str() { + "option" => Ok(Instrument::Option), + "future" => Ok(Instrument::Future), + _ => Err(PyValueError::new_err( + "instrument must be 'option' or 'future'", + )), + } +} + +fn parse_side_label(v: &str) -> PyResult { + match v.to_ascii_lowercase().as_str() { + "long" => Ok(Side::Long), + "short" => Ok(Side::Short), + _ => Err(PyValueError::new_err("side must be 'long' or 'short'")), + } +} + +fn parse_option_type_label(v: &str) -> PyResult { + match v.to_ascii_lowercase().as_str() { + "call" => Ok(OptionType::Call), + "put" => Ok(OptionType::Put), + _ => Err(PyValueError::new_err("option_type must be 'call' or 'put'")), + } +} + +fn leg_attr_string(leg: &Bound<'_, PyAny>, name: &str) -> PyResult { + let value = leg + .getattr(name) + .map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?; + value.extract::().map_err(|_| { + PyValueError::new_err(format!( + "leg field '{name}' has invalid type; expected string" + )) + }) +} + +fn leg_attr_f64(leg: &Bound<'_, PyAny>, name: &str) -> PyResult { + let value = leg + .getattr(name) + .map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?; + value.extract::().map_err(|_| { + PyValueError::new_err(format!( + "leg field '{name}' has invalid type; expected float" + )) + }) +} + +fn leg_attr_optional_string(leg: &Bound<'_, PyAny>, name: &str) -> PyResult> { + let value = leg + .getattr(name) + .map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?; + if value.is_none() { + return Ok(None); + } + value.extract::().map(Some).map_err(|_| { + PyValueError::new_err(format!( + "leg field '{name}' has invalid type; expected string or None" + )) + }) +} + +fn leg_attr_optional_f64(leg: &Bound<'_, PyAny>, name: &str) -> PyResult> { + let value = leg + .getattr(name) + .map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?; + if value.is_none() { + return Ok(None); + } + value.extract::().map(Some).map_err(|_| { + PyValueError::new_err(format!( + "leg field '{name}' has invalid type; expected float or None" + )) + }) +} + +/// Compute aggregate strategy payoff over a spot grid. +/// +/// Encoded arrays (same length = n_legs): +/// - `instruments`: 0=option, 1=future +/// - `sides`: 1=long, -1=short +/// - `option_types`: 1=call, -1=put (ignored for futures) +/// - `strikes`: strike for options, ignored for futures +/// - `premiums`: premium for options, ignored for futures +/// - `entry_prices`: entry price for futures, ignored for options +/// - `quantities`, `multipliers`: applied to both instruments +#[pyfunction] +#[allow(clippy::too_many_arguments)] +pub fn strategy_payoff_dense<'py>( + py: Python<'py>, + spot_grid: PyReadonlyArray1<'py, f64>, + instruments: PyReadonlyArray1<'py, i64>, + sides: PyReadonlyArray1<'py, i64>, + option_types: PyReadonlyArray1<'py, i64>, + strikes: PyReadonlyArray1<'py, f64>, + premiums: PyReadonlyArray1<'py, f64>, + entry_prices: PyReadonlyArray1<'py, f64>, + quantities: PyReadonlyArray1<'py, f64>, + multipliers: PyReadonlyArray1<'py, f64>, +) -> PyResult>> { + let grid = spot_grid.as_slice()?; + let inst = instruments.as_slice()?; + let side = sides.as_slice()?; + let opt_t = option_types.as_slice()?; + let strike = strikes.as_slice()?; + let premium = premiums.as_slice()?; + let entry = entry_prices.as_slice()?; + let qty = quantities.as_slice()?; + let mult = multipliers.as_slice()?; + + let n_legs = inst.len(); + if side.len() != n_legs + || opt_t.len() != n_legs + || strike.len() != n_legs + || premium.len() != n_legs + || entry.len() != n_legs + || qty.len() != n_legs + || mult.len() != n_legs + { + return Err(PyValueError::new_err( + "All leg arrays must have the same length", + )); + } + + let mut total = vec![0.0_f64; grid.len()]; + + for leg_idx in 0..n_legs { + let instrument = parse_instrument(inst[leg_idx])?; + let side_sign = parse_side(side[leg_idx])?.sign(); + let leg_scale = side_sign * qty[leg_idx] * mult[leg_idx]; + + match instrument { + Instrument::Option => { + let otype = parse_option_type(opt_t[leg_idx])?; + let k = strike[leg_idx]; + let p = premium[leg_idx]; + for (i, &s) in grid.iter().enumerate() { + let intrinsic = match otype { + OptionType::Call => (s - k).max(0.0), + OptionType::Put => (k - s).max(0.0), + }; + total[i] += leg_scale * (intrinsic - p); + } + } + Instrument::Future => { + let e = entry[leg_idx]; + for (i, &s) in grid.iter().enumerate() { + total[i] += leg_scale * (s - e); + } + } + } + } + + Ok(total.into_pyarray(py)) +} + +/// Compute aggregate strategy payoff from Python leg objects. +/// +/// `legs` is expected to be a sequence of `PayoffLeg`-like objects +/// with attributes used by `ferro_ta.analysis.derivatives_payoff`. +#[pyfunction] +pub fn strategy_payoff_legs<'py>( + py: Python<'py>, + spot_grid: PyReadonlyArray1<'py, f64>, + legs: Bound<'py, PyTuple>, +) -> PyResult>> { + let grid = spot_grid.as_slice()?; + let mut total = vec![0.0_f64; grid.len()]; + + for leg in legs.iter() { + let instrument = parse_instrument_label(&leg_attr_string(&leg, "instrument")?)?; + let side_sign = parse_side_label(&leg_attr_string(&leg, "side")?)?.sign(); + let quantity = leg_attr_f64(&leg, "quantity")?; + let multiplier = leg_attr_f64(&leg, "multiplier")?; + let leg_scale = side_sign * quantity * multiplier; + + match instrument { + Instrument::Option => { + let otype_raw = + leg_attr_optional_string(&leg, "option_type")?.ok_or_else(|| { + PyValueError::new_err("Option payoff legs require option_type.") + })?; + let otype = parse_option_type_label(&otype_raw)?; + let strike = leg_attr_optional_f64(&leg, "strike")? + .ok_or_else(|| PyValueError::new_err("Option payoff legs require strike."))?; + let premium = leg_attr_f64(&leg, "premium")?; + + for (i, &s) in grid.iter().enumerate() { + let intrinsic = match otype { + OptionType::Call => (s - strike).max(0.0), + OptionType::Put => (strike - s).max(0.0), + }; + total[i] += leg_scale * (intrinsic - premium); + } + } + Instrument::Future => { + let entry_price = leg_attr_optional_f64(&leg, "entry_price")?.ok_or_else(|| { + PyValueError::new_err("Futures payoff legs require entry_price.") + })?; + for (i, &s) in grid.iter().enumerate() { + total[i] += leg_scale * (s - entry_price); + } + } + } + } + + Ok(total.into_pyarray(py)) +} + +/// Aggregate Greeks over multiple legs. +/// +/// Encodings match `strategy_payoff_dense`. +#[pyfunction] +#[allow(clippy::too_many_arguments)] +pub fn aggregate_greeks_dense( + spot: f64, + instruments: PyReadonlyArray1<'_, i64>, + sides: PyReadonlyArray1<'_, i64>, + option_types: PyReadonlyArray1<'_, i64>, + strikes: PyReadonlyArray1<'_, f64>, + volatilities: PyReadonlyArray1<'_, f64>, + time_to_expiries: PyReadonlyArray1<'_, f64>, + rates: PyReadonlyArray1<'_, f64>, + carries: PyReadonlyArray1<'_, f64>, + quantities: PyReadonlyArray1<'_, f64>, + multipliers: PyReadonlyArray1<'_, f64>, +) -> PyResult<(f64, f64, f64, f64, f64)> { + let inst = instruments.as_slice()?; + let side = sides.as_slice()?; + let opt_t = option_types.as_slice()?; + let strike = strikes.as_slice()?; + let vol = volatilities.as_slice()?; + let tte = time_to_expiries.as_slice()?; + let rate = rates.as_slice()?; + let carry = carries.as_slice()?; + let qty = quantities.as_slice()?; + let mult = multipliers.as_slice()?; + + let n_legs = inst.len(); + if side.len() != n_legs + || opt_t.len() != n_legs + || strike.len() != n_legs + || vol.len() != n_legs + || tte.len() != n_legs + || rate.len() != n_legs + || carry.len() != n_legs + || qty.len() != n_legs + || mult.len() != n_legs + { + return Err(PyValueError::new_err( + "All leg arrays must have the same length", + )); + } + + let mut delta = 0.0_f64; + let mut gamma = 0.0_f64; + let mut vega = 0.0_f64; + let mut theta = 0.0_f64; + let mut rho = 0.0_f64; + + for i in 0..n_legs { + let instrument = parse_instrument(inst[i])?; + let side_sign = parse_side(side[i])?.sign(); + let leg_scale = side_sign * qty[i] * mult[i]; + match instrument { + Instrument::Future => { + delta += leg_scale; + } + Instrument::Option => { + if vol[i].is_nan() || tte[i].is_nan() { + return Err(PyValueError::new_err( + "Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.", + )); + } + let kind = match parse_option_type(opt_t[i])? { + OptionType::Call => ferro_ta_core::options::OptionKind::Call, + OptionType::Put => ferro_ta_core::options::OptionKind::Put, + }; + let greeks = ferro_ta_core::options::greeks::model_greeks( + ferro_ta_core::options::OptionEvaluation { + contract: ferro_ta_core::options::OptionContract { + model: ferro_ta_core::options::PricingModel::BlackScholes, + underlying: spot, + strike: strike[i], + rate: rate[i], + carry: carry[i], + time_to_expiry: tte[i], + kind, + }, + volatility: vol[i], + }, + ); + delta += leg_scale * greeks.delta; + gamma += leg_scale * greeks.gamma; + vega += leg_scale * greeks.vega; + theta += leg_scale * greeks.theta; + rho += leg_scale * greeks.rho; + } + } + } + + Ok((delta, gamma, vega, theta, rho)) +} + +/// Aggregate Greeks from Python leg objects. +#[pyfunction] +pub fn aggregate_greeks_legs( + spot: f64, + legs: Bound<'_, PyTuple>, +) -> PyResult<(f64, f64, f64, f64, f64)> { + let mut delta = 0.0_f64; + let mut gamma = 0.0_f64; + let mut vega = 0.0_f64; + let mut theta = 0.0_f64; + let mut rho = 0.0_f64; + + for leg in legs.iter() { + let instrument = parse_instrument_label(&leg_attr_string(&leg, "instrument")?)?; + let side_sign = parse_side_label(&leg_attr_string(&leg, "side")?)?.sign(); + let quantity = leg_attr_f64(&leg, "quantity")?; + let multiplier = leg_attr_f64(&leg, "multiplier")?; + let leg_scale = side_sign * quantity * multiplier; + + match instrument { + Instrument::Future => { + delta += leg_scale; + } + Instrument::Option => { + let otype_raw = + leg_attr_optional_string(&leg, "option_type")?.ok_or_else(|| { + PyValueError::new_err( + "Option legs require option_type for Greeks aggregation.", + ) + })?; + let otype = parse_option_type_label(&otype_raw)?; + let strike = leg_attr_optional_f64(&leg, "strike")?.ok_or_else(|| { + PyValueError::new_err( + "Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.", + ) + })?; + let volatility = leg_attr_optional_f64(&leg, "volatility")?.ok_or_else(|| { + PyValueError::new_err( + "Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.", + ) + })?; + let time_to_expiry = + leg_attr_optional_f64(&leg, "time_to_expiry")?.ok_or_else(|| { + PyValueError::new_err( + "Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.", + ) + })?; + let rate = leg_attr_f64(&leg, "rate")?; + let carry = leg_attr_f64(&leg, "carry")?; + + let kind = match otype { + OptionType::Call => ferro_ta_core::options::OptionKind::Call, + OptionType::Put => ferro_ta_core::options::OptionKind::Put, + }; + let greeks = ferro_ta_core::options::greeks::model_greeks( + ferro_ta_core::options::OptionEvaluation { + contract: ferro_ta_core::options::OptionContract { + model: ferro_ta_core::options::PricingModel::BlackScholes, + underlying: spot, + strike, + rate, + carry, + time_to_expiry, + kind, + }, + volatility, + }, + ); + delta += leg_scale * greeks.delta; + gamma += leg_scale * greeks.gamma; + vega += leg_scale * greeks.vega; + theta += leg_scale * greeks.theta; + rho += leg_scale * greeks.rho; + } + } + } + + Ok((delta, gamma, vega, theta, rho)) +} diff --git a/src/portfolio/mod.rs b/src/portfolio/mod.rs index 59f782e..3ce3eba 100644 --- a/src/portfolio/mod.rs +++ b/src/portfolio/mod.rs @@ -350,6 +350,35 @@ pub fn spread<'py>( Ok(result.into_pyarray(py)) } +// --------------------------------------------------------------------------- +// ratio +// --------------------------------------------------------------------------- + +/// Compute the ratio between two series: A / B. +/// +/// Where B is 0, returns NaN. +#[pyfunction] +pub fn ratio<'py>( + py: Python<'py>, + a: PyReadonlyArray1<'py, f64>, + b: PyReadonlyArray1<'py, f64>, +) -> PyResult>> { + let av = a.as_slice()?; + let bv = b.as_slice()?; + let n = av.len(); + if n == 0 || bv.len() != n { + return Err(PyValueError::new_err( + "a and b must be non-empty and equal length", + )); + } + let result: Vec = av + .iter() + .zip(bv.iter()) + .map(|(&x, &y)| if y == 0.0 { f64::NAN } else { x / y }) + .collect(); + Ok(result.into_pyarray(py)) +} + // --------------------------------------------------------------------------- // zscore_series // --------------------------------------------------------------------------- @@ -449,6 +478,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(correlation_matrix, m)?)?; m.add_function(wrap_pyfunction!(relative_strength, m)?)?; m.add_function(wrap_pyfunction!(spread, m)?)?; + m.add_function(wrap_pyfunction!(ratio, m)?)?; m.add_function(wrap_pyfunction!(zscore_series, m)?)?; m.add_function(wrap_pyfunction!(compose_weighted, m)?)?; Ok(()) diff --git a/tests/integration/test_cross_surface_manifest.py b/tests/integration/test_cross_surface_manifest.py new file mode 100644 index 0000000..3443673 --- /dev/null +++ b/tests/integration/test_cross_surface_manifest.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import json +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[2] +SCRIPTS = ROOT / "scripts" +if str(ROOT / "python") not in sys.path: + sys.path.insert(0, str(ROOT / "python")) +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from build_api_manifest import build_manifest + + +def test_api_manifest_is_deterministic_and_current() -> None: + manifest_path = ROOT / "docs" / "api_manifest.json" + assert manifest_path.exists(), "docs/api_manifest.json is missing" + + expected = build_manifest(ROOT, include_runtime_metadata=False) + actual = json.loads(manifest_path.read_text(encoding="utf-8")) + + assert actual == expected diff --git a/tests/integration/test_wasm_node_conformance.py b/tests/integration/test_wasm_node_conformance.py new file mode 100644 index 0000000..d21f981 --- /dev/null +++ b/tests/integration/test_wasm_node_conformance.py @@ -0,0 +1,180 @@ +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) diff --git a/tests/unit/test_infrastructure.py b/tests/unit/test_infrastructure.py index 97910de..b0b3115 100644 --- a/tests/unit/test_infrastructure.py +++ b/tests/unit/test_infrastructure.py @@ -626,6 +626,13 @@ class TestBatchApply: with pytest.raises(ValueError, match="1-D or 2-D"): batch_apply(np.zeros((5, 5, 5)), SMA, timeperiod=3) + def test_sma_fastpath_matches_batch_sma(self): + from ferro_ta.data.batch import batch_sma + + fast = batch_apply(self.C2D, SMA, timeperiod=10) + direct = batch_sma(self.C2D, timeperiod=10) + assert np.allclose(fast, direct, equal_nan=True) + class TestBatchShapeValidation: def test_batch_atr_shape_mismatch_raises(self): diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 83f3a56..dd58d06 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -47,10 +47,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "ferro_ta_core" +version = "1.0.4" + [[package]] name = "ferro_ta_wasm" -version = "1.0.0" +version = "1.0.4" dependencies = [ + "ferro_ta_core", "js-sys", "wasm-bindgen", "wasm-bindgen-test", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index cdfe7df..e3c9f76 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] wasm-bindgen = "0.2" js-sys = "0.3" +ferro_ta_core = { path = "../crates/ferro_ta_core", default-features = false } [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/wasm/README.md b/wasm/README.md index 0deccd2..18c862c 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -11,7 +11,7 @@ npm install ferro-ta-wasm ``` ```javascript -const { sma, ema, rsi, bbands, atr, obv, macd } = require('ferro-ta-wasm'); +const { sma, ema, wma, rsi, adx, mfi, bbands, atr, obv, macd } = require('ferro-ta-wasm'); const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10]); const smaOut = sma(close, 3); @@ -28,20 +28,23 @@ console.log('SMA:', Array.from(smaOut)); |------------|---------------|----------------------------------------------------|---------| | Overlap | `sma` | `close: Float64Array, timeperiod: number` | `Float64Array` | | Overlap | `ema` | `close: Float64Array, timeperiod: number` | `Float64Array` | +| Overlap | `wma` | `close: Float64Array, timeperiod: number` | `Float64Array` | | Overlap | `bbands` | `close, timeperiod, nbdevup, nbdevdn` | `Array[upper, middle, lower]` | | Momentum | `rsi` | `close: Float64Array, timeperiod: number` | `Float64Array` | +| Momentum | `adx` | `high, low, close: Float64Array, timeperiod` | `Float64Array` | | Momentum | `macd` | `close, fastperiod, slowperiod, signalperiod` | `Array[macd, signal, hist]` | | Momentum | `mom` | `close: Float64Array, timeperiod: number` | `Float64Array` | | Momentum | `stochf` | `high, low, close, fastk_period, fastd_period` | `Array[fastk, fastd]` | | Volatility | `atr` | `high, low, close: Float64Array, timeperiod` | `Float64Array` | | Volume | `obv` | `close: Float64Array, volume: Float64Array` | `Float64Array` | +| Volume | `mfi` | `high, low, close, volume: Float64Array, timeperiod` | `Float64Array` | ### Adding more indicators -All implementations are self-contained in `src/lib.rs` — no external crate dependency needed. +WASM exports live in `src/lib.rs` and can either implement logic directly or delegate to `ferro_ta_core`. To add a new indicator: -1. Implement the algorithm in a `#[wasm_bindgen]` function in `src/lib.rs`. +1. Add a `#[wasm_bindgen]` export in `src/lib.rs` (prefer delegating to `ferro_ta_core` where possible). 2. Add at least two `#[wasm_bindgen_test]` tests covering output length and a known value. 3. Update this README table. 4. Run `wasm-pack test --node` to verify. @@ -79,7 +82,7 @@ wasm-pack build --target web --out-dir pkg-web ## Usage (Node.js) ```javascript -const { sma, ema, rsi, bbands, atr, obv, macd } = require('./pkg/ferro_ta_wasm.js'); +const { sma, ema, wma, rsi, adx, mfi, bbands, atr, obv, macd } = require('./pkg/ferro_ta_wasm.js'); const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10]); @@ -91,6 +94,10 @@ console.log('SMA:', Array.from(smaOut)); // [ NaN, NaN, 44.193, ... ] const rsiOut = rsi(close, 5); console.log('RSI:', Array.from(rsiOut)); +// WMA (period 5) +const wmaOut = wma(close, 5); +console.log('WMA:', Array.from(wmaOut)); + // Bollinger Bands (period 5, ±2σ) — returns [upper, middle, lower] const [upper, middle, lower] = bbands(close, 5, 2.0, 2.0); console.log('BBANDS upper:', Array.from(upper)); @@ -107,10 +114,18 @@ const low = new Float64Array([43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]); const atrOut = atr(high, low, close, 3); console.log('ATR:', Array.from(atrOut)); +// ADX (period 3) +const adxOut = adx(high, low, close, 3); +console.log('ADX:', Array.from(adxOut)); + // OBV const volume = new Float64Array([1000, 1200, 900, 1500, 800, 600, 700]); const obvOut = obv(close, volume); console.log('OBV:', Array.from(obvOut)); + +// MFI (period 3) +const mfiOut = mfi(high, low, close, volume, 3); +console.log('MFI:', Array.from(mfiOut)); ``` ## Usage (Browser) @@ -150,7 +165,7 @@ from source: ## Limitations -- Only 9 indicators are currently exposed (SMA, EMA, BBANDS, RSI, MACD, MOM, STOCHF, ATR, OBV). +- Only 12 indicators are currently exposed (SMA, EMA, WMA, BBANDS, RSI, ADX, MACD, MOM, STOCHF, ATR, OBV, MFI). Additional indicators will be added following the same pattern in `src/lib.rs`. - Large arrays (> 10M bars) may be slow due to JS↔WASM memory copies. For high-throughput use cases prefer the Python (PyO3) binding. diff --git a/wasm/bench.js b/wasm/bench.js index 1f52b48..59312a8 100644 --- a/wasm/bench.js +++ b/wasm/bench.js @@ -23,14 +23,16 @@ function makeSeries(length) { const close = new Float64Array(length); const high = new Float64Array(length); const low = new Float64Array(length); + const volume = new Float64Array(length); let value = 100.0; for (let idx = 0; idx < length; idx += 1) { value += Math.sin(idx / 13.0) * 0.35 + Math.cos(idx / 29.0) * 0.18; close[idx] = value; high[idx] = value + 1.25; low[idx] = value - 1.10; + volume[idx] = 1000.0 + Math.abs(Math.sin(idx / 7.0) * 300.0) + (idx % 100); } - return { close, high, low }; + return { close, high, low, volume }; } function timeMin(fn, rounds = 7) { @@ -45,11 +47,14 @@ function timeMin(fn, rounds = 7) { } function runBenchmark({ bars }) { - const { close, high, low } = makeSeries(bars); + const { close, high, low, volume } = makeSeries(bars); const cases = [ ["SMA", () => wasm.sma(close, 20)], ["EMA", () => wasm.ema(close, 20)], + ["WMA", () => wasm.wma(close, 20)], ["RSI", () => wasm.rsi(close, 14)], + ["ADX", () => wasm.adx(high, low, close, 14)], + ["MFI", () => wasm.mfi(high, low, close, volume, 14)], ["ATR", () => wasm.atr(high, low, close, 14)], ["BBANDS", () => wasm.bbands(close, 20, 2.0, 2.0)], ]; diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index a72167f..4d93e14 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -10,6 +10,7 @@ and `MACD`). ## Overlap Studies - [`sma`] — Simple Moving Average - [`ema`] — Exponential Moving Average +- [`wma`] — Weighted Moving Average - [`bbands`] — Bollinger Bands (returns `[upper, middle, lower]`) ## Momentum Indicators @@ -17,12 +18,14 @@ and `MACD`). - [`macd`] — Moving Average Convergence/Divergence (returns `[macd, signal, hist]`) - [`mom`] — Momentum (close[i] - close[i-period]) - [`stochf`] — Fast Stochastic (returns `[fastk, fastd]`) +- [`adx`] — Average Directional Movement Index ## Volatility Indicators - [`atr`] — Average True Range (Wilder smoothing) ## Volume Indicators - [`obv`] — On-Balance Volume +- [`mfi`] — Money Flow Index */ use js_sys::{Array, Float64Array}; @@ -325,6 +328,24 @@ pub fn obv(close: &Float64Array, volume: &Float64Array) -> Float64Array { from_vec(result) } +// --------------------------------------------------------------------------- +// WMA — Weighted Moving Average +// --------------------------------------------------------------------------- + +/// Weighted Moving Average. +/// +/// # Arguments +/// - `close` – `Float64Array` of close prices. +/// - `timeperiod` – look-back window (default 30, minimum 1). +/// +/// # Returns +/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`. +#[wasm_bindgen] +pub fn wma(close: &Float64Array, timeperiod: usize) -> Float64Array { + let prices = to_vec(close); + from_vec(ferro_ta_core::overlap::wma(&prices, timeperiod)) +} + // --------------------------------------------------------------------------- // MOM — Momentum // --------------------------------------------------------------------------- @@ -433,6 +454,70 @@ pub fn stochf( out } +// --------------------------------------------------------------------------- +// ADX — Average Directional Movement Index +// --------------------------------------------------------------------------- + +/// Average Directional Movement Index (Wilder smoothing). +/// +/// # Arguments +/// - `high` – `Float64Array` of high prices. +/// - `low` – `Float64Array` of low prices. +/// - `close` – `Float64Array` of close prices. +/// - `timeperiod` – look-back period (default 14, minimum 1). +/// +/// # Returns +/// `Float64Array`; warm-up values are `NaN`. +#[wasm_bindgen] +pub fn adx( + high: &Float64Array, + low: &Float64Array, + close: &Float64Array, + timeperiod: usize, +) -> Float64Array { + let h = to_vec(high); + let l = to_vec(low); + let c = to_vec(close); + if h.len() != l.len() || h.len() != c.len() { + return from_vec(vec![f64::NAN; c.len()]); + } + from_vec(ferro_ta_core::momentum::adx(&h, &l, &c, timeperiod)) +} + +// --------------------------------------------------------------------------- +// MFI — Money Flow Index +// --------------------------------------------------------------------------- + +/// Money Flow Index. +/// +/// # Arguments +/// - `high` – `Float64Array` of high prices. +/// - `low` – `Float64Array` of low prices. +/// - `close` – `Float64Array` of close prices. +/// - `volume` – `Float64Array` of volume values. +/// - `timeperiod` – look-back period (default 14, minimum 1). +/// +/// # Returns +/// `Float64Array`; warm-up values are `NaN`. +#[wasm_bindgen] +pub fn mfi( + high: &Float64Array, + low: &Float64Array, + close: &Float64Array, + volume: &Float64Array, + timeperiod: usize, +) -> Float64Array { + let h = to_vec(high); + let l = to_vec(low); + let c = to_vec(close); + let v = to_vec(volume); + let n = c.len(); + if h.len() != n || l.len() != n || v.len() != n { + return from_vec(vec![f64::NAN; n]); + } + from_vec(ferro_ta_core::volume::mfi(&h, &l, &c, &v, timeperiod)) +} + // --------------------------------------------------------------------------- // MACD — Moving Average Convergence/Divergence // --------------------------------------------------------------------------- @@ -861,4 +946,77 @@ mod tests { assert!(v >= 0.0 && v <= 100.0, "fastk value {v} out of [0, 100]"); } } + + // ----------------------------------------------------------------------- + // WMA tests + // ----------------------------------------------------------------------- + + #[wasm_bindgen_test] + fn test_wma_output_length() { + let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]); + let out = wma(&close, 3); + assert_eq!(out.length(), 5); + } + + #[wasm_bindgen_test] + fn test_wma_known_value() { + // WMA(3) at index 2 = (1*1 + 2*2 + 3*3) / 6 = 14/6 + let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]); + let out = wma(&close, 3); + let mut vals = vec![0.0f64; 5]; + out.copy_to(&mut vals); + assert!(vals[0].is_nan()); + assert!(vals[1].is_nan()); + assert!((vals[2] - (14.0 / 6.0)).abs() < 1e-10); + } + + // ----------------------------------------------------------------------- + // ADX tests + // ----------------------------------------------------------------------- + + #[wasm_bindgen_test] + fn test_adx_output_length() { + let h = make_arr(&[10.0, 11.0, 12.0, 13.0, 13.5, 14.0, 14.5, 15.0]); + let l = make_arr(&[9.0, 9.5, 10.5, 11.5, 12.0, 12.5, 13.0, 13.5]); + let c = make_arr(&[9.5, 10.5, 11.5, 12.0, 13.0, 13.5, 14.0, 14.5]); + let out = adx(&h, &l, &c, 3); + assert_eq!(out.length(), 8); + } + + #[wasm_bindgen_test] + fn test_adx_values_in_range() { + let h = make_arr(&[10.0, 11.0, 12.0, 13.0, 13.5, 14.0, 14.5, 15.0]); + let l = make_arr(&[9.0, 9.5, 10.5, 11.5, 12.0, 12.5, 13.0, 13.5]); + let c = make_arr(&[9.5, 10.5, 11.5, 12.0, 13.0, 13.5, 14.0, 14.5]); + let out = adx(&h, &l, &c, 3); + for v in get_finite(&out) { + assert!((0.0..=100.0).contains(&v), "ADX out of range: {v}"); + } + } + + // ----------------------------------------------------------------------- + // MFI tests + // ----------------------------------------------------------------------- + + #[wasm_bindgen_test] + fn test_mfi_output_length() { + let h = make_arr(&[10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 13.5]); + let l = make_arr(&[9.0, 9.5, 10.5, 10.0, 11.0, 11.5, 12.0]); + let c = make_arr(&[9.5, 10.5, 11.5, 11.0, 12.0, 12.5, 13.0]); + let v = make_arr(&[100.0, 110.0, 120.0, 130.0, 125.0, 140.0, 150.0]); + let out = mfi(&h, &l, &c, &v, 3); + assert_eq!(out.length(), 7); + } + + #[wasm_bindgen_test] + fn test_mfi_values_in_range() { + let h = make_arr(&[10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 13.5]); + let l = make_arr(&[9.0, 9.5, 10.5, 10.0, 11.0, 11.5, 12.0]); + let c = make_arr(&[9.5, 10.5, 11.5, 11.0, 12.0, 12.5, 13.0]); + let v = make_arr(&[100.0, 110.0, 120.0, 130.0, 125.0, 140.0, 150.0]); + let out = mfi(&h, &l, &c, &v, 3); + for val in get_finite(&out) { + assert!((0.0..=100.0).contains(&val), "MFI out of range: {val}"); + } + } }