Skip to content

Integrations

riskkit is framework-agnostic, so it slots into whatever you already use.

backtesting.py

The first-class way is the RiskkitStrategy adapter: subclass it, write your signals in next() as usual, and call risk_long() / risk_short() instead of self.buy / self.sell. Every entry is then sized and validated by your one RiskConfig — and closed trades are fed back to the session manager, so streaks, cooldowns, and drawdown halts are live through the backtest.

pip install "riskkit[backtesting]"
from riskkit import RiskConfig
from riskkit.adapters.backtesting import RiskkitStrategy
from backtesting.lib import crossover
from backtesting.test import SMA

class SmaCross(RiskkitStrategy):
    risk_config = RiskConfig(base_risk_pct=2.0, max_notional_pct=15.0,
                             drawdown=dict(tier1_pct=3, halt_pct=12))

    def init(self):
        self.fast = self.I(SMA, self.data.Close, 10)
        self.slow = self.I(SMA, self.data.Close, 30)

    def next(self):
        price = self.data.Close[-1]
        if crossover(self.fast, self.slow) and not self.position:
            self.risk_long(stop_price=price * 0.97, target_price=price * 1.06)
        elif crossover(self.slow, self.fast) and self.position:
            self.position.close()

The full runnable demo is examples/backtesting_riskmanager.py (on the bundled GOOG data the risk layer holds max drawdown to ~1.5% while staying net positive). Prefer to wire a single component yourself? examples/backtesting_py_strategy.py drives sizing straight from PositionSizer. The signal is a plain SMA crossover in both — the point is that the risk layer is identical no matter what you swap in.

freqtrade

FreqtradeRiskManager adapts riskkit to freqtrade's callbacks — it imports nothing from freqtrade, so you compose it into your own IStrategy:

from riskkit import RiskConfig
from riskkit.adapters.freqtrade import FreqtradeRiskManager

class MyStrategy(IStrategy):
    def bot_start(self):
        self.risk = FreqtradeRiskManager(RiskConfig.balanced())

    def custom_stake_amount(self, pair, current_time, current_rate,
                            proposed_stake, min_stake, max_stake,
                            leverage, entry_tag, side, **kwargs):
        info = self.custom_info[pair]
        return self.risk.stake_amount(
            pair=pair, side=side,
            equity=self.wallets.get_total_stake_amount(),
            current_rate=current_rate, stop_price=info["stop_price"],
            max_stake=max_stake, min_stake=min_stake,
            score=info.get("score", 100), now=current_time,
        )

    def confirm_trade_entry(self, pair, *args, **kwargs):
        allowed = self.risk.confirm_entry(pair)
        if allowed:
            self.risk.on_fill(pair)
        return allowed

A returned stake of 0.0 makes freqtrade skip the entry, so sizing and the veto can both live in custom_stake_amount. Call on_fill() / on_exit() so cross-pair correlation, exposure, and session state stay current. Full snippet: examples/freqtrade_callbacks.py.

vectorbt

vectorbt is vectorized, so riskkit slots in at the sizing step: turn entry signals into a size array with size_signals, then pass it to Portfolio.from_signals:

from riskkit.adapters.vectorbt import size_signals

sizes = size_signals(
    equity=10_000,
    entry_prices=close.where(entries),     # price where entering, else NaN
    stop_prices=close * 0.97,
    atr=atr, atr_baseline=atr.rolling(100).mean(),
)
pf = vbt.Portfolio.from_signals(close, entries, exits,
                                size=sizes, size_type="value")

The stateful guards (drawdown halting, session caps) don't vectorize — step through bars with the RiskManager for those. Runnable demo: examples/vectorbt_sizing.py.

Your own loop

Nothing about riskkit assumes a framework. The examples/pipeline.py walkthrough wires drawdown posture → sizing → validation into a single decide_trade() function you can call from any event loop.