Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/najmulhossainnj/Hedge-fund-backend/llms.txt

Use this file to discover all available pages before exploring further.

The Hedge Fund Backend is built around a first-class plugin architecture that makes extending the platform a matter of dropping a single Python module into the right package. Every pluggable component — feature transforms, ML models, signal generators, and backtest engines — implements a thin abstract interface and registers itself with @registry.register("plugin.key") at import time. The core engine code never needs to be modified; as long as the new module is imported in the relevant package’s __init__.py, the framework discovers and routes to it automatically.

Plugin Categories

Feature Plugins

Transform raw OHLCV and alternative data into named feature columns. Registered in app/plugins/features/ and surfaced at GET /features/plugins/available.

Model Plugins

Trainable predictive models: tree ensembles, neural networks, statistical models. Registered in app/plugins/models/ and surfaced at GET /models/plugins/available.

Signal Plugins

Convert model predictions into actionable trade signals (BUY / SELL / HOLD or +1 / 0 / -1). Registered in app/plugins/signals/ and surfaced at GET /signals/plugins/available.

Backtest Engine Plugins

Adapters over concrete backtesting libraries (vectorbt, Backtrader). Registered in app/plugins/engines/. Each engine receives prices and signals and returns a metrics dictionary.

Base Interfaces

All plugins inherit from abstract base classes defined in app/plugins/base.py. You must implement every @abstractmethod; optional methods (save, load) have default raise NotImplementedError bodies.

BaseFeature

from abc import ABC, abstractmethod
from typing import Any
import pandas as pd

class BaseFeature(ABC):
    key: str  # unique plugin identifier, e.g. "technical.rsi"

    def __init__(self, **params: Any):
        self.params = params

    @abstractmethod
    def compute(self, data: pd.DataFrame) -> pd.DataFrame:
        """Return a DataFrame of one or more derived feature columns."""
        raise NotImplementedError
data is always a standard OHLCV DataFrame with columns open, high, low, close, volume. The returned DataFrame’s index must align with data.index.

BaseModel

class BaseModel(ABC):
    key: str  # e.g. "ml.xgboost"

    def __init__(self, **params: Any):
        self.params = params

    @abstractmethod
    def train(self, X: pd.DataFrame, y: pd.Series) -> None: ...

    @abstractmethod
    def predict(self, X: pd.DataFrame) -> pd.Series: ...

    def save(self, path: str) -> None: ...

    def load(self, path: str) -> None: ...

BaseSignalGenerator

class BaseSignalGenerator(ABC):
    key: str  # e.g. "signal.threshold"

    def __init__(self, **params: Any):
        self.params = params

    @abstractmethod
    def generate(self, predictions: pd.DataFrame) -> pd.DataFrame:
        """Return a DataFrame with a `signal` column (BUY/SELL/HOLD or +1/0/-1)."""
        raise NotImplementedError

BaseBacktestEngine

class BaseBacktestEngine(ABC):
    key: str  # e.g. "engine.vectorbt"

    def __init__(self, **config: Any):
        self.config = config

    @abstractmethod
    def run(self, prices: pd.DataFrame, signals: pd.DataFrame) -> dict:
        """Execute the backtest and return trades, equity_curve, and metrics."""
        raise NotImplementedError

Writing a Feature Plugin

The following example creates a new technical indicator — an Exponential Moving Average crossover — and registers it as technical.ema_cross. The implementation follows the exact same pattern as the built-in RSIFeature and ATRFeature plugins.
# app/plugins/features/ema_cross.py
import pandas as pd

from app.plugins.base import BaseFeature
from app.plugins.features import feature_registry


@feature_registry.register("technical.ema_cross")
class EMACrossFeature(BaseFeature):
    """
    Exponential Moving Average crossover signal.

    Returns two columns:
      ema_fast  — fast EMA of the close price
      ema_slow  — slow EMA of the close price
      ema_cross — +1 when fast > slow (bullish), -1 when fast < slow (bearish)

    params:
      fast  (int, default 12): Fast EMA period
      slow  (int, default 26): Slow EMA period
    """

    def compute(self, data: pd.DataFrame) -> pd.DataFrame:
        fast = self.params.get("fast", 12)
        slow = self.params.get("slow", 26)

        close = data["close"]
        ema_fast = close.ewm(span=fast, adjust=False).mean()
        ema_slow = close.ewm(span=slow, adjust=False).mean()
        cross = (ema_fast > ema_slow).map({True: 1, False: -1})

        return pd.DataFrame(
            {"ema_fast": ema_fast, "ema_slow": ema_slow, "ema_cross": cross},
            index=data.index,
        )
Then add a single import to app/plugins/features/__init__.py so the class self-registers when the package is loaded:
# app/plugins/features/__init__.py  (add one line)
from app.plugins.features import ema_cross  # noqa: F401

Writing a Model Plugin

The following example wraps scikit-learn’s Ridge regressor as a plugin, demonstrating the save/load contract using joblib:
# app/plugins/models/ridge_model.py
import pandas as pd

from app.plugins.base import BaseModel
from app.plugins.models import model_registry


@model_registry.register("ml.ridge")
class RidgeModel(BaseModel):
    """
    Ridge (L2-regularised) linear regression model.

    params:
      alpha (float, default 1.0): Regularisation strength
    """

    def __init__(self, **params):
        super().__init__(**params)
        self._model = None

    def train(self, X: pd.DataFrame, y: pd.Series) -> None:
        from sklearn.linear_model import Ridge

        self._model = Ridge(alpha=self.params.get("alpha", 1.0))
        self._model.fit(X, y)

    def predict(self, X: pd.DataFrame) -> pd.Series:
        if self._model is None:
            raise RuntimeError("Model has not been trained or loaded yet")
        return pd.Series(self._model.predict(X), index=X.index)

    def save(self, path: str) -> None:
        import joblib

        if self._model is None:
            raise RuntimeError("Nothing to save — model not trained")
        joblib.dump(self._model, path)

    def load(self, path: str) -> None:
        import joblib

        self._model = joblib.load(path)
Register it in app/plugins/models/__init__.py:
from app.plugins.models import ridge_model  # noqa: F401

Registering Plugins

The PluginRegistry class lives in app/plugins/registry.py and is a generic, typed container:
class PluginRegistry(Generic[PluginT]):
    def register(self, key: str) -> Callable[[Type[PluginT]], Type[PluginT]]:
        """Decorator — call at class definition time to add the plugin."""
        ...

    def get(self, key: str) -> Type[PluginT]:
        """Retrieve a plugin class by its key; raises KeyError if not found."""
        ...

    def list_keys(self) -> list[str]:
        """Return sorted list of all registered plugin keys."""
        ...

    def create(self, key: str, **params) -> PluginT:
        """Instantiate a plugin by key, forwarding params to __init__."""
        ...
Each plugin package’s __init__.py creates a singleton registry and imports all plugin modules immediately after:
# app/plugins/features/__init__.py
from app.plugins.base import BaseFeature
from app.plugins.registry import PluginRegistry

feature_registry: PluginRegistry[BaseFeature] = PluginRegistry("feature")

from app.plugins.features import (  # noqa: E402, F401
    automated,
    news_sentiment,
    statistical,
    technical,
)
When Python imports app.plugins.features, the @feature_registry.register(...) decorators on each class body fire immediately, populating the registry before any route handler runs. The same pattern applies to models and signals:
# app/plugins/models/__init__.py
model_registry: PluginRegistry[BaseModel] = PluginRegistry("model")

from app.plugins.models import (  # noqa: E402, F401
    catboost_model, lightgbm_model, lstm_model,
    random_forest_model, xgboost_model,
)
# app/plugins/signals/__init__.py
signal_registry: PluginRegistry[BaseSignalGenerator] = PluginRegistry("signal")

from app.plugins.signals import (  # noqa: E402, F401
    long_short, portfolio, ranking, threshold,
)

Built-in Plugins

CategoryPlugin keyDescription
Featuretechnical.rsiRelative Strength Index (Wilder’s smoothing, default period 14)
Featuretechnical.atrAverage True Range (default period 14)
Featurestatistical.momentumRate-of-change over a rolling window (pct_change(window))
Featurestatistical.mean_reversionDistance from rolling mean in rolling std units
Featurestatistical.z_scoreRolling z-score of any price column
Featurestatistical.hurst_exponentHurst exponent via R/S analysis — detects trending vs mean-reverting regimes
Featurestatistical.volatility_regimeClassifies rolling realised vol into low / mid / high regime labels
Featureautomated.tsfreshtsfresh automated feature extraction from rolling price windows (minimal/efficient/comprehensive feature sets)
Featurenews.finbert_sentimentFull FinBERT sentiment feature set: positive, negative, neutral, uncertainty scores and article volume
Featurenews.sentiment_momentumRolling mean of composite (positive − negative) FinBERT sentiment score
Featurenews.sentiment_divergenceSigned sentiment spread (positive − negative), optionally z-score normalised
Modelml.xgboostXGBoost gradient-boosted trees (XGBRegressor)
Modelml.lightgbmLightGBM gradient-boosted trees (LGBMRegressor)
Modelml.catboostCatBoost gradient-boosted trees
Modelml.random_forestScikit-learn RandomForestRegressor
Modeldl.lstmPyTorch LSTM sequence model
Signalsignal.thresholdEmits BUY/SELL/HOLD based on upper and lower prediction thresholds
Signalsignal.long_shortLong-only, short-only, or long-short signal generation
Signalsignal.rankingCross-sectional ranking of predictions across a universe
Signalsignal.portfolioPortfolio-level signal aggregation with position sizing
EnginevectorbtVectorBT vectorised backtesting engine
EnginebacktraderBacktrader event-driven backtesting engine

Discovery Endpoints

The API exposes read-only endpoints that return the live list of registered plugin keys. These are populated at runtime from the registry singletons, so newly added plugins appear immediately without redeployment (as long as the module is imported):
# List all registered feature plugins
GET /api/v1/features/plugins/available

# List all registered model plugins
GET /api/v1/models/plugins/available

# List all registered signal generator plugins
GET /api/v1/signals/plugins/available
Example response for /models/plugins/available:
{
  "plugins": [
    "ml.catboost",
    "ml.lightgbm",
    "ml.lstm",
    "ml.random_forest",
    "ml.xgboost"
  ]
}
Plugin keys must be globally unique within each registry. Attempting to register the same key twice raises a ValueError at startup:
ValueError: Plugin key 'ml.xgboost' already registered in model
Use a namespaced convention such as vendor.category.name (e.g. acme.technical.macd_histogram) to avoid collisions with built-in keys or other third-party plugins.

Build docs developers (and LLMs) love