← FORGE

FORGE — Experiment: Pipelines of Experiments Built on forge()

forgedge.experiment is a third sibling alongside forgedge.playground (read-only analysis) and forgedge.deployment (production) — it runs its own multi-stage pipeline on top of forge(), instead of only reading or acting on a ForgeResult that already exists. StepWiseDiscovery, its first class, grows hold-out-confirmed single-condition rules one AND-condition at a time by searching each rule's own active sub-population for a second dimension — a per-seed local search, distinct from forge()'s own single global, grade-guided two-pass composition.

This is a usage guide: signature, parameters, return fields, and verified examples. For the design rationale (why config resolution happens exactly once, why redundancy is checked both within a chain and across seeds, the research history that motivated every one of its fixes) see src/forgedge/docs/modules/Experiment.md.

from forgedge.experiment import StepWiseDiscovery, StepWiseDiscoveryConfig

Basic usage

import pandas as pd
from forgedge.experiment import StepWiseDiscovery, StepWiseDiscoveryConfig

kpi = pd.read_parquet("kpi_table.parquet")   # 'close' + a timestamp column, like forge()

engine = StepWiseDiscovery(
    kpi, ticker="BTCUSDC", timeframe="1D",
    config=StepWiseDiscoveryConfig(n_seeds=3, depth=2),
)
result = engine.run()

print(result.summary())          # one row per chain: expression, depth_reached, verdict, ...
for chain in result.edges():     # chains whose final (last-confirmed) state is EDGE/PARTIAL-EDGE
    print(chain.chain_label, chain.expression, chain.verdict)

Any asset-specific calibration (AlphaConfig.fee_per_side, mfe_floor, gate thresholds, ...) is the caller's responsibility, passed already resolved via event_discovery_config/alpha_config/rule_discovery_config — StepWiseDiscovery treats them as given and never second-guesses them. Leaving all three None (the default) resolves the same way an unconfigured forge() call would.


StepWiseDiscovery(kpi, *, ticker=None, asset="ASSET", timeframe, event_discovery_config=None, alpha_config=None, rule_discovery_config=None, config=None, run_market_context=True)

Parameters: - kpi: pd.DataFrame — the full KPI Table. Never mutated. - ticker: str, optional — forwarded to forge() and to every direct AlphaDiscovery/RuleDiscovery call this class makes, so IDs stay consistent across iteration 1 and every chain. - asset: str — traceability fallback, same semantics as forge()'s own asset parameter (used only when neither ticker nor an explicit alpha_config sets one). - timeframe: str — bar size, e.g. "1D", "1H". Required keyword. - event_discovery_config: DiscoveryConfig, optional — max_and_components is forced to 1 regardless of what's passed; composition is this class's own job. - alpha_config: AlphaConfig, optional - rule_discovery_config: RuleDiscoveryConfig, optional - config: StepWiseDiscoveryConfig, optional — search-algorithm parameters (see field table below). Defaults to StepWiseDiscoveryConfig(). - run_market_context: bool, default True — forwarded to the internal forge() call for iteration 1.

.run() -> StepWiseDiscoveryResult — executes the full algorithm (module docstring / modules/Experiment.md §3) and returns the result. Raises ValueError before running anything if config.strict (default True) and the resolved configuration has a FAIL-level coherence finding — the same fail-fast contract forge(strict=True) has.

.log_lines: list[str] — every diagnostic line logged during .run() (TimeBudget summary, seed acceptance/rejection with reasons, per-depth candidate counts, composed-rule attempts), in order. Useful to print alongside result.summary() the same way the manual recommends logging result.ledger.describe()/result.resolution.describe() next to a plain forge() run.


StepWiseDiscoveryConfig

None of these fields are asset- or timeframe-specific — that calibration belongs on the configs passed to the constructor instead.

Field Default Effect
n_seeds 3 Number of distinct seeds to start an independent chain from.
depth 2 Maximum AND-composition depth per chain.
train_ratio 0.80 Fraction of the KPI table reserved for the OUTER hold-out split — independent of, and in addition to, any internal split the passed-in configs use.
outer_horizon_bars / outer_embargo_bars None / None Purge/embargo width for the outer split. None derives them from the RESOLVED alpha_config.horizon_grid/embargo_bars — the same quantity forge() itself would use; an explicit value always takes priority (e.g. an OU-half-life-derived scale, if you have one).
retain_ratio_floor 0.2 Absolute floor on retain_ratio_min_k = max(floor, min_trades_M3 / n_rows), recomputed at every depth from the CURRENT partition's actual size — an adaptive floor tied to M3's own statistical requirement (rule_discovery_config.criteria.min_oos_trades), not a fixed row count.
max_constituent_jaccard 0.85 Same default as forgedge.event_discovery.diversity_gate/ANDComposer(max_constituent_jaccard=...) — see forgedge.experiment.redundancy.
max_constituent_abs_corr 0.95 Deliberately stricter than the Jaccard threshold — must only catch near-duplicate information, not merely-correlated-but-distinct signals.
child_gate None Consistency Gate for children evaluated on a partition. None uses GateParams(min_tpm=0.25, dispersion_margin=3.0, min_episodes=4, event_counting="episode", episode_gap=1) — looser than a typical session gate (a sub-population search needs it), but not so loose that a rare child intersected with an already-rare parent starves the composed rule of activations before Alpha Discovery can derive a target.
min_composed_activations 20 A parent-AND-child candidate below this many activations on the full search frame is rejected before even trying Alpha Discovery — fails fast with a clear reason instead of an opaque M2 "no derivable target".
min_local_partition_rows 10 A rolling-transform child whose local (partition-restricted) series has fewer non-NaN rows than this is skipped.
strict True Forwarded to forge()/config_report() — raise on a FAIL-level incoherence rather than proceeding.
config = StepWiseDiscoveryConfig(n_seeds=5, depth=3, max_constituent_abs_corr=0.90)

StepWiseDiscoveryResult

ChainResult

chain_label, seed_family, candidate: EventCandidate, contract: AlphaContract, response: RuleDiscoveryResponse, depth_reached: int, confirm_reason: str, stop_reason: str, plus convenience properties .expression, .composite_score, .verdict. candidate/contract/response describe the LAST CONFIRMED state of the chain — the seed alone if no composition ever confirmed on the hold-out, or the deepest composed rule that did; a composition attempt that fails hold-out confirmation is never written here (see modules/Experiment.md §2, principle 5).


forgedge.experiment.redundancy — the diversity checks

Pure functions, no pipeline dependency — usable and testable standalone:


Verified example

On this repo's own tests/fixtures/ADA_1D_TRAIN.parquet (882 bars), default DiscoveryConfig/AlphaConfig/RuleDiscoveryConfig, StepWiseDiscoveryConfig(n_seeds=2, depth=2):

import pandas as pd
from forgedge.experiment import StepWiseDiscovery, StepWiseDiscoveryConfig

kpi = pd.read_parquet("tests/fixtures/ADA_1D_TRAIN.parquet")
engine = StepWiseDiscovery(kpi, ticker="ADAUSDC", timeframe="1D",
                            config=StepWiseDiscoveryConfig(n_seeds=2, depth=2))
result = engine.run()
print(result.summary()[["chain", "expression", "depth_reached", "verdict"]].to_string(index=False))
                                        chain                                                            expression  depth_reached  verdict
seed1:diffnorm_close_vol_vol|rolling_pctrank                          pr_diffnorm_close_vol12_vol24_168 < 0.172619              0  PARTIAL-EDGE
          seed2:ratio_close_ema_ema|identity  (ratio_close_ema03_ema12 < 0.952059) AND (zs_close_ema_03_96 < -1.5)              1  NON-EDGE

seed2 reaches depth 1 — a genuinely confirmed two-condition composed rule (search-level verdict NON-EDGE, but a positive mean expectancy across post-holdout walk-forward folds on the concatenated search+holdout evaluation). seed1 stays single-condition: the one composition attempted at depth 0 failed hold-out confirmation, so the chain correctly reports the last confirmed state (the seed) rather than the failed attempt. Neither chain reaches depth 2 on this fixture.

A second, runnable example against this repo's own bundled OHLCV data (EURUSD 1D, with the fee-per-side diagnostic a non-crypto asset needs before trusting a verdict): examples/step_wise_discovery_usage.py.


What's next

StepWiseDiscovery is the first class in this module — future experiment pipelines built on forge() (e.g. a cross-ticker or cross-timeframe search orchestrator) would be added as new classes here, not by growing this one's scope.