Input contracts:
evaluateconsumes an evaluation panel with the four-column floor documented in Data schema.evaluate_horizonsconsumes a raw canonical panel withpriceand factor columns, then computesforward_returninternally for each horizon. Scalar post-processing helpers such asbreakeven_costandnet_spreadare direct-call utilities, notevaluate()metrics.
factrix.evaluate ¶
evaluate(data: DataInput, *, price_data: DataInput | None = None, metrics: dict[str, MetricBase], factor_cols: list[str], forward_periods: int | None = None, overlap_periods: int | None = None, strict: bool = True, expected_warnings: tuple[str, ...] = ()) -> dict[str, EvaluationResult]
Evaluate one or more factors against forward returns through the DAG executor.
Closed-set DAG dispatch — every spec referenced by another spec's
requires is auto-pulled into the executor, batched stage-1
producers run once across the whole factor batch (IC's
compute_ic etc.), and per-factor consumers run once per factor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataInput
|
Long-format data satisfying the four-column floor
|
required |
price_data
|
DataInput | None
|
Optional complete price panel containing exactly alignable
|
None
|
metrics
|
dict[str, MetricBase]
|
|
required |
factor_cols
|
list[str]
|
Names of factor columns on |
required |
forward_periods
|
int | None
|
The data's return horizon — the |
None
|
overlap_periods
|
int | None
|
The overlap of adjacent observations on the
evaluation grid — the quantity inference consumes (HAC bandwidth
and effective df, non-overlapping stride, stride-scaled floors).
Normally omitted — |
None
|
strict
|
bool
|
When |
True
|
expected_warnings
|
tuple[str, ...]
|
:class: |
()
|
Returns:
| Type | Description |
|---|---|
dict[str, EvaluationResult]
|
|
dict[str, EvaluationResult]
|
|
dict[str, EvaluationResult]
|
per-metric outputs, panel structural stats ( |
dict[str, EvaluationResult]
|
|
Raises:
| Type | Description |
|---|---|
UserInputError
|
|
Examples:
Single-factor IC + IC information ratio (IR):
>>> import factrix as fx
>>> from factrix.metrics import ic, ic_ir
>>> raw = fx.datasets.make_cs_panel(n_assets=15, n_dates=80)
>>> data = fx.preprocess.compute_forward_return(raw, forward_periods=5)
>>> results = fx.evaluate(
... data,
... metrics={"ic": ic(), "ic_ir": ic_ir()},
... factor_cols=["factor"],
... forward_periods=5,
... )
>>> "ic" in results["factor"].metrics
True
>>> "ic_ir" in results["factor"].metrics
True
factrix.evaluate_horizons ¶
evaluate_horizons(data: DataInput, *, metrics: dict[str, MetricBase], factor_cols: list[str], forward_periods: list[int], dates: Series | Iterable[object] | None = None, strict: bool = True, expected_warnings: tuple[str, ...] = ()) -> list[EvaluationResult]
Sweep evaluate across several return horizons of one raw panel.
A thin composition over the existing primitives — for each horizon it
rebuilds the panel with
:func:factrix.preprocess.compute_forward_return and runs a single
:func:evaluate, then flattens the per-factor results into one list.
No new type is introduced and the single-horizon contract of
evaluate is untouched: every inner run still evaluates one panel at
one stamped horizon.
The horizon must be rebuilt from the raw panel for each value —
compute_forward_return is not idempotent (it drops the last
forward_periods + 1 rows per asset and stamps the horizon), so a
horizon cannot be re-derived from an already-attached panel. This
wrapper exists to make that rebuild-per-horizon loop hard to get wrong.
The raw panel is also forwarded to each inner :func:evaluate call as
price_data. Event offsets and MFE/MAE windows therefore retain the
complete price grid even though each horizon's forward-return panel has a
shorter tail (and may use a coarser evaluation grid via dates=).
Identity of a swept result is the composite (factor, forward_periods),
not a unique scalar factor key — so the return is a flat
list[EvaluationResult] (the native shape of the aggregation layer),
not the factor-keyed dict that evaluate returns at a fixed
horizon. factor and forward_periods are existing native
attributes of :class:EvaluationResult; the list feeds straight into
:func:compare and into
:func:factrix.multi_factor.bhy. Pool all horizons when selection may
choose across them; use expand_over=('forward_periods',) only for
predeclared horizon-specific screens that are selected and reported
separately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataInput
|
A raw panel carrying |
required |
metrics
|
dict[str, MetricBase]
|
Same contract as :func: |
required |
factor_cols
|
list[str]
|
Same contract as :func: |
required |
forward_periods
|
list[int]
|
The horizons to sweep, as a non-empty |
required |
dates
|
Series | Iterable[object] | None
|
Optional evaluation grid, forwarded unchanged to each inner
:func: |
None
|
strict
|
bool
|
Forwarded unchanged to each inner :func: |
True
|
expected_warnings
|
tuple[str, ...]
|
Forwarded unchanged to each inner
:func: |
()
|
Returns:
| Type | Description |
|---|---|
list[EvaluationResult]
|
Flat |
list[EvaluationResult]
|
|
list[EvaluationResult]
|
|
Raises:
| Type | Description |
|---|---|
UserInputError
|
|
Notes
Comparability across horizons is a scale alignment, not a free
lunch: compute_forward_return divides by forward_periods so rank-IC is
directly comparable across horizons, but signed-return-mean metrics
carry a compounding bias that grows with forward_periods (see
:func:factrix.preprocess.compute_forward_return Notes). Treat a
cross-horizon sweep of signed-mean metrics as descriptive.
Examples:
>>> import factrix as fx
>>> from factrix.metrics import ic
>>> raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=300)
>>> results = fx.evaluate_horizons(
... raw,
... metrics={"ic": ic()},
... factor_cols=["factor"],
... forward_periods=[5, 10, 20],
... )
>>> [r.forward_periods for r in results]
[5, 10, 20]
>>> board = fx.compare(results, metrics=["ic"]) # one row per horizon
>>> board.height
3
Full price data for event paths¶
compute_forward_return returns the evaluation panel: it removes rows whose
forward return cannot be formed and, with dates=, keeps only the chosen
evaluation dates. Those rows are the correct sample for return metrics, but
they are not a complete price path. A later event offset or MFE/MAE window can
need prices from the removed tail or from dates between evaluations.
Keep the three grids separate by passing the original panel as price_data:
import factrix as fx
raw = fx.datasets.make_event_panel(n_assets=50, n_dates=400, rng=7)
panel = fx.preprocess.compute_forward_return(raw, forward_periods=5)
result = fx.evaluate(
panel,
price_data=raw,
metrics={
"path": fx.metrics.event_around_return(offsets=[-1, 6, 12, 24]),
"excursion": fx.metrics.mfe_mae(),
},
factor_cols=["factor"],
strict=False,
)["factor"]
| Input/grid | Owns | Never contributes to |
|---|---|---|
price_data price grid |
Complete per-asset prices used by offsets, excursion windows, and the event baseline | Forward-return samples, sample floors, or factor routing |
data evaluation grid |
Eligible event dates and the rows selected by dates= |
Prices outside the returned panel |
Finite data.forward_return rows |
Return estimates and their effective sample | Extending an event price path |
price_data must contain unique (date, asset_id) keys plus price; its key
dtypes must exactly match data. Extra columns are ignored, so the event and
factor authority cannot silently move to the side panel. If price_data is
omitted, event paths continue to use data.price.
Offsets and windows are counted on the grid that supplies them¶
offsets= and window= are counts of periods on the price grid the path walk
reads. Without price_data that grid is data's own distinct dates; with
price_data it is the price panel's. When the two grids differ, the same
argument therefore spans a different amount of the sample, and the excursion or
offset it measures changes with it.
Measured on a panel whose evaluation grid keeps every tenth period of the price
grid, mfe_mae(window=3):
| Grid the walk read | Median MFE |
|---|---|
data evaluation grid (no price_data) |
0.1272 |
price_data price grid |
0.0120 |
Three evaluation periods reach ten times further than three price periods, so
the coarser grid reports the larger excursion. Neither number is wrong; they
answer different questions. State the grid you mean, and scale window= and
offsets= to it.
Behaviour change (pre-1.0)¶
This is not an additive API. evaluate_horizons owns the raw panel and now
forwards it as price_data on every call, so an existing evaluate_horizons
run that includes event_around_return or mfe_mae changes value: event paths
gain the tail and the between-evaluation periods that were previously
unreachable, and offsets and windows are re-based onto the raw grid as above.
Return-only metrics are unaffected.
Migrate by deciding which grid each event-path argument counts on:
evaluate_horizonscallers: re-readoffsets=andwindow=against the raw panel's grid. Where the evaluation grid was the intended unit, pass the already-preprocessed panel toevaluateinstead.evaluatecallers who preprocess returns themselves: retainrawand addprice_data=rawto get the complete paths; no change is needed for return-only metrics.
forward_periods= and overlap_periods=¶
Both are properties of the data, normally read from the stamps
compute_forward_return leaves on the panel, and never
per-metric knobs. They answer different questions:
| Argument | Question | Read from | Declare it when |
|---|---|---|---|
forward_periods |
Over how many periods of the price grid was the return measured? The hypothesis; joins the (factor, forward_periods, *params) identity. |
_forward_periods stamp |
The panel carries a self-attached forward_return with no stamp. |
overlap_periods |
How many adjacent evaluation observations share future periods? What inference consumes. | _overlap_periods stamp (equal to the horizon on the full grid; derived from dates= on a coarser one) |
The unstamped panel sits on a coarser evaluation grid than its horizon. Otherwise it defaults to forward_periods. |
A value that disagrees with its stamp raises UserInputError
rather than silently overriding the data. Both surface on the result —
forward_periods as identity, overlap_periods as bookkeeping — and
overlap_periods is injected into every metric as
metadata["overlap_periods"]. See
Evaluating on a coarser grid
for why the two are kept apart.
Two more period counts the tradability metrics declare¶
On a full evaluation grid all four counts below are the same number, which is why one name used to carry all of them. They separate as soon as the evaluation grid is coarser than the return horizon.
| Quantity | Question | Unit | Declared on |
|---|---|---|---|
forward_periods |
Economic horizon: over how many periods was the return measured? | Underlying period grid | compute_forward_return; stamped |
overlap_periods |
Inference: how many adjacent evaluation observations share future periods? | Evaluation-grid observations | Derived and stamped; injected, never a metric knob |
rebalance_lag |
Turnover: how far apart are the rankings / memberships compared? | Evaluation-grid observations | rank_turnover(), notional_turnover() |
holding_periods |
Cost amortisation: how many return periods pass between paying trading cost? | Underlying period grid | breakeven_cost(), net_spread() |
rebalance_lag defaults to the injected overlap_periods — horizon-aligned
turnover, the historical behaviour. Pass rebalance_lag=1 when the evaluation
grid is the rebalance schedule. holding_periods has no default tie to the
stamp: gross_spread is per underlying return period (compute_forward_return
divides by forward_periods), so the cost must be amortised over underlying
periods too. Substituting the derived evaluation-grid overlap there is a unit
error, not a conservative choice — see the
tradability migration note.
Use cases¶
-
Single-factor significance
One panel of factor data → one result carrying the mainstream metric's
p_valueand the cell-specific statistics. -
Batch screening with false discovery rate (FDR)
Loop
evaluateover candidate signal columns and feed the resultingEvaluationResultlist tobhyfor false-discovery-rate control. See Multi-factor FDR. -
Cross-cell apples-to-apples
Compare information coefficient (IC) rank-ordering against Fama-MacBeth λ on the same panel, or individual-asset factors against broadcast macro factors. Return shape is identical across cells.
-
TIMESERIES dispatch
At
n_assets == 1there is no cross-section, so anyDENSEmetric whose cell isPANEL—Individual × Continuous(ic,fm_beta) andCommon × Continuous(common_beta,common_quantile,common_asymmetry) — raisesIncompatibleAxisError(or NaN +structure_mismatchunderstrict=False). Single-asset data runs through the same entry point withpredictive_betafor dense predictive-regression slopes, sparse metrics whose cell wildcard allowsTIMESERIES, and panel-input wildcard metrics such asdirectional_hit_rate. Two-column diagnostics (positive_rate,oos_decay,ic_trend) are standalone(date, value)tools; inevaluate()they layer on panel IC series, not raw single-asset dense panels.
Worked example — single-factor smoke test¶
Synthetic panel → evaluate → read value + p_value
Full runnable example complementing the doctest snippets in Examples above with realistic console output.
import factrix as fx
from factrix.metrics import ic, quantile_spread
# 1. Create dummy panel data
raw = fx.datasets.make_cs_panel(n_assets=15, n_dates=80)
data = fx.preprocess.compute_forward_return(raw, forward_periods=5)
# 2. Run evaluation
results = fx.evaluate(
data,
metrics={"ic": ic(), "spread": quantile_spread(n_groups=5)},
factor_cols=["factor"],
forward_periods=5,
)
# 3. Retrieve and inspect results
res = results["factor"]
print(f"Factor: {res.factor}")
print(f"Cell: {res.cell}")
print(f"Plan: \n{res.plan}")
# Access metrics result group
ic_res = res.metrics["ic"]
print(f"IC Value: {ic_res.value:.4f}")
print(f"IC p-value: {ic_res.p_value:.4f}")
Sensitivity grids¶
For exploratory grids across asset counts, horizons, or factor families, run
with strict=False and stack each result's long-form table. The table carries
is_applicable and reason, so the grid can keep running while still making
failed metric/input combinations visible.
import polars as pl
results = fx.evaluate(
data,
metrics={"ic": ic(), "spread": quantile_spread(n_groups=2)},
factor_cols=["factor"],
forward_periods=5,
strict=False,
)
status = pl.concat([r.to_frame() for r in results.values()])
failed = status.filter(~pl.col("is_applicable"))
Evaluating under different cell contexts¶
Metric behaviors are defined by instantiating metric classes directly. The DAG executor handles dispatch automatically depending on the cell registered by the metric.
import factrix as fx
from factrix.metrics import ic, caar, common_beta
# 1. Individual × Continuous (e.g. Information Coefficient)
results_ic = fx.evaluate(
data,
metrics={"ic": ic()},
factor_cols=["factor"],
forward_periods=5
)
# 2. Individual × Sparse (e.g. Event Study CAAR, requires a 'price' column)
results_caar = fx.evaluate(
data_with_price,
metrics={"caar": caar()},
factor_cols=["event_factor"],
forward_periods=5
)
# 3. Common × Continuous (e.g. Time-Series Beta)
results_common_beta = fx.evaluate(
data,
metrics={"common_beta": common_beta()},
factor_cols=["macro_factor"],
forward_periods=5
)
Per-cell required / optional columns and the DataStructure (PANEL vs TIMESERIES) derivation are automatically resolved at dispatch time.
Next steps¶
-
Multi-factor FDR
Wires
evaluateinto the multi-factor FDR pipeline: pass candidate results to BHY; choose betweenbhy/partial_conjunction/bhy_hierarchical; mixed-cell batches. -
Data schema
New to the fixed-horizon input contract? Start here for the evaluation panel floor (
date,asset_id,factor,forward_return), dtype semantics, and optional columns that activate extra metrics.
See also¶
-
Common-factor regression conventions
The
n_assets == 1dispatch rules and SE conventions for the per-asset time-series stage. -
Panel vs timeseries sample guard
Per-metric, per-axis sample floors on the effective sample, and the
InsufficientSampleErrorrecovery path (.axis/.actual/.required).