edgekit.core
The shared conventions every other module agrees on: the R-multiple, the OHLC contract, the causal lag() primitive, the Signal / Trade dataclasses, and the reproducible RNG. This is the vocabulary the rest of the library speaks.
What's inside. One idea underpins everything: a trade is measured in R-multiples — profit in units of its own initial risk (the stop distance). A +2R trade made twice what it risked; a −1R trade lost a full unit of risk. Dollars are applied once, later, from a single sizing scalar (see edgekit.sizing) — never baked into a strategy. Around that sit the two dataclasses the engine emits and consumes (Signal, Trade), the frame builder trades_to_frame, the annualisation helper infer_bars_per_year, the seeded bootstrap_rng, the schema validator as_ohlc, and the single most important function in the library — lag.
Everywhere in edgekit a “bar frame” means a pandas.DataFrame indexed by a tz-naive UTC DatetimeIndex, with float columns open, high, low, close (volume / tick_volume optional). OHLC = ("open", "high", "low", "close") is the module constant naming those four columns.
Causality#
lag#
Causal shift: a value that is only known after a bar has closed is pushed forward k bars, so that bar i can only ever act on information available through bar i-1. This is the single most important primitive in the library. Indicators are returned unlagged (see edgekit.indicators); every strategy must lag them before acting, or the backtest reads the future. It works on numpy arrays and pandas Series alike; leading positions become NaN.
lag(x, k: int = 1)| Param | Type | Default | Meaning |
|---|---|---|---|
x | np.ndarray | pd.Series | — | The indicator/series to shift forward. |
k | int | 1 | Number of bars to shift (delay). k=1 is the standard one-bar lag. |
Returns: the same type as the input — numpy array in, numpy array out; Series in, Series out.
from edgekit import indicators as ind
from edgekit.core import lag
# indicators come back UNLAGGED (value through bar i inclusive)
raw_atr = ind.atr(bars.high, bars.low, bars.close, 20)
# lag by one bar so a decision on bar i only sees info through i-1
N = lag(raw_atr, 1) # N[i] reflects the ATR known at the close of bar i-1Schema#
as_ohlc#
Validate and normalise any bar frame to the OHLC contract: it sorts the index, drops duplicate timestamps (keep first), and checks the required open/high/low/close columns exist. It does not resample or fill — that is the caller's decision. Run it on anything before it enters the engine so the monotone-and-unique invariant every indicator assumes actually holds.
as_ohlc(df: pd.DataFrame) -> pd.DataFramedf— a bar frame indexed by aDatetimeIndexwith the four OHLC columns.
Returns: the cleaned OHLC DataFrame (sorted, deduplicated). Raises ValueError if a required column is missing, and TypeError if the index is not a DatetimeIndex.
from edgekit.core import as_ohlc
bars = as_ohlc(raw_df) # sorted, deduped, schema-checked — safe to backtestThe dataclasses#
Signal#
A strategy's intent to open a position on the next bar's open. stop_dist is the stop distance in price units (e.g. 2 * ATR); the engine turns it into the R denominator. direction is +1 long, −1 short. This is the object a BracketStrategy returns to the prop-firm run_backtest engine.
Signal(direction: int, stop_dist: float, tag: str = "")| Field | Type | Default | Meaning |
|---|---|---|---|
direction | int | — | +1 long, −1 short. |
stop_dist | float | — | Stop distance in price units — becomes the R denominator. |
tag | str | "" | Free-text label carried onto the resulting trade (which sleeve fired). |
from edgekit.core import Signal
def my_strat(df, i):
if breakout(df, i):
return Signal(direction=1, stop_dist=20.0, tag="brk")
return NoneTrade#
One closed round-trip, priced in R. r is net of costs — the engine has already charged spread and swap before it lands here. A list of these is what the R-multiple engine accumulates and then collapses into a frame via trades_to_frame.
Trade(entry_time, exit_time, direction: int, entry: float, exit: float,
stop_dist: float, r: float, bars_held: int, tag: str = "", exit_reason: str = "")| Field | Type | Meaning |
|---|---|---|
entry_time | pd.Timestamp | Bar timestamp the position opened. |
exit_time | pd.Timestamp | Bar timestamp the position closed. |
direction | int | +1 long, −1 short. |
entry | float | Fill price at entry. |
exit | float | Fill price at exit. |
stop_dist | float | The R denominator (initial risk in price units). |
r | float | Realised return in R, NET of costs. |
bars_held | int | Number of bars the position was open. |
tag | str | Which strategy/sleeve produced it. |
exit_reason | str | Why it closed (e.g. "sl", "tp", "eod"). |
Frames & helpers#
trades_to_frame#
Collect a list of Trade into the canonical trade DataFrame that the rest of the library (metrics, sizing, viz) consumes. On top of the full record it adds a date column (the exit date) and a dir column (a direction alias), because downstream code expects at minimum date + r + dir.
trades_to_frame(trades: list[Trade]) -> pd.DataFrametrades— a list ofTradeobjects (may be empty).
Returns a DataFrame with columns: entry_time, exit_time, direction, entry, exit, stop_dist, r, bars_held, tag, exit_reason, date, dir. An empty trades list yields an empty frame with those columns. The module constant TRADE_COLUMNS is the base column order (everything except date/dir).
from edgekit.core import trades_to_frame, TRADE_COLUMNS
frame = trades_to_frame(trade_list)
frame[["date", "r", "dir"]].head() # the minimum downstream contract
TRADE_COLUMNS # base column orderinfer_bars_per_year#
Estimate bars-per-year from the median bar spacing of an index — used to annualise Sharpe. The median (not the mean) makes it robust to weekend and holiday gaps. Falls back to 252.0 for fewer than 3 bars or a non-positive step.
infer_bars_per_year(index: pd.DatetimeIndex) -> floatindex— the bar frame'sDatetimeIndex.
Returns: a float — approximate bars per calendar year (≈ 2190 for H4 crypto, 252 for daily).
from edgekit.core import infer_bars_per_year
ppy = infer_bars_per_year(bars.index) # e.g. ~2190 for H4 24/7 cryptobootstrap_rng#
The repo-wide seeded generator (default seed 11) so results are reproducible. Pass it into any permutation / bootstrap routine (edgekit.validation) and re-runs give identical p-values.
bootstrap_rng(seed: int = 11) -> np.random.Generatorseed— the RNG seed (default 11, the repo-wide default).
Returns: a numpy.random.Generator.
from edgekit.core import bootstrap_rng
rng = bootstrap_rng() # seed 11
rng2 = bootstrap_rng(42) # a different reproducible streamModule constants#
OHLC = ("open", "high", "low", "close")— the four required column names.TRADE_COLUMNS— the base column order of the canonical trade frame (beforedate/dirare appended).
OHLC, Signal, Trade, as_ohlc, lag and trades_to_frame are all re-exported at the package root, so edgekit.lag(...) and edgekit.core.lag(...) are the same function.See also#
- edgekit.indicators — the unlagged indicators
lagexists to guard. - edgekit.engine — consumes
Signal, emitsTrade. - edgekit.data — produces the OHLC frames
as_ohlcvalidates.