edgekit.strategy
Small, illustrative strategy templates expressed against the causal Strategy interface. Each subclasses BaseStrategy: prepare builds lagged indicator arrays, entry/exit make per-bar decisions, and .backtest(bars) runs the R-multiple bar loop. Bring your own strategy by subclassing the base.
What's inside. The abstract base BaseStrategy, plus two concrete templates: ORB (an intraday opening-range breakout) and SmaCross (a fast/slow moving-average crossover). Both are plain textbook demos built on the same interface.
BaseStrategy (ABC)#
The abstract base implementing the engine's Strategy protocol. Subclasses set a name and implement three methods; they inherit the one-line backtest wiring.
prepare(self, bars) -> dict # precompute causal indicator arrays (the P dict)
entry(self, bars, P, i) -> EntryIntent | None # entry decision on a flat bar i
exit(self, bars, P, pos, i) -> float | None # exit price on an in-position bar iThe inherited convenience method runs the strategy through run_bar_loop and returns the canonical trade DataFrame priced in R:
backtest(self, bars, cost: CostModel | None = None, warmup: int = 210,
bars_per_day: float = 6.0) -> pd.DataFrame| Param | Type | Default | Meaning |
|---|---|---|---|
bars | pd.DataFrame | — | OHLC frame (tz-naive UTC DatetimeIndex, float open/high/low/close). |
cost | CostModel | None | None | Cost model; default = the library convention (12 bps round-trip + 2 bps/day). |
warmup | int | 210 | Bars to skip before trading (indicator warmup). |
bars_per_day | float | 6.0 | Converts hold-in-bars to days for the swap cost (H4 = 6/day). |
Returns the canonical trade frame: one row per closed trade with net R in r, exit date in date, plus dir, bars_held, entry, exit, stop_dist, tag, exit_reason.
ORB#
Opening-range breakout — a classic intraday breakout, as a plain template.
ORB(or_bars: int = 30, target_r: float = 2.0)| Param | Default | Meaning |
|---|---|---|
or_bars | 30 | Bars in the opening range (its width = 1R). |
target_r | 2.0 | Flatten at target_r × risk (or at end of session). |
Each session's first or_bars bars define an opening range; a break of its high goes long / its low goes short, with the stop at the opposite edge (range width = 1R). One trade per day, flattened at the session end.
import edgekit as ek
from edgekit.strategy import ORB
rth = bars[ek.data.rth_mask(bars.index, start="09:30", end="16:00", tz="America/New_York")]
trades = ORB(or_bars=30, target_r=2.0).backtest(rth, warmup=5, bars_per_day=390)warmup and a bars_per_day matching your bar size. This is the raw, unfiltered breakout skeleton — a demonstration, not a tuned edge.SmaCross#
A vanilla fast/slow moving-average crossover with an ATR stop — the textbook trend-following template.
SmaCross(fast: int = 20, slow: int = 100, atr_n: int = 20, stop_mult: float = 2.0)| Param | Default | Meaning |
|---|---|---|
fast | 20 | Fast SMA period. |
slow | 100 | Slow SMA period. |
atr_n | 20 | ATR period for the stop distance. |
stop_mult | 2.0 | Hard stop distance in ATRs (the R denominator). |
Go long when the fast SMA crosses above the slow SMA, short on the opposite cross; exit on a stop_mult×ATR stop or when the SMAs cross back. All indicators are lagged, so bar i only sees information through i-1.
from edgekit.strategy import SmaCross
trades = SmaCross(fast=20, slow=100).backtest(bars, warmup=210, bars_per_day=6)Writing your own#
Subclass BaseStrategy, precompute (and lag) your indicators in prepare, return an EntryIntent(direction, level, stop_dist) from entry, and an exit price from exit. The engine handles gap-aware fills, the R accounting, and costs. See the custom-strategy guide and the tutorial chapter for a full walkthrough.
New templates#
Five more textbook edges. The first two are engine-driven like ORB and SmaCross — subclasses of BaseStrategy with the inherited .backtest(bars). The last three trade a cross-section or a pair, which the single-instrument bar loop cannot express, so each carries its own vectorised backtest with its own signature — read them below. All are demonstrations for the gauntlet, not tuned edges.
BollingerRevert#
Bollinger-band mean reversion — go long when the lagged close drops below the lower band, exit at the lagged middle band, with a hard ATR stop. Engine-driven: the usual backtest(bars) applies.
BollingerRevert(n: int = 20, k: float = 2.0, atr_n: int = 20, stop_mult: float = 2.5)| Param | Default | Meaning |
|---|---|---|
n | 20 | Bollinger lookback (middle band = SMA(n)). |
k | 2.0 | Band width in standard deviations. |
atr_n | 20 | ATR period for the stop distance. |
stop_mult | 2.5 | Hard stop in ATRs (the R denominator). |
from edgekit.strategy import BollingerRevert
trades = BollingerRevert(n=20, k=2.0).backtest(bars) # canonical trade frame in RHawkesBreakout#
A Donchian breakout gated by a Hawkes self-exciting activity process — enter a channel break only when the lagged Hawkes intensity of |log returns| sits above its rolling q-quantile, i.e. when volatility is actively clustering. The gate is the point: raw Donchian breaks mostly fail; breaks during an activity burst are the ones with follow-through. ATR stop, engine-driven, all inputs lagged.
HawkesBreakout(kappa: float = 0.1, don_n: int = 20, q: float = 0.95,
q_win: int = 100, atr_n: int = 20, stop_mult: float = 2.0)| Param | Default | Meaning |
|---|---|---|
kappa | 0.1 | Hawkes decay rate of the activity process. |
don_n | 20 | Donchian channel lookback. |
q | 0.95 | Rolling quantile the intensity must exceed to arm the breakout. |
q_win | 100 | Window (bars) of the rolling quantile gate. |
atr_n | 20 | ATR lookback for the stop. |
stop_mult | 2.0 | Stop distance in ATR multiples. |
from edgekit.strategy import HawkesBreakout
trades = HawkesBreakout(kappa=0.1, don_n=20, q=0.95).backtest(bars)PairsCoint#
A cointegration pairs trade over two bar frames — hedge ratio from timeseries.kalman_hedge (rolling-OLS fallback), z-score of the spread over lookback, enter beyond entry_z, exit inside exit_z, stop out at stop_z. R is defined in z-space: the z-move captured divided by stop_z − entry_z (the risk budget of the trade).
PairsCoint(entry_z: float = 2.0, exit_z: float = 0.5, stop_z: float = 4.0, lookback: int = 100)
.backtest(bars_a: pd.DataFrame, bars_b: pd.DataFrame, cost=None) -> pd.DataFrame| Param | Default | Meaning |
|---|---|---|
entry_z | 2.0 | Enter when |z| of the spread exceeds this. |
exit_z | 0.5 | Exit when |z| reverts inside this. |
stop_z | 4.0 | Stop out when |z| blows past this (defines 1R). |
lookback | 100 | Window for the spread z-score. |
Returns: the canonical trade frame in R — same columns as the engine-driven templates, one row per closed spread trade.
from edgekit.strategy import PairsCoint
trades = PairsCoint(entry_z=2.0, exit_z=0.5).backtest(bars_pep, bars_ko)
trades.r.sum() # R accounting works the same downstream (trade_stats, MCPT, sizing)CsMomentum#
Cross-sectional momentum over a price panel — rank assets by the lookback-bar return skipping the most recent skip bars (sidestepping short-term reversal), go long the top top_frac and short the bottom, rebalanced on the rebalance calendar. Weights are shifted before returns are computed, so the backtest is causal.
CsMomentum(lookback: int = 126, skip: int = 21, top_frac: float = 0.2, rebalance: str = "ME")
.backtest(prices: pd.DataFrame) -> dict| Param | Default | Meaning |
|---|---|---|
lookback | 126 | Ranking window in bars (~6 months daily). |
skip | 21 | Most-recent bars excluded from the ranking (~1 month). |
top_frac | 0.2 | Fraction of names held long (and short). |
rebalance | "ME" | pandas offset alias for the rebalance calendar (month-end). |
Returns: a dict with keys "weights" (the T×N weight panel actually held) and "returns" (the causal portfolio return series).
from edgekit.strategy import CsMomentum
res = CsMomentum(lookback=126, skip=21).backtest(prices_panel)
res["returns"].cumsum() # judge with ek.metrics; check ek.metrics.turnover(res["weights"])Carry#
A cross-sectional carry sort — each rebalance, go long the top top_frac of assets by carry and short the bottom, holding until the next rebalance. You supply the carry panel (futures roll yield, FX rate differential, funding rates …) aligned to the price panel; weights are applied causally (shifted) like CsMomentum.
Carry(top_frac: float = 0.3, rebalance: str = "W")
.backtest(prices: pd.DataFrame, carry: pd.DataFrame) -> dict| Param | Default | Meaning |
|---|---|---|
top_frac | 0.3 | Fraction of names long (and short) by carry rank. |
rebalance | "W" | pandas offset alias for the rebalance calendar (weekly). |
Returns: a dict with keys "weights" and "returns", same contract as CsMomentum.
from edgekit.strategy import Carry
res = Carry(top_frac=0.3, rebalance="W").backtest(prices_panel, carry_panel)
res["returns"] # long high carry, short low carry, causalSee also#
- edgekit.engine — the bar loop and the
Strategyprotocol. - edgekit.validation — prove a strategy before you trust it.
- edgekit.portfolio — combine several strategies into one book.