Files
2026-03-23 23:34:28 +05:30

200 lines
5.4 KiB
Python

"""
Plugin / Extension Registry
============================
A lightweight registry that allows users to register custom indicators and
call any indicator (built-in or custom) by name.
Usage
-----
>>> import numpy as np
>>> import ferro_ta
>>> from ferro_ta.core.registry import register, run, get, list_indicators
>>>
>>> # Call a built-in indicator by name
>>> close = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])
>>> result = run("SMA", close, timeperiod=3)
>>>
>>> # Register a custom indicator
>>> def MY_IND(close, timeperiod=10):
... \"\"\"Custom indicator: simple sum / timeperiod.\"\"\"
... import numpy as np
... out = np.full_like(close, np.nan)
... for i in range(timeperiod - 1, len(close)):
... out[i] = close[i - timeperiod + 1 : i + 1].sum() / timeperiod
... return out
>>> register("MY_IND", MY_IND)
>>> result = run("MY_IND", close, timeperiod=3)
Writing a plugin
----------------
A plugin function must:
1. Accept at least one positional array argument (``close``, ``high``, etc.).
2. Accept keyword arguments for parameters (e.g. ``timeperiod=14``).
3. Return a single ``numpy.ndarray`` *or* a tuple of ``numpy.ndarray`` for
multi-output indicators.
Example::
def DOUBLE_RSI(close, timeperiod=14, smooth=3):
import ferro_ta
rsi = ferro_ta.RSI(close, timeperiod=timeperiod)
return ferro_ta.SMA(rsi, timeperiod=smooth)
from ferro_ta.core.registry import register
register("DOUBLE_RSI", DOUBLE_RSI)
API
---
register(name, func) — Register *func* under *name*.
unregister(name) — Remove a registered indicator.
get(name) — Return the callable for *name*.
run(name, *args, **kw) — Look up *name* and call it with *args* / **kw*.
list_indicators() — Return a sorted list of all registered names.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from ferro_ta.core.exceptions import FerroTAError
class FerroTARegistryError(FerroTAError):
"""Raised when a registry lookup fails (unknown indicator name)."""
# ---------------------------------------------------------------------------
# Internal registry (module-level singleton dict)
# ---------------------------------------------------------------------------
_REGISTRY: dict[str, Callable[..., Any]] = {}
def register(name: str, func: Callable[..., Any]) -> None:
"""Register a callable under *name*.
Parameters
----------
name:
Indicator name (case-sensitive; convention is ALL_CAPS for
compatibility with TA-Lib naming).
func:
A callable that accepts at least one array-like positional argument
and optional keyword arguments, and returns a ``numpy.ndarray`` or a
tuple of ``numpy.ndarray``.
Raises
------
TypeError
If *func* is not callable.
"""
if not callable(func):
raise TypeError(f"Expected a callable for '{name}', got {type(func).__name__}")
_REGISTRY[name] = func
def unregister(name: str) -> None:
"""Remove the indicator registered under *name*.
Parameters
----------
name:
Indicator name to remove.
Raises
------
FerroTARegistryError
If *name* is not in the registry.
"""
if name not in _REGISTRY:
raise FerroTARegistryError(
f"Indicator '{name}' is not registered. "
f"Available indicators: {sorted(_REGISTRY)[:10]}…"
)
del _REGISTRY[name]
def get(name: str) -> Callable[..., Any]:
"""Return the callable registered under *name*.
Parameters
----------
name:
Indicator name (case-sensitive).
Returns
-------
Callable
The registered function.
Raises
------
FerroTARegistryError
If *name* is not in the registry.
"""
if name not in _REGISTRY:
raise FerroTARegistryError(
f"Unknown indicator '{name}'. "
f"Use list_indicators() to see all registered names."
)
return _REGISTRY[name]
def run(name: str, *args: Any, **kwargs: Any) -> Any:
"""Look up *name* in the registry and call it with *args* / *kwargs*.
Parameters
----------
name:
Indicator name (case-sensitive).
*args:
Positional arguments forwarded to the indicator function.
**kwargs:
Keyword arguments forwarded to the indicator function.
Returns
-------
numpy.ndarray or tuple of numpy.ndarray
Whatever the indicator function returns.
Raises
------
FerroTARegistryError
If *name* is not in the registry.
"""
func = get(name)
return func(*args, **kwargs)
def list_indicators() -> list[str]:
"""Return a sorted list of all registered indicator names.
Returns
-------
list of str
Sorted list of indicator names.
"""
return sorted(_REGISTRY)
# ---------------------------------------------------------------------------
# Auto-register all built-in indicators from ferro_ta.__all__
# ---------------------------------------------------------------------------
def _register_builtins() -> None:
"""Register every built-in indicator from ``ferro_ta.__all__``."""
# Lazy import to avoid circular imports at module load time
import ferro_ta # noqa: PLC0415
for _name in ferro_ta.__all__: # type: ignore[attr-defined]
_fn = getattr(ferro_ta, _name, None)
if callable(_fn):
_REGISTRY[_name] = _fn
_register_builtins()