Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/deskiziarecords/QUIMERIA-HYPERION/llms.txt

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

The plugin layer runs after all 18 core SMK modules on every bar. Plugins append extra sensor rows to r['sensors'] and store their full telemetry in r['plugins']. The base class, registry, and lifecycle are designed to keep the server stable — a failing plugin never crashes the pipeline.

How it works

On every call to SMKPipeline.step(), after all internal detectors have run, the PluginManager iterates over the plugin registry. Each enabled plugin receives the current bar dict, a rolling window DataFrame, and the partially assembled SMK result. The plugin’s update() method returns a flat telemetry dict; the manager appends a sensor row derived from that dict to r['sensors'].

Creating a plugin

1

Create the plugin file

Create a new Python file in backend/plugins/. The filename becomes the module path used in the registry.
backend/plugins/my_plugin.py
2

Extend SMKPlugin

Import and subclass SMKPlugin from plugins (the backend/plugins/__init__.py package). Set the required class attributes and implement update().
# backend/plugins/my_plugin.py
from __future__ import annotations
import pandas as pd
from plugins import SMKPlugin


class MyPlugin(SMKPlugin):
    name            = "MyPlugin"        # shown in the frontend sensor bar
    layer           = "L3-ext"          # L1, L2, L3, L4, L3-ext, L2-ext, EXE
    sensor_id       = "p07"             # unique ID shown in the sensor panel
    requires_warmup = 30                # bars before update() is called

    def update(self, bar: dict, df: pd.DataFrame, smk: dict) -> dict:
        """
        Called once per bar after the warmup period.

        bar  — current OHLCV dict: {time, open, high, low, close, volume}
        df   — rolling window DataFrame, up to 60 bars
        smk  — SMK result dict so far (read-only); earlier layers are complete
        """
        close = df["close"].values
        delta = float(close[-1] - close[-2]) if len(close) > 1 else 0.0
        active = abs(delta) > 0.0005   # threshold: 5 pips

        return {
            "active": active,          # bool — required
            "score":  min(abs(delta) / 0.001, 1.0),  # float 0–1 — required
            "status": "ACTIVE" if active else "IDLE", # str — required
            "delta":  round(delta, 6), # any extra plain-Python fields
        }
3

Register in PLUGIN_REGISTRY

Add a (module_path, class_name) tuple to the PLUGIN_REGISTRY list in backend/plugin_manager.py.
# backend/plugin_manager.py
PLUGIN_REGISTRY = [
    ("plugins.market_rhythm",    "MarketRhythmPlugin"),
    ("plugins.market_heuristic", "MarketHeuristicPlugin"),
    # ... existing plugins ...
    ("plugins.my_plugin",        "MyPlugin"),   # add this line
]
4

Restart the server

The plugin manager calls load_all() at startup. Stop and restart the uvicorn process to pick up the new plugin.
cd backend
python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload
Verify the plugin loaded:
curl http://localhost:8000/api/plugins

The SMKPlugin base class

# backend/plugins/__init__.py

class SMKPlugin(ABC):
    name:            str  = "unnamed"
    layer:           str  = "L3-ext"   # L1 | L2 | L3 | L4 | λ-ext | EXE
    sensor_id:       str  = "s99"      # shown in frontend sensor bar
    enabled:         bool = True
    requires_warmup: int  = 20         # minimum bars before firing

    def on_bar(self, bar: dict, df: pd.DataFrame, smk: dict) -> dict:
        """Called by plugin_manager on every bar. Handles warmup internally."""
        self._bar_count += 1
        if self._bar_count < self.requires_warmup:
            return {"status": "WARMUP", "active": False, "score": 0.0}
        self._ready = True
        try:
            return self.update(bar, df, smk)
        except Exception as e:
            return {"status": f"ERROR:{e}", "active": False, "score": 0.0}

    @abstractmethod
    def update(self, bar: dict, df: pd.DataFrame, smk: dict) -> dict:
        """Implement this method. Return a flat dict of plain Python types."""
        ...

    def reset(self):
        """Called when the pipeline reloads data. Resets warmup counter."""
        self._bar_count = 0
        self._ready     = False

Required return fields

Every dict returned by update() must include these three keys:
KeyTypeDescription
activeboolWhether the plugin detected its condition this bar
scorefloatNormalized score in the range 0.0–1.0
statusstrShort status string shown in the frontend tooltip
Additional keys are permitted and will be stored in r['plugins'][plugin.name] for downstream use.
All values returned from update() must be plain Python types: bool, int, float, str, list, or dict. Never return numpy scalars (np.float64, np.bool_, etc.) or numpy arrays. The WebSocket serializer will raise a TypeError on any unwrapped numpy type.To convert: use float(np_value), int(np_value), bool(np_value), or .tolist() on arrays.

Accessing the SMK result

The smk argument passed to update() is the partially assembled step result. Layers that have already run are available; layers that run after the plugin are not. The plugin layer runs after all 18 core detectors, so the full result including veto, fusion, and mandra is available.
def update(self, bar: dict, df: pd.DataFrame, smk: dict) -> dict:
    # Read the veto decision
    trade_allowed = smk.get("veto", {}).get("trade_allowed", True)

    # Read the AMD state
    amd_state = smk.get("amd", {}).get("state", "Accumulation")

    # Read the fusion confidence
    confidence = smk.get("fusion", {}).get("confidence", 0.0)

    return {
        "active": trade_allowed and confidence > 0.6,
        "score":  confidence,
        "status": f"AMD:{amd_state}",
    }

Complete minimal example

# backend/plugins/momentum_plugin.py
"""
Simple momentum plugin — detects when the last bar body exceeds 1.5× ATR.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from plugins import SMKPlugin


class MomentumPlugin(SMKPlugin):
    name            = "Momentum"
    layer           = "L3-ext"
    sensor_id       = "p08"
    requires_warmup = 20

    def update(self, bar: dict, df: pd.DataFrame, smk: dict) -> dict:
        closes = df["close"].values
        highs  = df["high"].values
        lows   = df["low"].values

        # ATR (14-period)
        atr_window = min(14, len(closes) - 1)
        tr = np.maximum(
            highs[1:] - lows[1:],
            np.maximum(
                np.abs(highs[1:] - closes[:-1]),
                np.abs(lows[1:]  - closes[:-1]),
            ),
        )
        atr = float(np.mean(tr[-atr_window:]))

        # Current bar body
        body = abs(float(closes[-1]) - float(bar["open"]))
        ratio = body / max(atr, 1e-9)
        active = ratio > 1.5

        return {
            "active":     active,
            "score":      float(min(ratio / 3.0, 1.0)),
            "status":     f"BURST:{ratio:.1f}x" if active else "CALM",
            "body_ratio": round(ratio, 3),
            "atr":        round(atr, 6),
        }

Auto-discovery behaviour

The PluginManager singleton is instantiated once at server startup via get_plugin_manager(). It calls load_all(), which iterates PLUGIN_REGISTRY in order. If a plugin fails to import (missing dependency, syntax error), the error is recorded in mgr.load_errors and the plugin is skipped — the server continues normally. Check plugin load errors with:
curl http://localhost:8000/api/plugins
# Look at the "errors" key in the response

Build docs developers (and LLMs) love