edgekit

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.

The OHLC contract

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)
ParamTypeDefaultMeaning
xnp.ndarray | pd.SeriesThe indicator/series to shift forward.
kint1Number 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.

lag_example.py
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-1
!Lag is the caller's job
edgekit deliberately keeps the lag at the call site instead of baking it into each indicator — it makes the causality decision visible and testable. If you feed an unlagged indicator straight into a trading rule, the backtest peeks at the current bar's completed value and the result is look-ahead-biased fiction.

Schema#

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.DataFrame
  • df — a bar frame indexed by a DatetimeIndex with 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 backtest

The 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 = "")
FieldTypeDefaultMeaning
directionint+1 long, −1 short.
stop_distfloatStop distance in price units — becomes the R denominator.
tagstr""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 None

Trade#

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 = "")
FieldTypeMeaning
entry_timepd.TimestampBar timestamp the position opened.
exit_timepd.TimestampBar timestamp the position closed.
directionint+1 long, −1 short.
entryfloatFill price at entry.
exitfloatFill price at exit.
stop_distfloatThe R denominator (initial risk in price units).
rfloatRealised return in R, NET of costs.
bars_heldintNumber of bars the position was open.
tagstrWhich strategy/sleeve produced it.
exit_reasonstrWhy 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.DataFrame
  • trades — a list of Trade objects (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 order

infer_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) -> float
  • index — the bar frame's DatetimeIndex.

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 crypto

bootstrap_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.Generator
  • seed — 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 stream

Module constants#

  • OHLC = ("open", "high", "low", "close") — the four required column names.
  • TRADE_COLUMNS — the base column order of the canonical trade frame (before date/dir are appended).
Top-level re-exports
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#