What (One-liner)
Simplified Harding-Pagan cyclical turning-point detection on FRED GDPC1 quarterly GDP: 3-point local extrema + strict peak/trough alternation + ≥ 5-quarter min-phase filter → current expansion/recession phase + most recent peak/trough + number of quarters in the current phase.
How to use
Drop macro_cycle_v1 (GET /mfle/cycles/phase) or use macro_snapshot_v1 aggregation directly. Typical pattern:
macro_snapshot_v1 → institution_composite (cycle_phase overlay)
↘
macro_cycle_v1 (direct call) → casa_strategy (macro context)Core formulas
Step 1: 3-point local extrema
peak[i] iff vals[i] >= vals[i-1] AND vals[i] >= vals[i+1]
trough[i] iff vals[i] <= vals[i-1] AND vals[i] <= vals[i+1]
Step 2: Strict alternation + MIN_PHASE=5 filter
keep if i-last_idx >= MIN_PHASE_QUARTERS
skip if same type as last kept
Step 3: Current phase (last turn relative to as_of)
last.type == 'trough' → 'expansion'
last.type == 'peak' → 'recession'
Step 4: Duration
quarters_in_phase = count(ts >= last_turn_date) - 1
months_in_phase = quarters_in_phase * 3
Step 5: Truncate to last 10 turns in cycles_from_gdp
(cycles_history keeps all)Assumptions & applicability
Assumptions: US GDPC1 quarterly data + phase durations ≥ 5 quarters + 3-point local-extrema detection + no amplitude filter.
Applies to: Canvas macro overlay, expansion/recession phase estimation, estimating how long the current cycle has been running.
Does not apply to: intraday/monthly, NBER-official reproduction (multi-indicator committee), non-US, short series, scenarios requiring amplitude-filtered fine-grained cycle dating.
Input / Output contract
{limit_quarters?=80} → {available, cycle_phase, cycle_peak_date, cycle_trough_date, quarters_in_phase, months_in_phase, turning_points[≤10], logic_lineage} (with reason when available=False).
Known limitations
- GDPC1 only: NBER uses multiple indicators (employment / IP / income) in committee judgment; this operator is a GDP-only simplification
- Simplified 3-point extrema: original Bry-Boschan uses a 2-quarter window + amplitude filter; this is the minimal simplification
- MIN_PHASE_QUARTERS=5 hard-coded: not configurable
- No amplitude filter: small noise peaks may be retained (fixture uses σ=0.1 smoothing)
- First turn requires i ≥ MIN_PHASE: early-series data points lost
- cycles_from_gdp truncated to last 10: full history via cycles_history endpoint
- Equal values (vals[i]==vals[i-1]) trigger BOTH peak and trough candidates (rare but possible)
- Series is sorted by index — irregular ts is garbage
- Non-US economies not supported
References
Harding-Pagan (2002) Dissecting the cycle (JME) + Bry-Boschan (1971) Cyclical Analysis (NBER) + Burns-Mitchell (1946) Measuring Business Cycles. See frontmatter.
Golden Test
tests/golden/fixtures/tier3/macro_cycle/ — monkey-patched FRED 60-quarter synthetic 3-cycle GDP (seed=123). 1e-6 tolerance. 11 tests:
normal_path— 3 cycles synthetic → 3 turning points + recession currenthistory_path— cycles_history returns all turning pointsfred_unavailable— available=False, empty turning_pointsinsufficient_data— 5 quarters (< 10) → available=Falsedeterminism— 5 reruns identicalmin_phase_quarters_locked— MIN_PHASE_QUARTERS constant == 5phase_enum_contract— enum subsetmonths_derived_from_quarters— months == quarters * 3turns_alternate_and_respect_min_phase— alternate + ≥ 5 quarters apartlogic_lineage_locked— ['harding_pagan', 'GDPC1']truncates_to_last_10_turns— cycles_from_gdp caps at 10; history unlimited
Changelog
- 1.0.0 (2026-04-20) — First Active (Tier 3 batch 2.1)

