Simen Bjerkelund
  • Home
  • CV
  • Blog
  • Data Projects
    • All Data Projects

    • Tennis Data
    • Tennis Statistics
    • Tennis Match Predictor
    • Grand Slam Point-by-Point

    • Finance and Factor Analysis
    • LLM Alpha Farming
    • Portfolio Construction
    • Signal Combination

    • Ad Hoc
    • Oslo Housing
    • Research Trends
  • SSB Analysis
  • Trading Analysis and Machine Learning

On this page

  • How the pipeline works
  • Results
  • What this run found
  • Best signal vs the market
  • The factors and the statistics, in depth
  • Run history
  • Where this goes next
  • The signal library

LLM Alpha Farming

finance
quant
ai-agent
llm
Can you farm tradable equity signals out of a language model? Three agents propose, implement and document cross-sectional alpha; one deterministic harness judges them all — and discounts every result for the size of the search.
Published

October 2, 2026

Farming alpha out of a language model

Three cooperating agents invent, implement and document equity signals. One deterministic harness judges every one of them under identical, point-in-time-correct rules — and then discounts the result for the size of the search.
Run 2026-10-02171 signals evaluated280 months of historyAPI spend $0.36
171
Signals evaluated
0
Survive correction
1.01
Search hurdle (Sharpe)
210
Trials priced in
0.92
Best raw Sharpe
4.26
Best alpha t-stat

How the pipeline works

The premise is a meta-experiment: generate hypotheses with the most capable model available, implement them with a cheaper one, and measure how many survive a ruthless, uniform backtest. The interesting number was never the best Sharpe — it is the survival rate, and that only means something if the judging is identical for every candidate and honest about the size of the search.

The six stages, end to end click to expand
01

Idea agent

Mines the asset-pricing literature for an economically motivated hypothesis that can actually be built from the panel — with a stated mechanism, a formula, an expected sign, and references.

Opus 5
02

Data agent

Maps each requested concept onto a real panel column and gates on coverage. An idea that needs data we do not have dies here, cheaply, before any code is written.

Sonnet 5
03

Coder agent

Writes a self-contained module implementing the signal interface, then self-corrects against the validator’s complaints. Generated code is untrusted: a static check blocks file, network and process access before the module is ever imported.

Sonnet 5
04

Shared harness

Monthly cross-sectional decile long/short on a survivorship-safe, liquidity-filtered universe. Identical for every signal — invented or canonical — so nothing gets a bespoke advantage.

deterministic
05

Robustness battery

Subperiod stability, transaction-cost sweep, sector-neutral variant, parameter and universe sensitivity, and holding-horizon decay.

deterministic
06

Selection-bias layer

Factor attribution against market, size, value and momentum; Benjamini-Hochberg across the library; and a deflated Sharpe that prices in how many ideas were tried to find this one.

deterministic

Everything is point-in-time. Fundamentals join as of the SEC filing date, never the fiscal period. The universe keeps delisted names. Signals must pass a no-look-ahead validator: recomputing on a time-truncated panel may not change any earlier value, or even its missing/not-missing status. A name that leaves the panel books a delisting return rather than silently disappearing from the cross-section — otherwise the worst outcomes quietly vanish from whichever decile was holding them.

Results

Last refreshed 2026-10-02 — regenerated automatically on every pipeline run.

Signal Verdict Sharpe Alpha t Factor R² Deflated Sharpe
gross_profitability Noise 0.92 3.22 21% 0.35
payout_funding_gap Noise 0.82 3.55 42% 0.19
sbc_adjusted_cashflow_yield Noise 0.78 3.30 55% 0.13
net_share_issuance Noise 0.73 4.26 59% 0.08
macro_systematic_variance_share Noise 0.57 1.78 53% 0.03
cross_sectional_beta_dispersion_timing Noise 0.51 1.10 39% 0.01
late_quarter_buyback_share_shrinkage Noise 0.56 3.07 70% 0.01
trade_credit_stretch_quality Noise 0.51 2.60 4% 0.01
stagflation_supply_shock_timing Noise 0.52 2.59 4% 0.01
predicted_dividend_month_premium Loses to factors 0.41 -4.06 95% 0.00

The 10 best of 171 signals, ranked by deflated Sharpe. All 171, with confidence intervals, are further down.

Why the board is not sorted by Sharpe. Farm enough strategies and one will post a fine Sharpe by luck alone. With 210 trials priced in, the expected best-of-search Sharpe — the hurdle — is 1.01. Anything below that is indistinguishable from noise no matter how good the raw number looks.

Alpha t is the return left after regressing on market, size, value and momentum, with Newey-West standard errors. A high Sharpe next to a high Factor R² and a flat alpha t means the strategy is a repackaged factor tilt, not a discovery. The reverse also happens: a modest Sharpe with a strong alpha t is worth more than it first looks.

Deflated Sharpe is the probability the signal is real once the size of the search is accounted for; q-value is the Benjamini-Hochberg false-discovery rate across the whole library.

Equity curves of surviving alpha signals

Net long/short equity curves for the signals that survived multiple-testing correction. Log scale — a straight line is a constant compounding rate.

What this run found

  • No signal in this run survives correction. With a hurdle Sharpe of 1.01, nothing on the board is currently distinguishable from the best of a random search of the same size — which is itself the honest headline, not a failure of the machinery.

  • 10 are factor tilts in disguise. The clearest case is gradual_information_momentum, whose returns are 93% explained by the standard factors with an alpha t-stat of -0.83 — it is not adding anything the four-factor model does not already capture.

  • excess_speculative_volatility is the case for looking past raw Sharpe: only 0.29 on its own, but an alpha t-stat of 2.95 once the known factors are stripped out — its raw performance understates it.

  • predicted_dividend_month_premium is the inverse warning: it tracks the standard factors closely (95% of variance) yet delivers significantly negative alpha against them (t=-4.06). On this universe and sample you would have been better off holding the plain factor than this construction of it.

All 171 signals, with 95% confidence intervals click to expand
Signal Verdict Sharpe 95% CI Alpha t Factor R² Deflated Sharpe q-value
gross_profitability Noise 0.92 [0.51, 1.36] 3.22 21% 0.35 0.002
payout_funding_gap Noise 0.82 [0.40, 1.28] 3.55 42% 0.19 0.011
sbc_adjusted_cashflow_yield Noise 0.78 [0.38, 1.19] 3.30 55% 0.13 0.015
net_share_issuance Noise 0.73 [0.36, 1.09] 4.26 59% 0.08 0.015
macro_systematic_variance_share Noise 0.57 [0.11, 0.98] 1.78 53% 0.03 0.081
cross_sectional_beta_dispersion_timing Noise 0.51 [0.07, 1.00] 1.10 39% 0.01 0.132
late_quarter_buyback_share_shrinkage Noise 0.56 [0.20, 0.88] 3.07 70% 0.01 0.061
trade_credit_stretch_quality Noise 0.51 [0.10, 0.89] 2.60 4% 0.01 0.081
stagflation_supply_shock_timing Noise 0.52 [0.12, 0.91] 2.59 4% 0.01 0.090
predicted_dividend_month_premium Loses to factors 0.41 [-0.00, 0.89] -4.06 95% 0.00 0.204
aggregate_accruals_market_timing Noise 0.37 [-0.07, 0.78] 1.70 14% 0.00 0.373
asset_growth Noise 0.43 [0.02, 0.81] 2.27 45% 0.00 0.204
tax_loss_selling_turn_of_year_pressure Noise -0.58 [-1.48, 0.18] -0.73 8% 0.00 0.374
average_systematic_comovement_timing Noise 0.36 [-0.12, 0.88] 0.25 39% 0.00 0.425
operating_leverage_industrial_cycle Noise 0.39 [-0.07, 0.81] 1.75 4% 0.00 0.335
retained_vs_contributed_capital_value Noise 0.38 [0.01, 0.75] 1.76 65% 0.00 0.230
frog_in_the_pan_gradual_momentum Factor tilt 0.33 [-0.10, 0.76] 1.07 90% 0.00 0.373
lottery_demand_sentiment_interaction Noise 0.31 [-0.15, 0.79] 1.08 7% 0.00 0.491
earnings_yield Noise 0.34 [-0.10, 0.80] -0.00 62% 0.00 0.416
cash_backed_earnings_surprise Noise 0.28 [-0.13, 0.69] 0.81 57% 0.00 0.441
sentiment_fundamental_pessimism_gap_timing Noise 0.25 [-0.26, 0.72] 0.45 12% 0.00 0.656
momentum_12_1 Factor tilt 0.27 [-0.16, 0.73] -0.10 92% 0.00 0.491
excess_speculative_volatility Noise 0.29 [-0.09, 0.67] 2.95 50% 0.00 0.391
equity_rate_correlation_regime_timing Noise 0.25 [-0.17, 0.73] 0.18 26% 0.00 0.592
inventory_growth_gap Noise 0.26 [-0.14, 0.70] 0.22 25% 0.00 0.484
gradual_information_momentum Factor tilt 0.25 [-0.17, 0.69] -0.83 93% 0.00 0.543
equity_yield_vs_real_rate_gap_timing Noise 0.18 [-0.31, 0.70] -0.75 38% 0.00 0.786
inflation_passthrough_margin_expansion Noise 0.23 [-0.20, 0.69] 0.75 27% 0.00 0.592
profitless_market_cap_share_timing Noise 0.18 [-0.42, 0.77] 0.19 18% 0.00 0.811
sticky_cost_downturn_drag Noise 0.24 [-0.14, 0.62] -0.15 42% 0.00 0.506
quiet_strength_low_attention_high Factor tilt 0.22 [-0.20, 0.65] 1.59 85% 0.00 0.592
low_volatility Noise 0.25 [-0.15, 0.65] 2.39 83% 0.00 0.507
real_yield_shock_timing Noise 0.13 [-0.27, 0.57] 0.48 2% 0.00 0.811
aggregate_rollover_risk_funding_shock_timing Noise 0.17 [-0.35, 0.68] 0.45 7% 0.00 0.811
aggregate_equity_issuance_market_timing Noise 0.20 [-0.34, 0.70] 1.11 7% 0.00 0.786
financial_slack_under_tightening Noise 0.21 [-0.17, 0.60] 0.38 7% 0.00 0.592
tax_loss_selling_seasonal_reversal Noise -0.57 [-1.39, 0.23] -1.06 19% 0.00 0.433
credit_vol_complacency_divergence_timing Noise 0.12 [-0.21, 0.51] -0.47 17% 0.00 0.811
depreciation_conservatism_reserve Noise 0.20 [-0.27, 0.65] 0.77 4% 0.00 0.745
sentiment_misery_residual_timing Noise 0.15 [-0.28, 0.55] 0.89 9% 0.00 0.786
seasonal_risk_appetite_credit_timing Noise 0.20 [-0.19, 0.52] 1.41 4% 0.00 0.649
accruals Noise 0.18 [-0.25, 0.64] 1.96 21% 0.00 0.745
cash_vs_accounting_value_divergence Noise 0.19 [-0.25, 0.64] 1.90 26% 0.00 0.746
residual_vix_hedge_premium Noise 0.18 [-0.25, 0.61] 1.01 15% 0.00 0.720
equity_fear_vs_credit_stress_divergence_timing Noise 0.09 [-0.25, 0.47] -1.11 25% 0.00 0.867
macro_beta_rotation_instability Noise 0.15 [-0.24, 0.54] 1.20 65% 0.00 0.786
effective_tax_rate_earnings_quality Noise 0.15 [-0.20, 0.51] 0.15 6% 0.00 0.781
sentiment_fundamental_gap_timing Noise 0.11 [-0.33, 0.54] 0.73 7% 0.00 0.867
market_breadth_high_ratio_timing Noise 0.12 [-0.24, 0.50] 2.64 24% 0.00 0.811
macro_exposure_drift Noise 0.13 [-0.26, 0.51] 1.04 66% 0.00 0.811
equity_duration_rate_trend_interaction Noise 0.13 [-0.37, 0.68] 1.28 7% 0.00 0.858
macro_trend_exposure_alignment Noise 0.01 [-0.47, 0.48] 0.15 24% 0.00 0.979
unproductive_acquisition_growth Noise 0.12 [-0.31, 0.58] -0.27 13% 0.00 0.838
credit_equity_divergence_timing Noise 0.08 [-0.39, 0.48] 0.21 3% 0.00 0.872
credit_spread_timing Noise 0.09 [-0.40, 0.52] 0.60 15% 0.00 0.872
market_trend_timing Noise 0.06 [-0.43, 0.53] 1.17 36% 0.00 0.919
macro_beta_instability Factor tilt 0.10 [-0.27, 0.45] 1.38 72% 0.00 0.864
credit_beta_defensive Noise 0.11 [-0.29, 0.44] 0.25 34% 0.00 0.833
oil_beta_trend_interaction Noise 0.10 [-0.33, 0.50] -0.49 16% 0.00 0.867
hidden_dilution_sbc_yield Factor tilt 0.10 [-0.32, 0.50] -0.82 75% 0.00 0.867
book_to_market Loses to factors 0.11 [-0.36, 0.58] -3.14 92% 0.00 0.867
sbc_inflated_operating_cash_flow Noise 0.10 [-0.37, 0.55] -1.13 53% 0.00 0.867
macro_momentum_diffusion_timing Noise 0.06 [-0.46, 0.59] 1.02 29% 0.00 0.930
money_illusion_real_yield_gap_timing Noise 0.05 [-0.34, 0.49] -1.86 39% 0.00 0.912
vix_conditioned_reversal_flip Noise 0.06 [-0.25, 0.39] -0.70 11% 0.00 0.872
usd_exposure_dollar_trend_interaction Noise -0.10 [-0.58, 0.42] -0.50 25% 0.00 0.867
vix_beta_drift Noise 0.07 [-0.41, 0.52] 0.02 15% 0.00 0.886
turn_of_year_tax_loss_rebound_intensity Noise 0.07 [-0.31, 0.46] 0.82 4% 0.00 0.872
macro_trend_beta_alignment Noise -0.20 [-0.77, 0.39] -0.94 40% 0.00 0.809
macro_beta_churn_instability Factor tilt 0.07 [-0.30, 0.44] 0.88 70% 0.00 0.872
dollar_trend_fx_beta_interaction Noise -0.08 [-0.55, 0.39] -0.58 26% 0.00 0.872
stock_comp_earnings_dilution Noise 0.08 [-0.39, 0.53] -1.27 53% 0.00 0.872
macro_exposure_instability Noise 0.05 [-0.33, 0.41] 0.65 66% 0.00 0.912
net_trade_credit_expansion Noise 0.04 [-0.31, 0.41] -0.47 9% 0.00 0.940
stock_comp_burden_yield Noise 0.04 [-0.36, 0.49] 1.09 24% 0.00 0.944
aggregate_inventory_overhang_timing Noise -0.02 [-0.57, 0.48] 1.07 38% 0.00 0.975
dollar_liquidity_cycle_timing Noise -0.18 [-0.70, 0.47] 0.32 17% 0.00 0.811
unemployment_news_regime_asymmetry_timing Noise -0.02 [-0.58, 0.55] 0.55 13% 0.00 0.979
macro_beta_churn Noise 0.03 [-0.34, 0.39] 0.73 70% 0.00 0.950
macro_beta_instability_ambiguity Noise 0.03 [-0.34, 0.39] 0.73 70% 0.00 0.950
cross_sectional_return_dispersion_timing Noise -0.05 [-0.62, 0.45] 0.49 28% 0.00 0.944
macro_pessimism_contrarian_market_timing Noise 0.00 [-0.54, 0.51] -0.16 5% 0.00 0.986
aggregate_inventory_sales_cycle_timing Noise -0.02 [-0.40, 0.37] 0.04 6% 0.00 0.975
rate_duration_mispricing Noise 0.03 [-0.36, 0.50] 1.05 29% 0.00 0.963
funding_conditioned_beta_tilt Noise 0.03 [-0.35, 0.38] -0.47 7% 0.00 0.950
book_tax_gap_earnings_quality Noise 0.03 [-0.38, 0.42] -0.97 13% 0.00 0.966
variance_risk_premium_timing Noise -0.01 [-0.46, 0.45] -1.67 31% 0.00 0.986
aggregate_earnings_growth_breadth_timing Noise -0.05 [-0.52, 0.40] -0.51 21% 0.00 0.944
unemployment_trough_breakout_timing Noise -0.02 [-0.57, 0.56] 0.22 10% 0.00 0.979
sentiment_fundamental_divergence_timing Noise -0.10 [-0.67, 0.43] -0.12 17% 0.00 0.872
macro_risk_on_breadth_contrarian_timing Noise -0.02 [-0.50, 0.43] -1.08 30% 0.00 0.979
rollover_risk_credit_cycle_interaction Noise 0.01 [-0.39, 0.45] 1.40 12% 0.00 0.979
aggregate_illiquidity_shock_timing Noise -0.04 [-0.50, 0.36] -1.47 29% 0.00 0.950
turn_of_year_tax_loss_pressure Noise -0.96 [-1.85, -0.15] -1.16 22% 0.00 0.128
sahm_labor_deterioration_timing Noise -0.02 [-0.61, 0.53] 0.20 11% 0.00 0.979
hidden_rate_duration_mismatch Noise -0.01 [-0.47, 0.51] 1.13 12% 0.00 0.986
long_term_reversal_sentiment_conditioned Noise -0.10 [-0.65, 0.40] -0.87 40% 0.00 0.867
sentiment_gap_contrarian_timing Noise -0.12 [-0.74, 0.50] -0.69 9% 0.00 0.867
aggregate_turnover_sentiment_timing Noise -0.10 [-0.52, 0.37] -1.43 20% 0.00 0.867
household_sentiment_fundamental_divergence_timing Noise -0.17 [-0.63, 0.30] -1.11 11% 0.00 0.802
sentiment_fundamental_disagreement_timing Noise -0.23 [-0.71, 0.20] -0.84 9% 0.00 0.592
sentiment_capitulation_recovery_timing Noise -0.09 [-0.58, 0.44] -0.58 14% 0.00 0.872
labor_cycle_turning_point_timing Noise -0.21 [-0.86, 0.36] -0.02 29% 0.00 0.803
hidden_equity_dilution_cost Noise -0.08 [-0.52, 0.28] -1.31 59% 0.00 0.867
attention_shock_amplified_reversal Noise -0.09 [-0.46, 0.25] -2.14 22% 0.00 0.867
halloween_volatility_scaled_timing Noise -0.11 [-0.47, 0.27] 0.30 5% 0.00 0.838
hidden_refinancing_rate_exposure Noise -0.07 [-0.41, 0.27] -1.71 16% 0.00 0.867
cross_asset_news_diffusion_momentum Noise -0.08 [-0.48, 0.32] 0.38 8% 0.00 0.872
sentiment_hard_data_divergence_timing Noise -0.21 [-0.72, 0.27] -0.55 21% 0.00 0.745
fundamental_momentum_price_gap Factor tilt -0.10 [-0.43, 0.26] 0.55 72% 0.00 0.838
net_cash_yield_rate_change_interaction Noise -0.09 [-0.48, 0.32] -1.04 12% 0.00 0.867
credit_sensitivity_amplified_balance_sheet_safety Noise -0.10 [-0.55, 0.34] 2.54 60% 0.00 0.867
macro_trend_beta_spillover Noise -0.17 [-0.60, 0.28] -0.99 35% 0.00 0.786
macro_trend_attributed_return_reversal Noise -0.13 [-0.47, 0.25] 0.13 39% 0.00 0.803
transitory_nonoperating_income_intensity Noise -0.14 [-0.59, 0.30] -0.08 39% 0.00 0.822
consumer_sentiment_credit_divergence_timing Noise -0.24 [-0.67, 0.19] -1.41 7% 0.00 0.636
real_policy_stance_timing Noise -0.16 [-0.55, 0.33] -2.07 21% 0.00 0.802
seasonal_same_month_return_persistence Noise -0.28 [-0.76, 0.11] -1.44 53% 0.00 0.441
macro_beta_dispersion_crowding_rotation Noise -0.17 [-0.62, 0.23] -1.97 24% 0.00 0.774
abnormal_reporting_lag Noise -0.16 [-0.60, 0.29] 0.17 21% 0.00 0.802
rate_beta_duration_mismatch Noise -0.20 [-0.74, 0.25] -2.15 13% 0.00 0.664
valuation_dispersion_market_timing Noise -0.31 [-0.83, 0.17] -0.22 32% 0.00 0.491
conditional_size_credit_cycle Noise -0.25 [-0.71, 0.23] -1.02 12% 0.00 0.592
operating_leverage_business_cycle Noise -0.19 [-0.69, 0.28] -0.56 3% 0.00 0.786
pure_calendar_month_seasonality_spread Noise -0.29 [-0.70, 0.12] -1.61 5% 0.00 0.441
attention_shock_at_the_highs Loses to factors -0.29 [-0.69, 0.09] -2.96 84% 0.00 0.373
taylor_rule_policy_accommodation_timing Noise -0.35 [-0.72, 0.07] -2.65 16% 0.00 0.372
working_capital_release_under_tightening Noise -0.29 [-0.68, 0.10] -0.56 7% 0.00 0.402
seasonal_same_month_residual_return Noise -0.38 [-0.81, 0.01] -1.92 4% 0.00 0.254
attention_shock_return_reversal Noise -0.42 [-0.78, -0.10] -3.92 31% 0.00 0.053
turnover_shock_price_run_reversal Noise -0.42 [-0.78, -0.10] -3.92 31% 0.00 0.053
earnings_smoothing_detection Noise -0.35 [-0.81, 0.15] -2.36 34% 0.00 0.425
same_quarter_earnings_seasonality Noise -0.29 [-0.72, 0.16] -0.68 23% 0.00 0.441
sbc_inflated_cash_earnings Loses to factors -0.31 [-0.78, 0.12] -3.55 61% 0.00 0.391
credit_beta_leverage_mismatch Factor tilt -0.28 [-0.63, 0.10] 0.22 73% 0.00 0.439
same_month_return_seasonality_spread Noise -0.45 [-0.95, 0.02] -1.72 13% 0.00 0.219
oil_shock_demand_supply_timing Noise -0.30 [-0.68, 0.12] -1.32 13% 0.00 0.433
unpriced_leverage_credit_insensitivity Factor tilt -0.32 [-0.66, 0.05] -0.14 73% 0.00 0.369
oil_demand_vs_supply_shock_timing Noise -0.40 [-0.83, 0.01] -1.42 3% 0.00 0.280
earnings_smoothing_opacity Noise -0.42 [-0.85, 0.01] -2.74 41% 0.00 0.261
earnings_smoothness_vs_cashflow Noise -0.42 [-0.86, 0.00] -2.78 41% 0.00 0.258
earnings_smoothing_vs_cashflow_volatility Noise -0.41 [-0.84, 0.01] -2.58 42% 0.00 0.249
same_month_return_seasonality Noise -0.65 [-1.19, -0.18] -2.59 10% 0.00 0.046
annual_return_seasonality_spread Noise -0.66 [-1.20, -0.19] -2.60 10% 0.00 0.046
external_finance_dependence_credit_shock Noise -0.46 [-0.90, -0.01] -1.96 1% 0.00 0.165
annual_return_seasonality_purged Noise -0.62 [-1.11, -0.18] -2.03 4% 0.00 0.061
same_month_seasonality_ex_adjacent Noise -0.62 [-1.11, -0.18] -2.03 4% 0.00 0.061
size_premium_credit_conditions_switch Noise -0.44 [-0.85, -0.03] -2.13 12% 0.00 0.199
same_month_seasonality_excess Noise -0.65 [-1.13, -0.24] -2.45 4% 0.00 0.039
relative_month_of_year_seasonality Noise -0.65 [-1.15, -0.23] -2.27 4% 0.00 0.040
same_month_seasonality_residual Noise -0.65 [-1.15, -0.23] -2.27 4% 0.00 0.040
annual_return_seasonality Noise -0.70 [-1.26, -0.24] -2.53 7% 0.00 0.039
zero_earnings_threshold_management Noise -0.56 [-1.00, -0.16] -1.06 46% 0.00 0.048
same_month_seasonal_persistence Noise -0.69 [-1.12, -0.30] -2.68 3% 0.00 0.015
pre_earnings_filing_clock_premium Noise -0.47 [-0.85, -0.10] -2.51 2% 0.00 0.073
hidden_credit_risk_mismatch Noise -0.48 [-0.81, -0.15] -1.29 46% 0.00 0.079
same_month_minus_offseason_seasonality Noise -0.73 [-1.20, -0.30] -3.02 5% 0.00 0.021
same_calendar_month_return_seasonality Noise -0.71 [-1.27, -0.22] -2.96 3% 0.00 0.040
same_calendar_month_seasonality Noise -0.71 [-1.27, -0.22] -2.96 3% 0.00 0.040
leverage_credit_beta_complacency Noise -0.51 [-0.86, -0.19] -1.40 49% 0.00 0.061
stock_comp_dilution_earnings_overstatement Noise -0.58 [-1.08, -0.06] -3.40 43% 0.00 0.090
residual_same_month_seasonality Noise -0.80 [-1.32, -0.32] -3.36 5% 0.00 0.015
same_month_annual_seasonality Noise -0.75 [-1.33, -0.25] -3.15 2% 0.00 0.035
same_month_seasonal_return_persistence Noise -0.75 [-1.33, -0.25] -3.15 2% 0.00 0.035
expected_earnings_announcement_month Noise -0.57 [-0.95, -0.20] -2.46 3% 0.00 0.046
tax_loss_overhang_seasonal Noise -0.50 [-0.90, -0.06] -1.82 6% 0.00 0.131
tax_loss_selling_turn_of_year_reversal Noise -0.47 [-0.78, -0.11] -2.26 3% 0.00 0.081
attention_spike_return_reversal Noise -0.75 [-1.28, -0.33] -5.32 10% 0.00 0.004
tax_loss_overhang_turn_of_year Noise -0.45 [-0.73, -0.12] -2.28 2% 0.00 0.104
macro_loading_instability Noise -0.70 [-1.08, -0.31] -3.42 10% 0.00 0.015
filing_staleness_information_neglect Noise -1.05 [-1.47, -0.67] -4.39 1% 0.00 0.000

Best signal vs the market

The top of the board is gross_profitability. Here it is against a large-cap benchmark — the 500 biggest eligible names each month, cap-weighted, rebalanced monthly, run through the identical point-in-time and delisting machinery.

Best signal versus large-cap benchmark equity curves and drawdowns

Cumulative growth and drawdown of the best signal against the cap-weighted large-cap benchmark. Log scale on the upper panel.
Metric gross_profitability Benchmark
Annualized return 11.1% 8.3%
Annualized volatility 12.3% 15.5%
Sharpe ratio 0.92 0.60
Max drawdown -21.3% -45.2%
Hit rate (months up) 62.9% 65.4%

These are not the same kind of return. The signal is a market-neutral long/short book: it holds the top decile and shorts the bottom, so it carries almost no market exposure and its return does not depend on the market going up. The benchmark is long-only and fully exposed. Comparing their raw returns rewards whichever one happened to suit the sample — the relationship between them is the informative part:

Correlation to the benchmark is -0.11 and beta is -0.09. A beta near zero is the point of a long/short construction, not a shortfall: it means the return is not repackaged market exposure. Up-capture is 19% and down-capture -39% — how much of the market’s average up-month and down-month the strategy tracked. Low or negative down-capture is the property a diversifying book is actually bought for.

Measured over 280 overlapping months. A market-neutral book with a lower headline return than a bull-market index can still be the more valuable asset in a portfolio, because it supplies a return stream the index cannot.

The factors and the statistics, in depth

The four factors — what they are and how they are built here click to expand

A new signal is only interesting if it earns something the known premia do not already pay for. These four are the standard yardstick. All are built from the same panel the signals are scored on — equal-weighted top-third minus bottom-third spreads on the same eligible universe — so attribution is like-for-like rather than against an external dataset with different conventions.

MKT — Market
The excess return of holding equities at all. Every long-only strategy earns most of its return here, so it is the first thing to strip out.Built here as: Equal-weighted return of the whole eligible universe each month.
SMB — Size (small minus big)
Small-cap stocks have historically earned more than large caps — compensation for illiquidity and distress risk, and partly a reflection of how hard small names are to trade at size.Built here as: Long the smallest third by market cap, short the largest third.
HML — Value (high minus low)
Cheap stocks relative to book value have outearned expensive ones. The debate over whether this is risk compensation or a behavioural mispricing is unsettled; either way it is a known, harvestable premium, so a new signal should not be paid twice for it.Built here as: Long the cheapest third by price-to-book, short the most expensive third.
UMD — Momentum (up minus down)
Past winners keep winning over horizons of roughly three to twelve months, usually attributed to under-reaction to news and to slow diffusion of information.Built here as: Long the top third by 12-1 month return, short the bottom third.
The statistics — what each one answers, and the failure it guards against click to expand

Each statistic exists because a simpler one can be fooled in a specific way. Read top to bottom and the argument builds: a Sharpe is not enough, so bound it; a bound is not enough on skewed returns, so reshape it; and none of it means anything until you price in how many strategies were tried to find this one.

Sharpe ratio
Return per unit of volatility, annualized. The default headline — and the single most misleading number on any farmed leaderboard, because it says nothing about how many strategies were tried before this one was picked.Sharpe = mean(r) / sd(r) × √12
Guards against: Says nothing about selection. Rewards fat left tails.
95% confidence interval
A circular block bootstrap resamples contiguous blocks of months — preserving short-run autocorrelation an i.i.d. bootstrap would destroy — and re-computes the Sharpe a few thousand times. The interval is the middle 95% of that distribution. If it straddles zero, the point estimate is not doing the work you think it is.block length ≈ T^(1/3), 2,000 resamples
Guards against: A single Sharpe with no interval invites false confidence.
Newey-West t-statistic
The t-stat of the mean return, with standard errors corrected for autocorrelation and heteroskedasticity. Monthly strategy returns are rarely independent, and ignoring that inflates significance.t = mean(r) / SE_NW(r), lags = ⌊4(T/100)^(2/9)⌋
Guards against: Plain OLS errors overstate significance on persistent series.
Factor R²
The share of the strategy’s month-to-month variance explained by regressing it on market, size, value and momentum. High R² means you have largely rebuilt a portfolio of things that already have names.R² from r_t = α + β₁MKT + β₂SMB + β₃HML + β₄UMD + ε_t
Guards against: A high Sharpe can be entirely factor exposure.
Alpha t-statistic
The intercept of that same regression — the return the four factors cannot explain — divided by its Newey-West standard error. This is the closest thing here to a test of whether a signal is genuinely new. Note the sign: a large negative alpha t means the construction loses to the factor it tracks.α from the regression above; t = α / SE_NW(α)
Guards against: Sharpe alone cannot tell a discovery from a repackaged tilt.
Probabilistic Sharpe (PSR)
The probability the true Sharpe exceeds zero, adjusted for skew and kurtosis. Strategies that make small gains most months and lose badly occasionally — the classic short-leg blow-up shape — get marked down relative to their raw Sharpe.PSR = Φ( (SR − SR*)·√(T−1) / √(1 − γ₃SR + (γ₄−1)SR²/4) )
Guards against: Raw Sharpe assumes normality that returns rarely have.
Search hurdle
The Sharpe you would expect from the best of N random strategies, given how dispersed the Sharpes in this library actually are. It is the bar luck alone clears. Anything under it is not evidence of anything.E[max SR] ≈ √V · [ (1−γ)Φ⁻¹(1 − 1/N) + γΦ⁻¹(1 − 1/(Ne)) ]
Guards against: Testing many strategies guarantees a good-looking winner.
Deflated Sharpe (DSR)
The PSR measured against that hurdle instead of against zero: the probability the signal is real given the size of the search that found it. This is what the board is sorted by. N counts every farming attempt in the ledger — including the ideas that died at the data or compile stage — because counting only survivors would understate the search and flatter every number.DSR = PSR evaluated at SR* = E[max SR]
Guards against: The headline result of a bulk search is biased upward by construction.
Benjamini-Hochberg q-value
Ranks every p-value in the library and controls the expected proportion of false discoveries among those called significant. Far less brutal than Bonferroni when signals are correlated — which factor strategies always are — while still bounding the damage.q_(i) = min over j ≥ i of p_(j) · m / j
Guards against: Per-signal p-values ignore that the library is a search.

How to read a row in one pass. Start at the deflated Sharpe: if it is low, nothing else on the row rescues the signal. If it is high, check the alpha t-stat to see whether the return is genuinely new or a factor tilt wearing a different name. Then check the confidence interval: if it straddles zero, the sample is too short to lean on regardless of what the point estimates say.

Run history

Run Signals Survivors Hurdle Best Sharpe
2026-10-02 171 0 1.01 0.92
2026-09-15 170 0 1.00 0.92
2026-09-02 121 0 0.95 0.92
2026-09-01 113 0 0.95 0.92
2026-08-31 102 0 0.95 0.92
2026-08-30 96 0 0.94 0.92
2026-08-29 88 0 0.92 0.92
2026-08-27 77 0 0.89 0.92
2026-08-26 69 0 0.89 0.92
2026-08-25 58 0 0.83 0.92
2026-08-24 48 0 0.75 0.92
2026-08-23 39 0 0.66 0.92

Where this goes next

The pipeline works; the constraint now is the hypothesis space, not the machinery. Ranked roughly by expected value per unit of effort:

Widen the data surface

Every signal is bounded by what the panel contains. The highest-leverage additions are short interest and borrow cost (crowding and limits to arbitrage), institutional 13F holdings (ownership churn), analyst revisions and dispersion (expectation dynamics), and options-implied volatility surfaces (skew as a crash-risk premium). Each new provider under data/features/ multiplies the hypothesis space rather than adding to it.

high impact · medium effort

Walk-forward, not full-sample

The harness currently scores each signal over the whole history. A purged, embargoed walk-forward protocol — fit and select on an expanding window, evaluate only on the untouched next block — would turn every number on this page into an out-of-sample one, and would catch signals that only work in the regime they were invented from.

high impact · medium effort

Let the agents read their own failures

The idea agent sees which themes are thin, but not why past ideas died. Feeding back the specific failure — the validator’s complaint, the coverage gate, the factor attribution that revealed a tilt — should raise the hit rate per unit of spend, and it costs nothing but prompt tokens.

high impact · low effort

Search combinations, not just singles

Most published anomalies are weak alone and strong combined. A composite layer — rank-average within a theme, or a shrunk regression on standardized signals — would test whether the farmed library is greater than its parts. The novelty machinery already stores the return streams needed for it.

medium impact · low effort

Model capacity honestly

Costs are a flat per-unit-traded charge. Real capacity binds through market impact that scales with size and inversely with liquidity. A square-root impact model plus an explicit AUM assumption would tell us which of these signals survive at size rather than only on paper.

medium impact · medium effort

Turn the delisting assumption into a measurement

Delisted names currently book a configurable return, defaulted to a liquidation at the last observed price. Sharadar’s corporate-actions table carries the actual delisting events; using them would replace an assumption with data, and would matter most for exactly the distressed, cheap names that value signals load on.

medium impact · low effort

Regime-condition the evaluation

A signal that works only in high-dispersion or high-rate regimes is still useful — if you know which regime you are in. Conditioning the report on macro states already in the panel would separate genuinely broken signals from ones that are simply out of season.

medium impact · medium effort

Raise the bar as the library grows

The deflation hurdle scales with the number of trials, so the more this pipeline runs, the harder it becomes to claim a discovery — correctly. The counter-move is not a lower bar but a better prior: pre-register the economic mechanism before the backtest, and treat signals whose mechanism was stated in advance as a separate, smaller family of tests.

structural · ongoing

The honest summary is that a language model is a good generator of plausible hypotheses and a poor judge of which ones are real. That division of labour is the whole design: let the model propose freely, and make the harness — not the model — decide what counts.

The signal library

All 171 signals — what each one measures, and the code that computes it click to expand

One entry per signal currently in the library: the reference anomalies from the literature and every agent-written signal that compiled and passed validation. The calculation shown is the compute method itself, read straight out of the module at publish time — the page cannot drift away from the code it documents. Each returns a raw score; the harness multiplies it by the expected sign, so after orientation a high score always means “go long”.

Cross-sectional signals (122)

Scored for every stock every month. The strategy is long the top third of the oriented score and short the bottom third, rebalanced monthly.

credit_sensitivity_amplified_balance_sheet_safetycomboInteraction of credit-beta sensitivity with fundamental balance-sheet safety: among stocks whose returns load…

Interaction of credit-beta sensitivity with fundamental balance-sheet safety: among stocks whose returns load heavily on credit-market moves, those with low leverage, high EBIT coverage, high liquidity and cash reserves are over-discounted and expected to outperform.

Category comboDirection Long the high scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    debt = panel['debt']
    assets = panel['assets']
    ebit = panel['ebit']
    currentratio = panel['currentratio']
    cashneq = panel['cashneq']
    liabilities = panel['liabilities']
    beta_credit = panel['beta_credit_252']
    # base validity mask: require all raw inputs present
    base_valid = (
        debt.notna() & assets.notna() & ebit.notna() &
        currentratio.notna() & cashneq.notna() &
        liabilities.notna() & beta_credit.notna()
    )
    assets_pos = assets > 0
    debt_pos = debt > 0
    lev = pd.Series(np.nan, index=panel.index)
    lev_mask = base_valid & assets_pos
    lev.loc[lev_mask] = debt.loc[lev_mask] / assets.loc[lev_mask]
    denom = pd.Series(np.nan, index=panel.index)
    cov_mask = base_valid & debt_pos
    denom.loc[cov_mask] = debt.loc[cov_mask]
    # fallback to eps-like max(debt, eps) per formula, but eps not mapped -> use debt only
    cov = pd.Series(np.nan, index=panel.index)
    cov.loc[cov_mask] = ebit.loc[cov_mask] / denom.loc[cov_mask]
    cov = cov.clip(-5, 5)
    liq = currentratio.clip(upper=5)
    liq = liq.where(base_valid)
    cashr = pd.Series(np.nan, index=panel.index)
    cashr.loc[lev_mask] = cashneq.loc[lev_mask] / assets.loc[lev_mask]
    df = pd.DataFrame({
        'lev': lev,
        'cov': cov,
        'liq': liq,
        'cashr': cashr,
        'beta_credit': beta_credit.where(base_valid),
    }, index=panel.index)
    def per_date(g: pd.DataFrame) -> pd.Series:
        valid_rows = g[['lev', 'cov', 'liq', 'cashr', 'beta_credit']].notna().all(axis=1)
        g_valid = g.loc[valid_rows]
        out = pd.Series(np.nan, index=g.index)
        if g_valid.empty:
            return out
        z_lev = self._zscore(g_valid['lev'])
        z_cov = self._zscore(g_valid['cov'])
        z_liq = self._zscore(g_valid['liq'])
        z_cashr = self._zscore(g_valid['cashr'])
        safety = -z_lev + 0.5 * z_cov + 0.5 * z_liq + 0.5 * z_cashr
        abs_beta = g_valid['beta_credit'].abs()
        w = abs_beta.rank(pct=True)
        sig = safety * (0.25 + w)
        out.loc[g_valid.index] = sig
        return out
    raw_signal = df.groupby(level='date', group_keys=False).apply(per_date)
    # macro_baa_spread past-only z-score scaling
    baa = panel['macro_baa_spread']
    baa_by_date = baa.groupby(level='date').first()
    baa_by_date = baa_by_date.sort_index()
    mean_rolling = baa_by_date.expanding(min_periods=2).mean().shift(1)
    std_rolling = baa_by_date.expanding(min_periods=2).std().shift(1)
    z_baa = (baa_by_date - mean_rolling) / std_rolling
    z_baa = z_baa.replace([np.inf, -np.inf], np.nan)
    z_baa_clipped = z_baa.clip(-1, 1)
    multiplier = 1 + 0.5 * z_baa_clipped
    multiplier = multiplier.fillna(1.0)
    multiplier_full = panel.index.get_level_values('date').map(multiplier)
    multiplier_full = pd.Series(multiplier_full, index=panel.index)
    result = raw_signal * multiplier_full
    result.name = self.name
    return result
funding_conditioned_beta_tiltcomboFlips the sign of the beta tilt with the funding-liquidity cycle, measured by the joint 12-month change in…

Flips the sign of the beta tilt with the funding-liquidity cycle, measured by the joint 12-month change in fed funds rate and Baa credit spread. During funding easing episodes, high-beta names are favored; during tightening, low-beta names are favored.

Category comboDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta = panel['beta_252']
    fedfunds_chg = panel['macro_fedfunds_chg12m']
    baa_chg = panel['macro_baa_spread_chg12m']
    nobs = panel['nobs']
    tighten = fedfunds_chg + baa_chg
    ease = -tighten
    score = beta * np.tanh(ease)
    invalid = beta.isna() | fedfunds_chg.isna() | baa_chg.isna() | (nobs < 200)
    score = score.where(~invalid, np.nan)
    return score.reindex(panel.index)
net_cash_yield_rate_change_interactioncomboInteraction between net-cash-to-marketcap ratio and the signed 12-month change in the 2-year Treasury yield

Interaction between net-cash-to-marketcap ratio and the signed 12-month change in the 2-year Treasury yield. Cash-rich firms benefit from rising short rates via interest income while net-debt firms benefit from falling rates via cheaper refinancing; this macro-conditioned quality/leverage signal flips sign with the direction of the short-rate cycle.

Category comboDirection Long the high scoresData fundamental, price, macro
def compute(self, panel) -> pd.Series:
    cashneq = panel['cashneq']
    debt = panel['debt']
    marketcap = panel['marketcap']
    cond = panel['macro_dgs2_chg12m']
    valid = cashneq.notna() & debt.notna() & marketcap.notna() & (marketcap > 0) & cond.notna()
    net_cash_yield = (cashneq - debt) / marketcap
    net_cash_yield = net_cash_yield.where(valid)
    def winsorize(s):
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    net_cash_yield_w = net_cash_yield.groupby(level='date').transform(winsorize)
    signal = net_cash_yield_w * cond
    signal = signal.where(valid)
    return signal.reindex(panel.index)
operating_leverage_business_cyclecomboOperating leverage (opex/assets) conditioned on the trailing 12m change in industrial production and…

Operating leverage (opex/assets) conditioned on the trailing 12m change in industrial production and recession state: high fixed-cost intensity firms are expected to outperform when industrial activity is improving (and underperform when it is deteriorating), capturing a macro-state-dependent risk premium orthogonal to static value/quality factors.

Category comboDirection Long the high scoresData fundamental, macro
def compute(self, panel) -> pd.Series:
    opex = panel['opex']
    assets = panel['assets']
    revenue = panel['revenue']
    valid = (assets > 0) & (revenue > 0)
    ol = (opex / assets).where(valid)
    ol_clipped = ol.clip(lower=0, upper=2)
    def zscore(s):
        mu = s.mean()
        sd = s.std()
        if sd is None or sd == 0 or pd.isna(sd):
            return pd.Series(np.nan, index=s.index)
        return (s - mu) / sd
    ol_z = ol_clipped.groupby(level='date').transform(zscore)
    indpro_chg = panel['macro_indpro_chg12m']
    usrec = panel['macro_usrec']
    cyc = np.tanh(indpro_chg / 2.0) * (1 - 0.5 * usrec)
    cyc = cyc.where(indpro_chg.notna() & usrec.notna())
    raw = ol_z * cyc
    return raw.reindex(panel.index)
rollover_risk_credit_cycle_interactioncomboInteraction between near-term rollover risk (net current debt / assets) and the 12-month change in the…

Interaction between near-term rollover risk (net current debt / assets) and the 12-month change in the Baa-Treasury credit spread. Firms with high net short-term debt suffer when credit spreads widen and benefit when spreads tighten, so the score sign-flips with the credit cycle.

Category comboDirection Long the high scoresData fundamental, macro, price
def compute(self, panel) -> pd.Series:
    debtc = panel['debtc']
    cashneq = panel['cashneq']
    assets = panel['assets']
    tighten = panel['macro_baa_spread_chg12m']
    valid = debtc.notna() & cashneq.notna() & assets.notna() & tighten.notna() & (assets > 0)
    rollover = (debtc - cashneq) / assets.where(assets > 0)
    rollover = rollover.clip(-1, 1)
    score = -rollover * tighten
    score = score.where(valid, np.nan)
    return score.reindex(panel.index)
asset_growthinvestmentCooper-Gulen-Schill asset growth (YoY); low-growth firms outperform

Cooper-Gulen-Schill asset growth (YoY); low-growth firms outperform.

Category investmentDirection Long the low scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    assets = panel["assets"].where(panel["assets"] > 0)
    return _yoy(assets)
external_finance_dependence_credit_shockinvestmentRajan-Zingales style external finance dependence (capex exceeding internal operating cash flow) combined with…

Rajan-Zingales style external finance dependence (capex exceeding internal operating cash flow) combined with short-term debt load, interacted with the 12-month change in the Baa credit spread. Firms with high financing need are hurt when credit spreads widen and helped when spreads tighten; the harness orients the score so that expected sign is positive.

Category investmentDirection Long the high scoresData fundamental, macro
def compute(self, panel) -> pd.Series:
    assets = panel['assets']
    capex = panel['capex']
    ncfo = panel['ncfo']
    debtc = panel['debtc']
    macro_chg = panel['macro_baa_spread_chg12m']
    valid_assets = assets > 0
    dep = pd.Series(np.nan, index=panel.index)
    stdebt = pd.Series(np.nan, index=panel.index)
    dep_valid = valid_assets & capex.notna() & ncfo.notna() & assets.notna()
    dep.loc[dep_valid] = (capex.loc[dep_valid].abs() - ncfo.loc[dep_valid]) / assets.loc[dep_valid]
    stdebt_valid = valid_assets & debtc.notna() & assets.notna()
    stdebt.loc[stdebt_valid] = debtc.loc[stdebt_valid] / assets.loc[stdebt_valid]
    base_missing = (
        (~valid_assets)
        | capex.isna()
        | ncfo.isna()
        | assets.isna()
        | debtc.isna()
        | macro_chg.isna()
    )
    dep = dep.where(~base_missing)
    stdebt = stdebt.where(~base_missing)
    def cross_section_z(group: pd.Series) -> pd.Series:
        n_valid = group.notna().sum()
        if n_valid < 20:
            return pd.Series(np.nan, index=group.index)
        return _zscore(group)
    dep_z = dep.groupby(level='date').apply(cross_section_z)
    dep_z.index = dep_z.index.droplevel(0) if isinstance(dep_z.index, pd.MultiIndex) and dep_z.index.nlevels > panel.index.nlevels else dep_z.index
    stdebt_z = stdebt.groupby(level='date').apply(cross_section_z)
    stdebt_z.index = stdebt_z.index.droplevel(0) if isinstance(stdebt_z.index, pd.MultiIndex) and stdebt_z.index.nlevels > panel.index.nlevels else stdebt_z.index
    need = dep_z + stdebt_z
    tight = np.tanh(macro_chg / 0.5)
    raw_signal = -need * tight
    raw_signal = raw_signal.where(~base_missing)
    return raw_signal.reindex(panel.index)
inventory_growth_gapinvestmentGap between inventory growth and sales growth over trailing 12 months

Gap between inventory growth and sales growth over trailing 12 months. High inventory growth relative to sales growth signals over-optimistic demand forecasts, future write-downs, and margin pressure, predicting negative future abnormal returns.

Category investmentDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    inventory = panel['inventory']
    revenue = panel['revenue']
    assets = panel['assets']
    inv_l12 = inventory.groupby(level='ticker').shift(12)
    rev_l12 = revenue.groupby(level='ticker').shift(12)
    inv_g = pd.Series(np.nan, index=panel.index)
    valid_inv = inv_l12 > 0
    inv_g[valid_inv] = inventory[valid_inv] / inv_l12[valid_inv] - 1.0
    sales_g = pd.Series(np.nan, index=panel.index)
    valid_rev = rev_l12 > 0
    sales_g[valid_rev] = revenue[valid_rev] / rev_l12[valid_rev] - 1.0
    inv_g_clipped = inv_g.clip(-1, 3)
    sales_g_clipped = sales_g.clip(-1, 3)
    raw = inv_g_clipped - sales_g_clipped
    valid_mask = (
        (assets > 0)
        & (inventory > 0)
        & (inv_l12 > 0)
        & (revenue > 0)
        & (rev_l12 > 0)
    )
    raw = raw.where(valid_mask, np.nan)
    return raw
late_quarter_buyback_share_shrinkageinvestmentSignal detects late-quarter share buybacks by comparing weighted-average shares to point-in-time basic shares…

Signal detects late-quarter share buybacks by comparing weighted-average shares to point-in-time basic shares outstanding: sharesbas materially below shareswa implies repurchases concentrated late in the quarter, not yet reflected in annual issuance measures. The signal is confirmed with actual cash outflow for equity (negative ncfcommon relative to market cap) to filter mechanical share-count artifacts.

Category investmentDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    shareswa = panel['shareswa']
    sharesbas = panel['sharesbas']
    ncfcommon = panel['ncfcommon']
    marketcap = panel['marketcap']
    valid_shares = (shareswa > 0) & (sharesbas > 0)
    shrink = (shareswa - sharesbas) / shareswa.where(shareswa > 0)
    shrink = shrink.where(valid_shares)
    cash_conf = (-ncfcommon / marketcap.where(marketcap > 0))
    cash_conf = cash_conf.where((marketcap > 0) & ncfcommon.notna(), 0.0)
    cash_conf = cash_conf.clip(-0.25, 0.25)
    signal = shrink + 0.5 * cash_conf
    invalid = shrink.isna() | (shrink.abs() > 0.5)
    signal = signal.where(~invalid, np.nan)
    return signal.reindex(panel.index)
net_share_issuanceinvestmentYoY growth in shares outstanding; net issuers underperform, repurchasers outperform

YoY growth in shares outstanding; net issuers underperform, repurchasers outperform.

Category investmentDirection Long the low scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    shares = panel["sharesbas"].where(panel["sharesbas"] > 0)
    return _yoy(shares)
payout_funding_gapinvestmentPayout-versus-free-cash-flow funding gap: payout = max(-ncfdiv,0) + max(-ncfcommon,0); gap = (payout - fcf) /…

Payout-versus-free-cash-flow funding gap: payout = max(-ncfdiv,0) + max(-ncfcommon,0); gap = (payout - fcf) / assets, winsorized at 1/99% per date. Firms paying out far more cash than their free cash flow supports are unlikely to sustain the distribution and tend to underperform; self-funded payers (negative gap) tend to outperform.

Category investmentDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    ncfdiv = panel['ncfdiv']
    ncfcommon = panel['ncfcommon']
    fcf = panel['fcf']
    assets = panel['assets']
    valid = ncfdiv.notna() & ncfcommon.notna() & fcf.notna() & assets.notna() & (assets > 0)
    payout = (-ncfdiv).clip(lower=0) + (-ncfcommon).clip(lower=0)
    gap = (payout - fcf) / assets
    gap = gap.where(valid, np.nan)
    def _winsorize(s: pd.Series) -> pd.Series:
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    gap = gap.groupby(level='date').transform(_winsorize)
    return gap.reindex(panel.index)
unproductive_acquisition_growthinvestmentGrowth in goodwill/intangibles (acquisition footprint) net of revenue growth

Growth in goodwill/intangibles (acquisition footprint) net of revenue growth. High acquisition-driven balance-sheet expansion unaccompanied by revenue growth signals overpayment/empire-building and predicts lower future returns.

Category investmentDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    intangibles = panel['intangibles']
    assets = panel['assets']
    revenue = panel['revenue']
    grp = panel.index.get_level_values('ticker')
    lag_intangibles = intangibles.groupby(grp).shift(12)
    lag_assets = assets.groupby(grp).shift(12)
    lag_revenue = revenue.groupby(grp).shift(12)
    acq = (intangibles - lag_intangibles) / lag_assets
    acq = acq.where(lag_assets > 0, np.nan)
    acq = acq.where(intangibles.notna() & lag_intangibles.notna(), np.nan)
    acq = acq.clip(-1, 1)
    rev_g = (revenue - lag_revenue) / lag_revenue.abs()
    rev_g = rev_g.where(lag_revenue > 0, np.nan)
    rev_g = rev_g.where(revenue.notna() & lag_revenue.notna(), np.nan)
    rev_g = rev_g.clip(-2, 2)
    df = pd.DataFrame({'acq': acq, 'rev_g': rev_g})
    dates = panel.index.get_level_values('date')
    def cs_zscore(s: pd.Series) -> pd.Series:
        mean = s.groupby(dates).transform('mean')
        std = s.groupby(dates).transform('std')
        z = (s - mean) / std
        return z.where(std > 0, np.nan)
    z_acq = cs_zscore(df['acq'])
    z_rev = cs_zscore(df['rev_g'])
    signal = z_acq - z_rev
    signal = signal.where(df['acq'].notna() & df['rev_g'].notna(), np.nan)
    return signal.reindex(panel.index)
credit_beta_defensivelow_riskCross-asset correlation factor: prefer defensive stocks least hurt by credit-spread widening (higher corr to…

Cross-asset correlation factor: prefer defensive stocks least hurt by credit-spread widening (higher corr to spread changes).

Category low_riskDirection Long the high scoresData price, macro
def compute(self, panel: pd.DataFrame) -> pd.Series:
    return panel["corr_credit_252"]
credit_beta_leverage_mismatchlow_riskCross-asset residual comparing equity’s market-implied distress sensitivity (negative of credit beta) to…

Cross-asset residual comparing equity’s market-implied distress sensitivity (negative of credit beta) to balance-sheet net leverage. Stocks whose equity underprices credit risk relative to true leverage (implied distress low, leverage high) are expected to underperform; the reverse mismatch is expected to earn a premium.

Category low_riskDirection Long the high scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    beta_credit = panel['beta_credit_252']
    corr_credit = panel['corr_credit_252']
    debt = panel['debt']
    cashneq = panel['cashneq']
    assets = panel['assets']
    valid_beta_corr = beta_credit.notna() & corr_credit.notna()
    implied_distress = -beta_credit
    implied_distress = implied_distress.where(
        valid_beta_corr & (corr_credit.abs() >= 0.02)
    )
    assets_valid = assets.notna() & (assets > 0)
    netlev = (debt - cashneq) / assets
    netlev = netlev.where(assets_valid)
    netlev = netlev.clip(lower=-1.0, upper=2.0)
    valid = valid_beta_corr & assets_valid
    implied_distress = implied_distress.where(valid)
    netlev = netlev.where(valid)
    def zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.replace(0, np.nan)
    z1 = zscore(implied_distress)
    z2 = zscore(netlev)
    signal = z1 - z2
    signal = signal.where(z1.notna() & z2.notna())
    return signal.reindex(panel.index)
leverage_credit_beta_complacencylow_riskInteraction of balance-sheet net leverage with complacency toward credit risk (low absolute correlation to…

Interaction of balance-sheet net leverage with complacency toward credit risk (low absolute correlation to credit-spread changes). Highly levered firms whose equity returns are largely uncorrelated with credit conditions are hypothesized to harbor unpriced distress risk, predicting subsequent underperformance.

Category low_riskDirection Long the low scoresData fundamental, price, macro
def compute(self, panel) -> pd.Series:
    debt = panel['debt']
    cashneq = panel['cashneq']
    assets = panel['assets']
    corr_credit = panel['corr_credit_252']
    valid_assets = assets > 0
    netlev = (debt - cashneq) / assets.where(valid_assets)
    netlev = netlev.where(valid_assets)
    complacency = 1.0 - corr_credit.abs()
    complacency = complacency.clip(lower=0.0, upper=1.0)
    # drop rows missing required inputs
    base_valid = debt.notna() & cashneq.notna() & assets.notna() & valid_assets & corr_credit.notna()
    netlev = netlev.where(base_valid)
    complacency = complacency.where(base_valid)
    lev_r = netlev.groupby(level='date').rank(pct=True)
    comp_r = complacency.groupby(level='date').rank(pct=True)
    signal = lev_r * comp_r
    signal = signal.where(lev_r.notna() & comp_r.notna())
    return signal.reindex(panel.index)
lottery_demand_sentiment_interactionlow_riskInteraction of a lottery-stock proxy (high vol, low nominal price, small size) with a publication-lagged…

Interaction of a lottery-stock proxy (high vol, low nominal price, small size) with a publication-lagged consumer-sentiment state variable. Lottery stocks are expected to be overpriced (and thus underperform) when sentiment/optimism is high, with the effect attenuating or reversing after prolonged pessimism.

Category low_riskDirection Long the high scoresData price, macro
def compute(self, panel: pd.DataFrame) -> pd.Series:
    vol_252 = panel['vol_252']
    closeunadj = panel['closeunadj']
    marketcap = panel['marketcap']
    log_price = np.log(closeunadj.where(closeunadj > 0))
    log_mcap = np.log(marketcap.where(marketcap > 0))
    neg_log_price = -log_price
    neg_log_mcap = -log_mcap
    df = pd.DataFrame({
        'vol_252': vol_252,
        'neg_log_price': neg_log_price,
        'neg_log_mcap': neg_log_mcap,
    }, index=panel.index)
    r_vol = df.groupby(level='date')['vol_252'].transform(_rank_scaled)
    r_price = df.groupby(level='date')['neg_log_price'].transform(_rank_scaled)
    r_mcap = df.groupby(level='date')['neg_log_mcap'].transform(_rank_scaled)
    lottery = r_vol + r_price + r_mcap
    lottery = lottery.where(
        vol_252.notna() & (closeunadj > 0) & (marketcap > 0)
    )
    # date-level sentiment series
    umcsent = panel['macro_umcsent'].groupby(level='date').first()
    umcsent_chg12m = panel['macro_umcsent_chg12m'].groupby(level='date').first()
    umcsent = umcsent.sort_index()
    umcsent_chg12m = umcsent_chg12m.sort_index()
    # backward-looking: use shift(1) before rolling to avoid look-ahead
    umcsent_lag = umcsent.shift(1)
    roll_mean = umcsent_lag.rolling(120, min_periods=36).mean()
    roll_std = umcsent_lag.rolling(120, min_periods=36).std()
    sz = (umcsent - roll_mean) / roll_std
    sz = sz.clip(-2, 2)
    chg_lag = umcsent_chg12m.shift(1)
    chg_roll_mean = chg_lag.rolling(120, min_periods=36).mean()
    chg_roll_std = chg_lag.rolling(120, min_periods=36).std()
    chg_z = (umcsent_chg12m - chg_roll_mean) / chg_roll_std
    chg_z = chg_z.clip(-2, 2)
    sent_state = 0.5 * sz + 0.5 * chg_z
    # broadcast sent_state to panel index
    sent_state_full = panel.index.get_level_values('date').map(sent_state)
    sent_state_full = pd.Series(sent_state_full, index=panel.index)
    score = -lottery * sent_state_full
    valid = (
        lottery.notna()
        & sent_state_full.notna()
        & panel['macro_umcsent'].notna()
        & panel['macro_umcsent_chg12m'].notna()
    )
    score = score.where(valid)
    return score
low_volatilitylow_riskLow-volatility anomaly: trailing 1y realized vol of daily returns (low = long)

Low-volatility anomaly: trailing 1y realized vol of daily returns (low = long).

Category low_riskDirection Long the low scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    return panel["vol_252"]
macro_beta_churnlow_riskMeasures churn/instability in a stock’s trailing macro factor betas (credit, rates, oil, usd, vix) over 12…

Measures churn/instability in a stock’s trailing macro factor betas (credit, rates, oil, usd, vix) over 12 months, cross-sectionally z-scored per factor per date and averaged. High churn indicates an unstable risk profile that predicts lower future risk-adjusted returns.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    factors = [
        'beta_credit_252',
        'beta_rates_252',
        'beta_oil_252',
        'beta_usd_252',
        'beta_vix_252',
    ]
    z_scores = []
    for f in factors:
        cur = panel[f]
        lag = cur.groupby(level='ticker').shift(12)
        d = (cur - lag).abs()
        d = d.where(cur.notna() & lag.notna())
        std_by_date = d.groupby(level='date').transform('std')
        z = d / std_by_date.where(std_by_date > 0)
        z_scores.append(z)
    z_df = pd.concat(z_scores, axis=1)
    z_df.columns = factors
    count = z_df.notna().sum(axis=1)
    mean_z = z_df.mean(axis=1, skipna=True)
    signal = mean_z.where(count >= 3)
    signal.name = self.name
    return signal
macro_beta_churn_instabilitylow_riskMeasures instability of a stock’s macro factor exposures (rates, credit, oil, usd, vix betas) over the…

Measures instability of a stock’s macro factor exposures (rates, credit, oil, usd, vix betas) over the trailing 12 months. High churn in factor loadings implies ambiguous risk profile and speculative-flow-driven comovement, predicting underperformance; stable-exposure firms earn higher realized returns.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    factors = ['beta_rates_252', 'beta_credit_252', 'beta_oil_252',
               'beta_usd_252', 'beta_vix_252']
    s_list = []
    for f in factors:
        if f not in panel.columns:
            continue
        cur = panel[f]
        lag = panel[f].groupby(level='ticker').shift(12)
        d_f = (cur - lag).abs()
        d_f = d_f.where(cur.notna() & lag.notna())
        med = d_f.groupby(level='date').transform('median')
        s_f = d_f / med.where(med > 0)
        s_list.append(s_f)
    if not s_list:
        return pd.Series(np.nan, index=panel.index)
    s_df = pd.concat(s_list, axis=1)
    count = s_df.notna().sum(axis=1)
    churn = s_df.mean(axis=1, skipna=True)
    churn = churn.where(count >= 3)
    return churn.reindex(panel.index)
macro_beta_instabilitylow_riskMeasures instability of a firm’s macro-exposure vector (rates, oil, usd, credit, vix, market beta) over the…

Measures instability of a firm’s macro-exposure vector (rates, oil, usd, credit, vix, market beta) over the trailing 12 months, using the mean cross-sectional percentile rank of the absolute 12-month change across available beta fields (requires >=4 available). High instability signals an unstable, hard-to-hedge risk profile associated with lower future returns.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    fields = [
        'beta_rates_252',
        'beta_oil_252',
        'beta_usd_252',
        'beta_credit_252',
        'beta_vix_252',
        'beta_252',
    ]
    rank_frames = []
    for f in fields:
        if f not in panel.columns:
            continue
        current = panel[f]
        lagged = current.groupby(level='ticker').shift(12)
        diff = (current - lagged).abs()
        valid = current.notna() & lagged.notna()
        diff = diff.where(valid)
        rank = diff.groupby(level='date').rank(pct=True)
        rank_frames.append(rank)
    if not rank_frames:
        return pd.Series(np.nan, index=panel.index)
    rank_df = pd.concat(rank_frames, axis=1)
    count_valid = rank_df.notna().sum(axis=1)
    mean_rank = rank_df.mean(axis=1)
    score = mean_rank.where(count_valid >= 4)
    score.name = self.name
    return score
macro_beta_instability_ambiguitylow_riskInstability of a stock’s cross-asset macro betas (oil, usd, rates, credit, vix) over the trailing 12 months,…

Instability of a stock’s cross-asset macro betas (oil, usd, rates, credit, vix) over the trailing 12 months, standardized by cross-sectional dispersion each date. Stocks with unstable macro exposures have opaque/changing risk profiles and tend to underperform, so a higher instability score predicts LOWER returns (expected_sign=-1).

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    eps = 1e-8
    factors = [
        'beta_oil_252',
        'beta_usd_252',
        'beta_rates_252',
        'beta_credit_252',
        'beta_vix_252',
    ]
    deltas = []
    for f in factors:
        if f not in panel.columns:
            continue
        b = panel[f]
        b_lag = b.groupby(level='ticker').shift(12)
        valid = b.notna() & b_lag.notna()
        abs_chg = (b - b_lag).abs()
        cs_std = abs_chg.where(valid).groupby(level='date').transform('std')
        d = abs_chg / (cs_std + eps)
        d = d.where(valid)
        deltas.append(d)
    if not deltas:
        return pd.Series(np.nan, index=panel.index)
    deltas_df = pd.concat(deltas, axis=1)
    valid_count = deltas_df.notna().sum(axis=1)
    instability = deltas_df.mean(axis=1, skipna=True)
    instability = instability.where(valid_count >= 3)
    return instability.reindex(panel.index)
macro_beta_rotation_instabilitylow_riskMeasures instability (rotation) of a firm’s vector of macro betas (credit, oil, rates, usd, vix) over a…

Measures instability (rotation) of a firm’s vector of macro betas (credit, oil, rates, usd, vix) over a trailing 12-month window. Firms whose macro exposure vector rotates sharply are opaque and subject to parameter/model uncertainty, generating mispricing that on average resolves adversely, leading to lower subsequent returns.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta_cols = [
        'beta_credit_252',
        'beta_oil_252',
        'beta_rates_252',
        'beta_usd_252',
        'beta_vix_252',
    ]
    def winsorized_zscore(s: pd.Series) -> pd.Series:
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        z = (s - mean) / std.replace(0, np.nan)
        z = z.clip(-4, 4)
        return z
    z_diffs = []
    missing_flags = []
    for col in beta_cols:
        if col not in panel.columns:
            # treat as fully missing component
            nan_series = pd.Series(np.nan, index=panel.index)
            z_diffs.append(nan_series)
            missing_flags.append(pd.Series(True, index=panel.index))
            continue
        raw = panel[col]
        valid_mask = raw.notna()
        z = pd.Series(np.nan, index=panel.index)
        z.loc[valid_mask] = winsorized_zscore(raw.loc[valid_mask])
        z_lag = z.groupby(level='ticker').shift(12)
        missing = z.isna() | z_lag.isna()
        diff_sq = (z - z_lag) ** 2
        z_diffs.append(diff_sq)
        missing_flags.append(missing)
    missing_count = pd.concat(missing_flags, axis=1).sum(axis=1)
    diff_matrix = pd.concat(z_diffs, axis=1)
    # sum of squared diffs, treating missing contributions as 0 (only allowed if <=1 missing)
    diff_matrix_filled = diff_matrix.fillna(0.0)
    sum_sq = diff_matrix_filled.sum(axis=1)
    churn = np.sqrt(sum_sq)
    churn[missing_count > 1] = np.nan
    churn.name = self.name
    return churn
macro_exposure_driftlow_riskMeasures the drift in a stock’s macro exposure profile (rates, oil, USD, credit, VIX betas) over the trailing…

Measures the drift in a stock’s macro exposure profile (rates, oil, USD, credit, VIX betas) over the trailing 12 months via cross-sectional z-score changes. Stocks with unstable macro exposures are harder to model and attract speculative demand, leading to subsequent underperformance.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    betas = [
        'beta_rates_252',
        'beta_oil_252',
        'beta_usd_252',
        'beta_credit_252',
        'beta_vix_252',
    ]
    drift_terms = []
    for b in betas:
        col = panel[b]
        # cross-sectional zscore within each date
        grp = col.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        z = (col - mean) / std
        z = z.where(std > 0, np.nan)
        z = z.clip(lower=-4, upper=4)
        z_lag = z.groupby(level='ticker').shift(12)
        d = (z - z_lag).abs()
        drift_terms.append(d)
    drift_df = pd.concat(drift_terms, axis=1)
    drift_df.columns = betas
    nonnull_count = drift_df.notna().sum(axis=1)
    avg_drift = drift_df.mean(axis=1, skipna=True)
    signal = avg_drift.where(nonnull_count >= 3, np.nan)
    signal.name = self.name
    return signal
macro_exposure_instabilitylow_riskMeasures instability of a stock’s macro factor betas (oil, rates, usd, credit, vix) over a 12-month horizon

Measures instability of a stock’s macro factor betas (oil, rates, usd, credit, vix) over a 12-month horizon. Stocks with unstable macro exposures are hard to hedge/value and earn lower subsequent returns; stable macro-exposure stocks earn higher returns (expected_sign=-1).

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    factors = [
        'beta_oil_252',
        'beta_rates_252',
        'beta_usd_252',
        'beta_credit_252',
        'beta_vix_252',
    ]
    s_list = []
    for f in factors:
        if f not in panel.columns:
            continue
        cur = panel[f]
        lag = panel[f].groupby(level='ticker').shift(12)
        d_f = (cur - lag).abs()
        d_f = d_f.where(cur.notna() & lag.notna())
        # cross-sectional median of |d_f| per date
        med = d_f.groupby(level='date').transform('median')
        mad = (d_f - med).abs()
        mad_med = mad.groupby(level='date').transform('median')
        s_f = d_f / mad_med.where(mad_med > 0)
        s_f = s_f.where(mad_med > 0)
        s_list.append(s_f)
    if len(s_list) == 0:
        return pd.Series(np.nan, index=panel.index)
    s_df = pd.concat(s_list, axis=1)
    count_avail = s_df.notna().sum(axis=1)
    instab = s_df.mean(axis=1, skipna=True)
    instab = instab.where(count_avail >= 3)
    # orthogonalization term: date-level cross-sectional rank of vol_252
    if 'vol_252' in panel.columns:
        vol = panel['vol_252']
        vol_rank = vol.groupby(level='date').rank(pct=True)
        vol_rank = vol_rank.fillna(0.0)
    else:
        vol_rank = pd.Series(0.0, index=panel.index)
    score = instab - 0.5 * vol_rank
    score = score.where(instab.notna())
    return score.reindex(panel.index)
macro_loading_instabilitylow_riskMeasures the time variation of a stock’s cross-asset macro loading vector (corr to credit, oil, rates, usd,…

Measures the time variation of a stock’s cross-asset macro loading vector (corr to credit, oil, rates, usd, vix) over the trailing year. Firms with unstable macro exposures have opaque risk profiles and tend to underperform relative to firms with stable macro loadings.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    corr_cols = [
        'corr_credit_252',
        'corr_oil_252',
        'corr_rates_252',
        'corr_usd_252',
        'corr_vix_252',
    ]
    grp = panel.groupby(level='ticker')
    d_abs_list = []
    for c in corr_cols:
        if c not in panel.columns:
            continue
        lagged = grp[c].shift(12)
        d_abs = (panel[c] - lagged).abs()
        d_abs_list.append(d_abs)
    d_df = pd.concat(d_abs_list, axis=1)
    d_df.columns = range(len(d_abs_list))
    valid_count = d_df.notna().sum(axis=1)
    instability = d_df.mean(axis=1, skipna=True)
    score = instability.where(
        (valid_count >= 3) & panel['vol_252'].notna()
    )
    if 'sector' in panel.columns:
        date_level = panel.index.get_level_values('date')
        sector_vals = panel['sector']
        group_keys = [date_level, sector_vals]
        sector_median = score.groupby(group_keys).transform('median')
        score = score - sector_median
    score = score.reindex(panel.index)
    score.name = self.name
    return score
macro_systematic_variance_sharelow_riskSystematic variance share: sum of squared trailing 252d correlations of a stock’s returns to five macro…

Systematic variance share: sum of squared trailing 252d correlations of a stock’s returns to five macro factors (credit, rates, oil, usd, vix), clipped to [0,1]. Low share indicates idiosyncratic, opaque, lottery-like names that historically underperform (Ang et al., Durnev/Morck).

Category low_riskDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    cols = [
        'corr_credit_252',
        'corr_rates_252',
        'corr_oil_252',
        'corr_usd_252',
        'corr_vix_252',
    ]
    valid = pd.Series(True, index=panel.index)
    for c in cols:
        valid &= panel[c].notna()
    nobs_ok = panel['nobs'] >= 200
    share = pd.Series(0.0, index=panel.index)
    for c in cols:
        share = share + panel[c].fillna(0) ** 2
    share = share.clip(lower=0, upper=1)
    score = share.where(valid & nobs_ok)
    return score
rate_duration_mispricinglow_riskShorts hidden long-duration equities (high PB, low yield, high realized rate beta) during rising-yield…

Shorts hidden long-duration equities (high PB, low yield, high realized rate beta) during rising-yield regimes and reverses during falling-yield regimes, by comparing realized rate beta to fundamental duration implied by valuation/dividend structure.

Category low_riskDirection Long the high scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    pb = panel['pb']
    divyield = panel['divyield']
    beta_rates = panel['beta_rates_252']
    macro_chg = panel['macro_dgs10_chg12m']
    pb_valid = pb.where(pb > 0)
    log_pb = np.log(pb_valid)
    def cs_zscore(s: pd.Series) -> pd.Series:
        grouped = s.groupby(level='date')
        mean = grouped.transform('mean')
        std = grouped.transform('std')
        z = (s - mean) / std
        return z.where(std > 0)
    log_pb_z = cs_zscore(log_pb)
    # only include divyield in z-score for rows with valid pb
    divyield_masked = divyield.where(pb_valid.notna())
    divyield_z = cs_zscore(divyield_masked)
    dur_z = log_pb_z - divyield_z
    implied_beta = -dur_z
    beta_rates_masked = beta_rates.where(pb_valid.notna())
    beta_rates_z = cs_zscore(beta_rates_masked)
    mismatch = beta_rates_z - implied_beta
    rate_trend = np.tanh(macro_chg)
    signal = -mismatch * rate_trend
    # apply missing-data policy
    valid_mask = pb_valid.notna() & divyield.notna() & beta_rates.notna() & macro_chg.notna()
    signal = signal.where(valid_mask)
    return signal.reindex(panel.index)
residual_vix_hedge_premiumlow_riskResidual VIX beta after removing market-beta component via per-date cross-sectional OLS of beta_vix_252 on…

Residual VIX beta after removing market-beta component via per-date cross-sectional OLS of beta_vix_252 on [1, beta_252]. High residual VIX beta stocks act as crash hedges and are bid up, earning lower subsequent returns.

Category low_riskDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    cols = ['beta_vix_252', 'beta_252']
    df = panel[cols].copy()
    result = df.groupby(level='date', group_keys=False).apply(self._fit_resid)
    result = result.reindex(panel.index)
    result.name = self.name
    return result
unpriced_leverage_credit_insensitivitylow_riskMismatch between balance-sheet leverage and realized equity credit-beta: highly levered firms whose equity…

Mismatch between balance-sheet leverage and realized equity credit-beta: highly levered firms whose equity shows low sensitivity to credit spread moves are ‘credit-complacent’ and prone to sharp repricing when credit conditions tighten. Score = z(net leverage) - z(-beta_credit_252).

Category low_riskDirection Long the low scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    debt = panel['debt']
    cashneq = panel['cashneq']
    assets = panel['assets']
    beta_credit = panel['beta_credit_252']
    corr_credit = panel['corr_credit_252']
    valid = (
        assets.notna() & (assets > 0)
        & debt.notna() & cashneq.notna()
        & beta_credit.notna() & corr_credit.notna()
    )
    netlev = (debt - cashneq) / assets
    netlev = netlev.clip(-1, 2)
    credit_exposure = -beta_credit
    netlev_v = netlev.where(valid)
    credit_exposure_v = credit_exposure.where(valid)
    def zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.replace(0, np.nan)
    zl = zscore(netlev_v)
    zc = zscore(credit_exposure_v)
    score = zl - zc
    score = score.where(valid)
    return score.reindex(panel.index)
vix_beta_driftlow_riskSignal based on the 12-month drift in trailing VIX beta, scaled by realized volatility

Signal based on the 12-month drift in trailing VIX beta, scaled by realized volatility. A deteriorating (more negative) VIX beta signals quietly accumulating crash exposure not yet priced in, predicting underperformance; an improving VIX beta signals a cheap hedge, predicting outperformance. Restricted to stocks with meaningful trailing correlation to VIX (|corr_vix_252| >= 0.02).

Category low_riskDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta_vix = panel['beta_vix_252']
    corr_vix = panel['corr_vix_252']
    vol_252 = panel['vol_252']
    beta_vix_lag12 = beta_vix.groupby(level='ticker').shift(12)
    dbeta = beta_vix - beta_vix_lag12
    scale = vol_252.clip(lower=0.05, upper=2.0)
    signal = dbeta / scale
    valid = (
        beta_vix.notna()
        & beta_vix_lag12.notna()
        & corr_vix.notna()
        & vol_252.notna()
        & (vol_252 > 0)
        & (corr_vix.abs() >= 0.02)
    )
    signal = signal.where(valid, np.nan)
    return signal.reindex(panel.index)
annual_return_seasonality_purgedmomentumHeston-Sadka style calendar-month seasonality: average of same-calendar-month returns at 1-5 year annual…

Heston-Sadka style calendar-month seasonality: average of same-calendar-month returns at 1-5 year annual lags, purged by subtracting the average of adjacent (non-annual) lags to remove generic momentum/reversal, isolating pure seasonal return predictability.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    annual_lags = [12, 24, 36, 48, 60]
    adjacent_lags = [11, 13, 23, 25, 35, 37]
    annual_shifts = [grp.shift(k) for k in annual_lags]
    adjacent_shifts = [grp.shift(k) for k in adjacent_lags]
    annual_df = pd.concat(annual_shifts, axis=1)
    adjacent_df = pd.concat(adjacent_shifts, axis=1)
    annual_count = annual_df.notna().sum(axis=1)
    seas_mean = annual_df.mean(axis=1, skipna=True)
    nonseas_mean = adjacent_df.mean(axis=1, skipna=True)
    signal = seas_mean - nonseas_mean
    signal = signal.where(annual_count >= 3)
    signal.name = self.name
    return signal
cross_asset_news_diffusion_momentummomentumCombines trailing macro-factor loadings (beta_usd_252, beta_oil_252, beta_rates_252 gated by correlation…

Combines trailing macro-factor loadings (beta_usd_252, beta_oil_252, beta_rates_252 gated by correlation strength) with recent standardized macro shocks (USD, oil, 10y rate) to capture slow diffusion of cross-asset news into stock prices.

Category momentumDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    df = panel
    def date_level_shock(chg_series):
        # date-level series (same value across tickers per date)
        date_level = chg_series.groupby(level='date').first()
        roll_std = date_level.rolling(36, min_periods=18).std()
        sh_date = date_level / roll_std.replace(0, np.nan)
        # map back to (date,ticker)
        dates = chg_series.index.get_level_values('date')
        return sh_date.reindex(dates).to_numpy()
    # USD shock: pct change over 3 months
    usd_shift3 = df.groupby(level='ticker')['macro_usd'].shift(3)
    chg_usd = df['macro_usd'] / usd_shift3.replace(0, np.nan) - 1
    sh_usd = pd.Series(date_level_shock(chg_usd), index=df.index)
    # Oil shock: pct change over 3 months
    oil_shift3 = df.groupby(level='ticker')['macro_oil'].shift(3)
    chg_oil = df['macro_oil'] / oil_shift3.replace(0, np.nan) - 1
    sh_oil = pd.Series(date_level_shock(chg_oil), index=df.index)
    # Rates shock: level difference over 3 months
    rates_shift3 = df.groupby(level='ticker')['macro_dgs10'].shift(3)
    chg_rates = df['macro_dgs10'] - rates_shift3
    sh_rates = pd.Series(date_level_shock(chg_rates), index=df.index)
    def loading(beta_col, corr_col):
        beta = df[beta_col]
        corr = df[corr_col]
        valid = corr.abs() > 0.15
        load = beta.where(valid, 0.0)
        load = load.where(beta.notna() & corr.notna(), np.nan)
        return load
    load_usd = loading('beta_usd_252', 'corr_usd_252')
    load_oil = loading('beta_oil_252', 'corr_oil_252')
    load_rates = loading('beta_rates_252', 'corr_rates_252')
    term_usd = load_usd * sh_usd
    term_oil = load_oil * sh_oil
    term_rates = load_rates * sh_rates
    terms = pd.concat([term_usd, term_oil, term_rates], axis=1)
    terms.columns = ['usd', 'oil', 'rates']
    # a term contributes 0 if its inputs are missing/invalid, but overall
    # signal is NaN if ALL three loadings are missing
    loadings_all_nan = load_usd.isna() & load_oil.isna() & load_rates.isna()
    signal = terms.fillna(0.0).sum(axis=1)
    signal = signal.where(~loadings_all_nan, np.nan)
    return signal.reindex(panel.index)
frog_in_the_pan_gradual_momentummomentumInteracts 12-1 momentum with ‘information discreteness’ of the path: momentum accumulated via many small…

Interacts 12-1 momentum with ‘information discreteness’ of the path: momentum accumulated via many small same-sign monthly moves (gradual) is up-weighted, while momentum driven by a few large jumps is down-weighted, per Da, Gurun and Warachka’s frog-in-the-pan hypothesis.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret_1m = panel["ret_1m"]
    ret_12_1 = panel["ret_12_1"]
    vol_252 = panel["vol_252"]
    grp = ret_1m.groupby(level="ticker")
    pos_sum = pd.Series(0.0, index=panel.index)
    count = pd.Series(0.0, index=panel.index)
    for k in range(1, 13):
        r_k = grp.shift(k)
        valid = r_k.notna()
        pos_sum = pos_sum + (r_k > 0).astype(float).where(valid, 0.0)
        count = count + valid.astype(float)
    pos = pos_sum / count
    neg = 1.0 - pos
    enough = count >= 9
    sign_ret = np.sign(ret_12_1)
    ID = sign_ret * (neg - pos)
    cont = -ID
    denom_ok = vol_252 > 0
    base = ret_12_1 / vol_252.where(denom_ok, np.nan)
    # cross-sectional z-score of base, per date
    def zscore(s):
        mu = s.mean()
        sd = s.std()
        if sd is None or sd == 0 or pd.isna(sd):
            return pd.Series(np.nan, index=s.index)
        return (s - mu) / sd
    z = base.groupby(level="date").transform(zscore)
    score = z * (0.5 + cont)
    valid_mask = enough & ret_12_1.notna() & denom_ok & base.notna() & z.notna()
    score = score.where(valid_mask, np.nan)
    return score
fundamental_momentum_price_gapmomentumGap between fundamental momentum (YoY change in gross profit and operating cash flow scaled by assets) and…

Gap between fundamental momentum (YoY change in gross profit and operating cash flow scaled by assets) and price momentum (6m and 12-1m returns). Positive gap indicates improved fundamentals without commensurate price appreciation, predicting underreaction drift; near-neutral to raw price momentum.

Category momentumDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    gp = panel['gp']
    ncfo = panel['ncfo']
    assets = panel['assets']
    ret_6m = panel['ret_6m']
    ret_12_1 = panel['ret_12_1']
    gp_lag12 = gp.groupby(level='ticker').shift(12)
    ncfo_lag12 = ncfo.groupby(level='ticker').shift(12)
    valid_assets = assets > 0
    dgp = (gp - gp_lag12) / assets.where(valid_assets)
    dcfo = (ncfo - ncfo_lag12) / assets.where(valid_assets)
    dgp = dgp.where(valid_assets & gp_lag12.notna())
    dcfo = dcfo.where(valid_assets & ncfo_lag12.notna())
    def cs_rank(s):
        return s.groupby(level='date').rank(pct=True)
    rank_dgp = cs_rank(dgp)
    rank_dcfo = cs_rank(dcfo)
    rank_ret6 = cs_rank(ret_6m)
    rank_ret12_1 = cs_rank(ret_12_1)
    fund_mom = pd.concat([rank_dgp, rank_dcfo], axis=1).mean(axis=1, skipna=True)
    price_mom = pd.concat([rank_ret6, rank_ret12_1], axis=1).mean(axis=1, skipna=True)
    signal = fund_mom - price_mom
    invalid = dgp.isna() & dcfo.isna()
    signal = signal.where(~invalid)
    missing_price = ret_6m.isna() & ret_12_1.isna()
    signal = signal.where(~missing_price)
    return signal
gradual_information_momentummomentumFrog-in-the-pan style momentum: conditions 12-1 momentum on the gradualness of the underlying monthly return…

Frog-in-the-pan style momentum: conditions 12-1 momentum on the gradualness of the underlying monthly return path (Da-Gurun-Warachka information discreteness proxy). Momentum is weighted up for stocks with many small same-signed monthly moves (gradual info flow) and down for stocks whose 12-month return was driven by a few discrete jumps.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret_1m = panel["ret_1m"]
    ret_12_1 = panel["ret_12_1"]
    grouped = ret_1m.groupby(level="ticker")
    pos_count = None
    nonmiss_count = None
    for k in range(1, 13):
        lagged = grouped.shift(k)
        is_pos = (lagged > 0).astype(float)
        is_nonmiss = lagged.notna().astype(float)
        is_pos = is_pos.where(lagged.notna(), 0.0)
        if pos_count is None:
            pos_count = is_pos
            nonmiss_count = is_nonmiss
        else:
            pos_count = pos_count + is_pos
            nonmiss_count = nonmiss_count + is_nonmiss
    pos_frac = pos_count / nonmiss_count.replace(0, np.nan)
    pos_frac = pos_frac.where(nonmiss_count >= 9)
    sign_mom = np.sign(ret_12_1)
    ID = sign_mom * (1 - 2 * pos_frac)
    def cross_sectional_pct_rank(s: pd.Series) -> pd.Series:
        return s.groupby(level="date").rank(pct=True, method="average")
    def cross_sectional_zscore(s: pd.Series) -> pd.Series:
        mean = s.groupby(level="date").transform("mean")
        std = s.groupby(level="date").transform("std")
        z = (s - mean) / std.replace(0, np.nan)
        return z
    id_pct_rank = cross_sectional_pct_rank(ID)
    g = 1 - id_pct_rank
    mom_z = cross_sectional_zscore(ret_12_1)
    signal = mom_z * g
    signal = signal.where(ret_12_1.notna())
    signal = signal.where(pos_frac.notna())
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal
momentum_12_1momentumJegadeesh-Titman 12-1 month price momentum (skip most recent month)

Jegadeesh-Titman 12-1 month price momentum (skip most recent month).

Category momentumDirection Long the high scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    return panel["ret_12_1"]
pure_calendar_month_seasonality_spreadmomentumSeasonal spread: average of a stock’s ret_1m in the same calendar month over prior years (lags 11,23,35,47,59…

Seasonal spread: average of a stock’s ret_1m in the same calendar month over prior years (lags 11,23,35,47,59 months) minus the average of all trailing 60 monthly returns, isolating pure calendar-month seasonality net of the stock’s unconditional mean return level.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [11, 23, 35, 47, 59]
    same_shifts = [grp.shift(k) for k in same_lags]
    same_df = pd.concat(same_shifts, axis=1)
    same_mean = same_df.mean(axis=1, skipna=True)
    same_count = same_df.notna().sum(axis=1)
    all_lags = list(range(0, 60))
    all_shifts = [grp.shift(k) for k in all_lags]
    all_df = pd.concat(all_shifts, axis=1)
    all_mean = all_df.mean(axis=1, skipna=True)
    all_count = all_df.notna().sum(axis=1)
    score = same_mean - all_mean
    valid = (same_count >= 3) & (all_count >= 36)
    score = score.where(valid, np.nan)
    return score.reindex(panel.index)
quiet_strength_low_attention_highmomentumInteraction of proximity-to-52-week-high with low turnover (low dollar volume relative to market cap) and…

Interaction of proximity-to-52-week-high with low turnover (low dollar volume relative to market cap) and 12-1 momentum, capturing ‘quiet strength’: slow diffusion of good news in low-attention names near their highs, which should earn positive future drift versus high-turnover attention-saturated names near their highs.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    high_ratio = panel['high_ratio']
    dvol_21 = panel['dvol_21']
    marketcap = panel['marketcap']
    ret_12_1 = panel['ret_12_1']
    valid = (
        high_ratio.notna()
        & dvol_21.notna()
        & marketcap.notna()
        & ret_12_1.notna()
        & (marketcap > 0)
        & (dvol_21 > 0)
    )
    turn = dvol_21 / marketcap.where(marketcap > 0)
    log_turn = np.log(turn.where(turn > 0))
    ret_clipped = ret_12_1.clip(-1, 3)
    def zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.replace(0, np.nan)
    zh = zscore(high_ratio.where(valid))
    zt = zscore(log_turn.where(valid))
    zm = zscore(ret_clipped.where(valid))
    score = 0.5 * zh + 0.5 * zm - zt
    score = score.where(valid)
    return score.reindex(panel.index)
same_calendar_month_return_seasonalitymomentumSame-calendar-month return seasonality (Heston-Sadka): average of cross-sectionally winsorized same-month…

Same-calendar-month return seasonality (Heston-Sadka): average of cross-sectionally winsorized same-month lagged returns at 12,24,36,48,60 months back, requiring at least 3 non-null observations.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    lags = [12, 24, 36, 48, 60]
    ret = panel['ret_1m']
    grouped = ret.groupby(level='ticker')
    winsorized_lags = []
    for k in lags:
        lagged = grouped.shift(k)
        # cross-sectional winsorization at 1st/99th percentile, per date
        def _winsorize(s):
            lo = s.quantile(0.01)
            hi = s.quantile(0.99)
            return s.clip(lower=lo, upper=hi)
        wins = lagged.groupby(level='date').transform(_winsorize)
        winsorized_lags.append(wins)
    stacked = pd.concat(winsorized_lags, axis=1)
    stacked.columns = [f"lag_{k}" for k in lags]
    count_valid = stacked.notna().sum(axis=1)
    mean_val = stacked.mean(axis=1, skipna=True)
    signal = mean_val.where(count_valid >= 3, np.nan)
    signal.name = self.name
    return signal
same_month_annual_seasonalitymomentumFirm-level same-calendar-month annual seasonality: average of a stock’s ret_1m from the same calendar month…

Firm-level same-calendar-month annual seasonality: average of a stock’s ret_1m from the same calendar month over the prior 1-5 years predicts current-month return (Heston & Sadka seasonality effect).

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    shifts = []
    for k in range(1, 6):
        s_k = ret.groupby(level='ticker').shift(12 * k)
        shifts.append(s_k)
    stacked = pd.concat(shifts, axis=1)
    count_valid = stacked.notna().sum(axis=1)
    mean_val = stacked.mean(axis=1, skipna=True)
    signal = mean_val.where(count_valid >= 3, np.nan)
    signal = signal.reindex(panel.index)
    return signal
same_month_minus_offseason_seasonalitymomentumFirm-level calendar-month seasonality spread: mean of same-calendar-month historical 1m returns (at lags…

Firm-level calendar-month seasonality spread: mean of same-calendar-month historical 1m returns (at lags 12,24,36,48,60 months) minus mean of off-season average monthly return over the same lags. Isolates pure seasonal mispricing (Heston-Sadka / Keloharju-Linnainmaa-Nyberg).

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret_1m = panel['ret_1m']
    ret_12m = panel['ret_12m']
    lags = [12, 24, 36, 48, 60]
    same_list = []
    other_list = []
    for k in lags:
        same_k = ret_1m.groupby(level='ticker').shift(k)
        r12_k = ret_12m.groupby(level='ticker').shift(k)
        other_k = (r12_k - same_k) / 11.0
        valid = same_k.notna() & r12_k.notna()
        same_k = same_k.where(valid)
        other_k = other_k.where(valid)
        same_list.append(same_k)
        other_list.append(other_k)
    same_df = pd.concat(same_list, axis=1)
    other_df = pd.concat(other_list, axis=1)
    count_available = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    other_mean = other_df.mean(axis=1)
    signal = same_mean - other_mean
    signal = signal.where(count_available >= 2)
    return signal.reindex(panel.index)
same_month_seasonal_return_persistencemomentumAverages a stock’s ret_1m over the same calendar month in each of the past 5 years (lags of 12/24/36/48/60…

Averages a stock’s ret_1m over the same calendar month in each of the past 5 years (lags of 12/24/36/48/60 months) to capture persistent calendar-month seasonality in cross-sectional returns, requiring at least 3 of 5 lagged observations to be non-NaN.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    lags = [g.shift(k) for k in (12, 24, 36, 48, 60)]
    lag_df = pd.concat(lags, axis=1)
    valid_count = lag_df.notna().sum(axis=1)
    seas_mean = lag_df.mean(axis=1, skipna=True)
    signal = seas_mean.where(valid_count >= 3)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal
same_month_seasonality_ex_adjacentmomentumStock-level calendar-month seasonality signal: average same-calendar-month return over the past 5 years (lags…

Stock-level calendar-month seasonality signal: average same-calendar-month return over the past 5 years (lags 12,24,36,48,60 months) minus the average return in adjacent non-seasonal months (lags 11,13,23,25,35,37), isolating pure seasonal return persistence from slow-moving level effects.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel["ret_1m"].groupby(level="ticker")
    seas_lags = [12, 24, 36, 48, 60]
    nonseas_lags = [11, 13, 23, 25, 35, 37]
    seas_shifts = [g.shift(k) for k in seas_lags]
    nonseas_shifts = [g.shift(k) for k in nonseas_lags]
    seas_df = pd.concat(seas_shifts, axis=1)
    nonseas_df = pd.concat(nonseas_shifts, axis=1)
    seas_count = seas_df.notna().sum(axis=1)
    nonseas_count = nonseas_df.notna().sum(axis=1)
    seas_mean = seas_df.mean(axis=1, skipna=True)
    nonseas_mean = nonseas_df.mean(axis=1, skipna=True)
    signal = seas_mean - nonseas_mean
    valid = (seas_count >= 3) & (nonseas_count >= 1)
    signal = signal.where(valid, np.nan)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal
same_month_seasonality_excessmomentumAverage same-calendar-month return over the past 5 years minus the average return of adjacent (off-annual)…

Average same-calendar-month return over the past 5 years minus the average return of adjacent (off-annual) month lags, isolating a repeating seasonal component in stock returns net of slow momentum and drift.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    annual_lags = [12, 24, 36, 48, 60]
    adjacent_lags = [11, 13, 23, 25, 35, 37, 47, 49, 59, 61]
    annual_shifts = [g.shift(k) for k in annual_lags]
    adjacent_shifts = [g.shift(k) for k in adjacent_lags]
    annual_df = pd.concat(annual_shifts, axis=1)
    annual_count = annual_df.notna().sum(axis=1)
    annual_mean = annual_df.mean(axis=1, skipna=True)
    annual_mean = annual_mean.where(annual_count >= 3, np.nan)
    adjacent_df = pd.concat(adjacent_shifts, axis=1)
    adjacent_mean = adjacent_df.mean(axis=1, skipna=True)
    adjacent_mean = adjacent_mean.fillna(0.0)
    signal = annual_mean - adjacent_mean
    signal = signal.where(annual_mean.notna(), np.nan)
    return signal.reindex(panel.index)
seasonal_same_month_residual_returnmomentumSame-calendar-month seasonal return persistence: average of ret_1m at lags 11,23,35,47,59 months minus the…

Same-calendar-month seasonal return persistence: average of ret_1m at lags 11,23,35,47,59 months minus the trailing 60-month base mean of 1-month-lagged returns, isolating a purely seasonal component.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel["ret_1m"]
    grp = ret.groupby(level="ticker")
    same_month_lags = [11, 23, 35, 47, 59]
    shifted = [grp.shift(k) for k in same_month_lags]
    same_df = pd.concat(shifted, axis=1)
    same_month_count = same_df.notna().sum(axis=1)
    same_month = same_df.mean(axis=1)
    same_month = same_month.where(same_month_count >= 3, np.nan)
    base_lag1 = grp.shift(1)
    base_grp = base_lag1.groupby(level="ticker")
    base_mean = base_grp.rolling(window=60, min_periods=36).mean()
    base_mean.index = base_mean.index.droplevel(0)
    base_mean = base_mean.reindex(ret.index)
    signal = same_month - base_mean
    signal = signal.where(same_month.notna() & base_mean.notna(), np.nan)
    return signal.reindex(panel.index)
seasonal_same_month_return_persistencemomentumSame-calendar-month seasonality in stock returns: uses average return in the same calendar month over the…

Same-calendar-month seasonality in stock returns: uses average return in the same calendar month over the past ~5 years (lags 11,23,35,47,59 months) relative to the average return over the other recent months (lags 1-10) to capture firm-specific seasonal return persistence.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    g = ret.groupby(level='ticker')
    seasonal_lags = [11, 23, 35, 47, 59]
    seasonal_shifts = [g.shift(k) for k in seasonal_lags]
    seasonal_df = pd.concat(seasonal_shifts, axis=1)
    seasonal_count = seasonal_df.notna().sum(axis=1)
    seas = seasonal_df.mean(axis=1)
    seas = seas.where(seasonal_count >= 3)
    other_lags = list(range(1, 11))
    other_shifts = [g.shift(k) for k in other_lags]
    other_df = pd.concat(other_shifts, axis=1)
    other_count = other_df.notna().sum(axis=1)
    other = other_df.mean(axis=1)
    other = other.where(other_count >= 1)
    score = seas - other
    score = score.where(seasonal_count >= 3)
    score.index = panel.index
    return score
tax_loss_overhang_turn_of_yearmomentumTax-loss overhang proxy: distance from 52-week high scaled by volatility identifies stocks likely subject to…

Tax-loss overhang proxy: distance from 52-week high scaled by volatility identifies stocks likely subject to tax-motivated selling pressure in Nov/Dec, with mechanical reversal in January.

Category momentumDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    req_cols = ['ret_12_1', 'marketcap', 'vol_252']
    df = panel[req_cols].copy()
    valid = df.notna().all(axis=1)
    log_mcap = np.log(df['marketcap'].where(df['marketcap'] > 0))
    valid = valid & log_mcap.notna()
    def zscore_by_date(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.where(std > 0)
    neg_ret = (-df['ret_12_1']).where(valid)
    neg_logmcap = (-log_mcap).where(valid)
    vol = df['vol_252'].where(valid)
    z_ret = zscore_by_date(neg_ret)
    z_logmcap = zscore_by_date(neg_logmcap)
    z_vol = zscore_by_date(vol)
    loss = z_ret + 0.5 * z_logmcap + 0.5 * z_vol
    dates = panel.index.get_level_values('date')
    months = pd.Series(dates, index=panel.index).dt.month
    signal = pd.Series(np.nan, index=panel.index, dtype=float)
    signal = signal.where(~(months == 12), loss)
    signal = signal.where(~(months.isin([10, 11])), -loss)
    return signal
annual_return_seasonalityotherSame-calendar-month return seasonality: average of a stock’s ret_1m at annual lags (12,24,36,48,60 months)…

Same-calendar-month return seasonality: average of a stock’s ret_1m at annual lags (12,24,36,48,60 months) minus the average of its ret_1m over non-seasonal lags (13..23 months). Captures the Heston-Sadka same-month seasonality effect, largely orthogonal to 12-1 momentum.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    other_lags = list(range(13, 24))
    same_shifts = [grp.shift(k) for k in same_lags]
    other_shifts = [grp.shift(k) for k in other_lags]
    same_df = pd.concat(same_shifts, axis=1)
    other_df = pd.concat(other_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    same_mean = same_mean.where(same_count >= 3, np.nan)
    other_count = other_df.notna().sum(axis=1)
    other_mean = other_df.mean(axis=1)
    other_mean = other_mean.where(other_count > 0, 0.0)
    signal = same_mean - other_mean
    signal = signal.where(same_mean.notna(), np.nan)
    return signal.reindex(panel.index)
annual_return_seasonality_spreadotherSeasonality spread: mean of same-calendar-month returns over prior years (12/24/36/48/60 month lags) minus…

Seasonality spread: mean of same-calendar-month returns over prior years (12/24/36/48/60 month lags) minus the stock’s average trailing 60-month return, isolating the pure seasonal component of monthly returns as documented by Heston and Sadka.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    lags = [12, 24, 36, 48, 60]
    shifted = [g.shift(k) for k in lags]
    same_month_df = pd.concat(shifted, axis=1)
    same_month = same_month_df.mean(axis=1, skipna=True)
    same_month_count = same_month_df.notna().sum(axis=1)
    same_month = same_month.where(same_month_count >= 3, np.nan)
    shifted1 = g.shift(1)
    all_month = (
        shifted1.groupby(level='ticker')
        .rolling(60, min_periods=24)
        .mean()
    )
    # rolling on grouped series returns a MultiIndex with an extra ticker level
    all_month = all_month.droplevel(0)
    all_month = all_month.reindex(panel.index)
    signal = same_month - all_month
    return signal.reindex(panel.index)
attention_shock_amplified_reversalotherShort-horizon reversal amplified by abnormal dollar-volume shocks: reversal (ret_1m scaled by vol_252) is…

Short-horizon reversal amplified by abnormal dollar-volume shocks: reversal (ret_1m scaled by vol_252) is combined multiplicatively with a rank-normalized measure of how far current log dollar volume deviates from its trailing 12-month average, capturing attention/liquidity-driven demand shocks that tend to reverse.

Category otherDirection Long the low scoresData price
def compute(self, panel) -> pd.Series:
    dvol_21 = panel["dvol_21"]
    ret_1m = panel["ret_1m"]
    vol_252 = panel["vol_252"]
    base = np.log(dvol_21.where(dvol_21 > 0))
    grp = base.groupby(level="ticker")
    lags = [grp.shift(k) for k in range(1, 13)]
    lag_df = pd.concat(lags, axis=1)
    nonmissing_count = lag_df.notna().sum(axis=1)
    avg = lag_df.mean(axis=1)
    vshock = base - avg
    vshock = vshock.where(nonmissing_count >= 9)
    # cross-sectional rank normalization into [-0.5, 0.5], per date
    def rank_norm(s: pd.Series) -> pd.Series:
        valid = s.dropna()
        if len(valid) == 0:
            return pd.Series(np.nan, index=s.index)
        ranks = valid.rank(method="average")
        n = len(valid)
        if n > 1:
            normed = (ranks - 1) / (n - 1) - 0.5
        else:
            normed = pd.Series(0.0, index=valid.index)
        out = pd.Series(np.nan, index=s.index)
        out.loc[valid.index] = normed
        return out
    vshock_c = vshock.groupby(level="date").apply(rank_norm)
    vshock_c.index = vshock.index
    # reversal component
    r = ret_1m.where(vol_252 > 0) / vol_252.where(vol_252 > 0)
    def winsorize(s: pd.Series) -> pd.Series:
        valid = s.dropna()
        if len(valid) == 0:
            return s
        lo = valid.quantile(0.01)
        hi = valid.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    r_wins = r.groupby(level="date").apply(winsorize)
    r_wins.index = r.index
    signal = r_wins * (0.5 + vshock_c)
    # enforce missing-data policy: require dvol_21, ret_1m, vol_252 valid
    valid_mask = dvol_21.notna() & ret_1m.notna() & vol_252.notna() & (vol_252 > 0)
    signal = signal.where(valid_mask)
    return signal.reindex(panel.index)
attention_shock_return_reversalotherInteraction of abnormal turnover (attention shock) with trailing 1m return

Interaction of abnormal turnover (attention shock) with trailing 1m return. High abnormal turnover accompanying a large recent gain forecasts reversal; high abnormal turnover accompanying a loss forecasts a bounce. Uses log turnover relative to its trailing 12m (min 6) mean, times ret_1m, with expected_sign=-1 so the harness implements the reversal orientation.

Category otherDirection Long the low scoresData price
def compute(self, panel) -> pd.Series:
    dvol = panel['dvol_21']
    mcap = panel['marketcap']
    ret_1m = panel['ret_1m']
    turn = (dvol / mcap).where((dvol > 0) & (mcap > 0))
    lt = np.log(turn)
    lt_shifted = lt.groupby(level='ticker').shift(1)
    base = (
        lt_shifted.groupby(level='ticker')
        .rolling(12, min_periods=6)
        .mean()
        .reset_index(level=0, drop=True)
    )
    abn = lt - base
    score = abn * ret_1m
    score = score.where(abn.notna() & ret_1m.notna())
    return score.reindex(panel.index)
attention_spike_return_reversalotherInteraction of abnormal 21d dollar volume (attention spike) with recent 1m return scaled by volatility:…

Interaction of abnormal 21d dollar volume (attention spike) with recent 1m return scaled by volatility: reverses momentum on high-attention runs and rewards liquidity discount on high-attention sell-offs.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    dvol = panel['dvol_21']
    ret_1m = panel['ret_1m']
    vol_252 = panel['vol_252']
    grp = dvol.groupby(level='ticker')
    lagged = [grp.shift(k) for k in range(1, 13)]
    stacked = pd.concat(lagged, axis=1)
    valid_count = stacked.notna().sum(axis=1)
    avg_dvol = stacked.mean(axis=1)
    valid_avg = (valid_count >= 6) & (avg_dvol > 0)
    avg_dvol = avg_dvol.where(valid_avg)
    valid_dvol = dvol > 0
    abn_vol = np.log(dvol / avg_dvol)
    abn_vol = abn_vol.where(valid_dvol & valid_avg)
    abn_vol_clipped = abn_vol.clip(-2, 2)
    vol_floor = vol_252.clip(lower=0.05)
    valid_vol = vol_252 > 0
    scaled_ret = ret_1m / vol_floor
    scaled_ret = scaled_ret.where(valid_vol)
    scaled_ret_clipped = scaled_ret.clip(-3, 3)
    signal = -1.0 * abn_vol_clipped * scaled_ret_clipped
    valid = (
        valid_avg
        & valid_dvol
        & valid_vol
        & ret_1m.notna()
        & dvol.notna()
    )
    signal = signal.where(valid)
    return signal.reindex(panel.index)
dollar_trend_fx_beta_interactionotherInteracts a stock’s trailing USD beta with the realized 12-month dollar trend: cross-sectionally z-scored,…

Interacts a stock’s trailing USD beta with the realized 12-month dollar trend: cross-sectionally z-scored, winsorized beta_usd_252 (excluding names with weak |corr_usd_252|) is scaled by tanh of the fractional 12m dollar trend, producing a sign-switching macro-conditioned score.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta = panel['beta_usd_252']
    corr = panel['corr_usd_252']
    macro_usd = panel['macro_usd']
    macro_usd_chg12m = panel['macro_usd_chg12m']
    # base validity: all inputs present
    valid = beta.notna() & corr.notna() & macro_usd.notna() & macro_usd_chg12m.notna()
    # winsorize beta at 1st/99th percentile per date
    def winsorize(s):
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    beta_masked = beta.where(valid)
    beta_wins = beta_masked.groupby(level='date').transform(winsorize)
    # cross-sectional zscore per date
    def zscore(s):
        mu = s.mean()
        sd = s.std()
        if sd is None or not np.isfinite(sd) or sd == 0:
            return s * np.nan
        return (s - mu) / sd
    zb = beta_wins.groupby(level='date').transform(zscore)
    # exclude weak corr names
    weak_corr = corr.abs() <= 0.05
    zb = zb.where(~weak_corr)
    # dollar trend, same for all tickers on a date
    trend = macro_usd_chg12m / macro_usd.where(macro_usd > 0)
    trend_signal = np.tanh(trend / 0.05)
    # if macro_usd or macro_usd_chg12m missing for date -> NaN for all tickers that date
    macro_valid_by_date = (macro_usd.notna() & macro_usd_chg12m.notna()).groupby(level='date').transform('all')
    signal = zb * trend_signal
    signal = signal.where(valid & macro_valid_by_date)
    return signal
expected_earnings_announcement_monthotherScores stocks by how close their expected next earnings announcement is to the upcoming month, based on the…

Scores stocks by how close their expected next earnings announcement is to the upcoming month, based on the gap between the current point-in-time date and the last SEC filing date. Firms whose gap since last filing is close to ~72 days (implying an announcement roughly one quarter after the prior filing) score highest, capturing the earnings-announcement attention premium.

Category otherDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    asof_date = pd.to_datetime(panel['asof_date'])
    fund_datekey = pd.to_datetime(panel['fund_datekey'])
    gap_days = (asof_date - fund_datekey).dt.days
    score = np.exp(-((gap_days - 72.0) ** 2) / (2 * 15.0 ** 2))
    invalid = fund_datekey.isna() | gap_days.isna() | (gap_days > 200)
    score = score.where(~invalid, np.nan)
    score = score.reindex(panel.index)
    score.name = self.name
    return score
filing_staleness_information_neglectotherAbnormal filing staleness: days elapsed since the most recent SEC filing (fund_datekey) relative to the…

Abnormal filing staleness: days elapsed since the most recent SEC filing (fund_datekey) relative to the industry-median lag on the same date. Unusually stale filings signal information friction / neglect and predict negative future drift; fresh filings predict positive drift.

Category otherDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    asof = pd.to_datetime(panel['asof_date'])
    datekey = pd.to_datetime(panel['fund_datekey'])
    lag_days = (asof - datekey).dt.days
    valid = datekey.notna() & (lag_days >= 0) & (lag_days <= self.MAX_LAG_DAYS)
    lag_valid = lag_days.where(valid)
    industry = panel['industry']
    df = pd.DataFrame({
        'lag': lag_valid,
        'industry': industry,
    }, index=panel.index)
    group_keys = [df.index.get_level_values('date'), df['industry']]
    group_median = df.groupby(group_keys)['lag'].transform('median')
    group_count = df.groupby(group_keys)['lag'].transform('count')
    abn = df['lag'] - group_median
    abn = abn.where(group_count >= self.MIN_GROUP_SIZE)
    abn = abn.where(valid)
    abn.name = self.name
    return abn
hidden_rate_duration_mismatchotherCombines fundamental duration proxies (log P/B, log P/S) with trailing realized rate beta to identify…

Combines fundamental duration proxies (log P/B, log P/S) with trailing realized rate beta to identify long-duration names whose historical beta understates true rate sensitivity; conditions the mismatch score on the 12-month change in the 10y yield to time macro-state-dependent duration risk.

Category otherDirection Long the high scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    pb = panel['pb']
    ps = panel['ps']
    beta_rates = panel['beta_rates_252']
    dgs10_chg12m = panel['macro_dgs10_chg12m']
    log_pb = np.log(pb.where(pb > 0))
    log_ps = np.log(ps.where(ps > 0))
    def cs_zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.where(std > 0)
    zpb = cs_zscore(log_pb)
    zps = cs_zscore(log_ps)
    zrb = cs_zscore(beta_rates)
    hidden_duration = zpb + zps + zrb
    signal = -hidden_duration * np.tanh(dgs10_chg12m)
    mask = zpb.notna() & zps.notna() & zrb.notna() & dgs10_chg12m.notna()
    signal = signal.where(mask)
    return signal.reindex(panel.index)
hidden_refinancing_rate_exposureotherInteraction of net short-term debt burden (fundamental rollover risk) with a stock’s realized beta to rates:…

Interaction of net short-term debt burden (fundamental rollover risk) with a stock’s realized beta to rates: firms with high net short-term debt but positive rate beta are mispriced ‘hidden’ rollover-risk exposures, whose returns should suffer when short rates rise (and benefit when they fall). Signal = z(net_st) * z(beta_rates_252) * clipped 12m change in 2y yield.

Category otherDirection Long the low scoresData fundamental, price, macro
def compute(self, panel) -> pd.Series:
    debtc = panel['debtc']
    cashneq = panel['cashneq']
    assets = panel['assets']
    beta_rates = panel['beta_rates_252']
    macro_chg = panel['macro_dgs2_chg12m']
    valid = (
        debtc.notna()
        & cashneq.notna()
        & assets.notna()
        & (assets > 0)
        & beta_rates.notna()
        & macro_chg.notna()
    )
    net_st = pd.Series(np.nan, index=panel.index)
    net_st[valid] = (debtc[valid] - cashneq[valid]) / assets[valid]
    beta_valid = beta_rates.where(valid)
    z1 = net_st.groupby(level='date').transform(_winsorized_zscore)
    z2 = beta_valid.groupby(level='date').transform(_winsorized_zscore)
    mismatch = z1 * z2
    macro_clipped = macro_chg.clip(lower=-1.5, upper=1.5)
    score = mismatch * macro_clipped
    score = score.where(valid)
    return score
long_term_reversal_sentiment_conditionedotherLong-term (t-36 to t-12) reversal signal conditioned on past-only consumer sentiment state: reversal weighted…

Long-term (t-36 to t-12) reversal signal conditioned on past-only consumer sentiment state: reversal weighted more heavily when sentiment is above its own trailing average (elevated optimism), consistent with Baker-Wurgler/Stambaugh-Yu-Yuan sentiment-conditioned mispricing correction.

Category otherDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    ret_12m = panel['ret_12m']
    r1 = ret_12m.groupby(level='ticker').shift(12)
    r2 = ret_12m.groupby(level='ticker').shift(24)
    cum_36_12 = (1 + r1) * (1 + r2) - 1
    # date-level sentiment series
    sent_by_date = panel['macro_umcsent'].groupby(level='date').mean()
    sent_by_date = sent_by_date.sort_index()
    rolling_mean = sent_by_date.rolling(36, min_periods=24).mean().shift(1)
    sent_state = sent_by_date / rolling_mean.where(rolling_mean > 0)
    w_by_date = pd.Series(
        np.where(sent_state > 1.0, 1.0, 0.3),
        index=sent_state.index,
    )
    w_by_date = w_by_date.where(sent_state.notna())
    # broadcast weight to (date, ticker) index
    dates = panel.index.get_level_values('date')
    w = w_by_date.reindex(dates)
    w.index = panel.index
    score = cum_36_12 * w
    return score.reindex(panel.index)
macro_beta_dispersion_crowding_rotationotherEntropy of squared macro correlations (oil, rates, usd, credit, vix) capturing the shape of a stock’s macro…

Entropy of squared macro correlations (oil, rates, usd, credit, vix) capturing the shape of a stock’s macro exposure profile. Diffuse macro exposure (high entropy) earns a premium for undiversifiable macro risk; concentrated exposure in a single fashionable factor is subject to crowded-flow reversals.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    cols = ['corr_oil_252', 'corr_rates_252', 'corr_usd_252',
            'corr_credit_252', 'corr_vix_252']
    df = panel[cols].copy()
    n_valid = df.notna().sum(axis=1)
    sq = df.pow(2)
    sq_filled = sq.fillna(0.0)
    sum_sq = sq_filled.sum(axis=1)
    valid_mask = (n_valid >= 4) & (sum_sq > 0)
    w = sq_filled.div(sum_sq.where(sum_sq > 0, np.nan), axis=0)
    entropy = -(w * np.log(w + 1e-12)).sum(axis=1)
    score = entropy.where(valid_mask, np.nan)
    score.name = self.name
    return score
macro_trend_attributed_return_reversalotherContrarian signal on the macro-attributed component of past returns: sums cross-sectionally z-scored macro…

Contrarian signal on the macro-attributed component of past returns: sums cross-sectionally z-scored macro betas (rates, oil, usd, credit, vix) signed by the direction of the corresponding 12-month macro trend. Stocks with high macro-attributed ‘performance’ are expected to mean-revert as macro trends are less persistent than the market extrapolates.

Category otherDirection Long the low scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta_cols = {
        'rates': 'beta_rates_252',
        'oil': 'beta_oil_252',
        'usd': 'beta_usd_252',
        'credit': 'beta_credit_252',
        'vix': 'beta_vix_252',
    }
    chg_cols = {
        'rates': 'macro_dgs10_chg12m',
        'oil': 'macro_oil_chg12m',
        'usd': 'macro_usd_chg12m',
        'credit': 'macro_baa_spread_chg12m',
        'vix': 'macro_vix_chg12m',
    }
    contributions = []
    valid_masks = []
    for f in beta_cols:
        beta_col = beta_cols[f]
        chg_col = chg_cols[f]
        if beta_col not in panel.columns or chg_col not in panel.columns:
            continue
        beta = panel[beta_col]
        chg = panel[chg_col]
        # cross-sectional zscore per date, clipped
        grp = beta.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        zb = (beta - mean) / std
        zb = zb.where(std > 0, np.nan)
        zb = zb.clip(-3, 3)
        d = np.sign(chg)
        d = d.where(chg.notna() & (chg != 0), np.nan)
        contrib = d * zb
        valid = contrib.notna()
        contributions.append(contrib.fillna(0.0))
        valid_masks.append(valid)
    if not contributions:
        return pd.Series(np.nan, index=panel.index)
    total = contributions[0]
    for c in contributions[1:]:
        total = total + c
    count = valid_masks[0].astype(int)
    for v in valid_masks[1:]:
        count = count + v.astype(int)
    result = total.where(count >= 3, np.nan)
    result.name = None
    return result
macro_trend_beta_alignmentotherAligns stocks’ trailing cross-asset macro betas (oil, USD, rates, credit) with the prevailing direction of…

Aligns stocks’ trailing cross-asset macro betas (oil, USD, rates, credit) with the prevailing direction of the corresponding macro trend, exploiting gradual investor diffusion of macro information into stock prices with differentiated factor exposure.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    dates = panel.index.get_level_values('date')
    # --- date-level macro trends (unique per date) ---
    macro_cols = [
        'macro_oil', 'macro_oil_chg12m',
        'macro_usd', 'macro_usd_chg12m',
        'macro_dgs10_chg12m', 'macro_baa_spread_chg12m',
    ]
    macro_df = panel[macro_cols].copy()
    macro_df = macro_df.reset_index(level='ticker', drop=True)
    macro_df = macro_df[~macro_df.index.duplicated(keep='first')].sort_index()
    # unitless trends with guarded denominators
    oil_denom = macro_df['macro_oil'].where(macro_df['macro_oil'] > 0)
    usd_denom = macro_df['macro_usd'].where(macro_df['macro_usd'] > 0)
    raw_tr_oil = macro_df['macro_oil_chg12m'] / oil_denom
    raw_tr_usd = macro_df['macro_usd_chg12m'] / usd_denom
    raw_tr_rate = macro_df['macro_dgs10_chg12m']
    raw_tr_cred = macro_df['macro_baa_spread_chg12m']
    tr_oil = self._z_exp_past_only(raw_tr_oil)
    tr_usd = self._z_exp_past_only(raw_tr_usd)
    tr_rate = self._z_exp_past_only(raw_tr_rate)
    tr_cred = self._z_exp_past_only(raw_tr_cred)
    # broadcast date-level trends back onto panel
    tr_oil_b = tr_oil.reindex(dates).to_numpy()
    tr_usd_b = tr_usd.reindex(dates).to_numpy()
    tr_rate_b = tr_rate.reindex(dates).to_numpy()
    tr_cred_b = tr_cred.reindex(dates).to_numpy()
    tr_oil_s = pd.Series(tr_oil_b, index=panel.index)
    tr_usd_s = pd.Series(tr_usd_b, index=panel.index)
    tr_rate_s = pd.Series(tr_rate_b, index=panel.index)
    tr_cred_s = pd.Series(tr_cred_b, index=panel.index)
    # --- cross-sectional z-scores of betas within each date ---
    def cs_z(col: str) -> pd.Series:
        s = panel[col]
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        z = (s - mean) / std.replace(0, np.nan)
        return z
    bo = cs_z('beta_oil_252')
    bu = cs_z('beta_usd_252')
    br = cs_z('beta_rates_252')
    bc = cs_z('beta_credit_252')
    n_valid = (
        bo.notna().astype(int)
        + bu.notna().astype(int)
        + br.notna().astype(int)
        + bc.notna().astype(int)
    )
    score = (
        bo.fillna(0) * tr_oil_s
        + bu.fillna(0) * tr_usd_s
        + br.fillna(0) * tr_rate_s
        + bc.fillna(0) * tr_cred_s
    )
    score = score.where(n_valid >= 3)
    return score.reindex(panel.index)
macro_trend_beta_spilloverotherInteracts trailing macro factor trends (oil, USD, rates, credit) with each stock’s estimated macro betas to…

Interacts trailing macro factor trends (oil, USD, rates, credit) with each stock’s estimated macro betas to produce a macro-implied expected return that diffuses gradually into equity prices.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    dates = panel.index.get_level_values('date')
    # date-level macro series (one value per date)
    macro_oil = panel['macro_oil'].groupby(dates).first()
    macro_oil_chg12m = panel['macro_oil_chg12m'].groupby(dates).first()
    macro_usd = panel['macro_usd'].groupby(dates).first()
    macro_usd_chg12m = panel['macro_usd_chg12m'].groupby(dates).first()
    macro_dgs10_chg12m = panel['macro_dgs10_chg12m'].groupby(dates).first()
    macro_baa_spread_chg12m = panel['macro_baa_spread_chg12m'].groupby(dates).first()
    macro_oil = macro_oil.sort_index()
    macro_oil_chg12m = macro_oil_chg12m.sort_index()
    macro_usd = macro_usd.sort_index()
    macro_usd_chg12m = macro_usd_chg12m.sort_index()
    macro_dgs10_chg12m = macro_dgs10_chg12m.sort_index()
    macro_baa_spread_chg12m = macro_baa_spread_chg12m.sort_index()
    oil_denom = macro_oil - macro_oil_chg12m
    oil_tr = macro_oil_chg12m / oil_denom.where(oil_denom > 0)
    usd_denom = macro_usd - macro_usd_chg12m
    usd_tr = macro_usd_chg12m / usd_denom.where(usd_denom > 0)
    rate_tr = macro_dgs10_chg12m
    cred_tr = macro_baa_spread_chg12m
    def past_only_z(x: pd.Series) -> pd.Series:
        x_shift = x.shift(1)
        std = x_shift.expanding(min_periods=36).std()
        return x / std.where(std > 0)
    z_oil = past_only_z(oil_tr)
    z_usd = past_only_z(usd_tr)
    z_rate = past_only_z(rate_tr)
    z_cred = past_only_z(cred_tr)
    # map back to panel by date
    z_oil_p = dates.map(z_oil)
    z_usd_p = dates.map(z_usd)
    z_rate_p = dates.map(z_rate)
    z_cred_p = dates.map(z_cred)
    z_oil_p = pd.Series(z_oil_p, index=panel.index)
    z_usd_p = pd.Series(z_usd_p, index=panel.index)
    z_rate_p = pd.Series(z_rate_p, index=panel.index)
    z_cred_p = pd.Series(z_cred_p, index=panel.index)
    # winsorize betas cross-sectionally per date
    beta_oil = panel['beta_oil_252'].groupby(level='date').transform(self._winsorize)
    beta_usd = panel['beta_usd_252'].groupby(level='date').transform(self._winsorize)
    beta_rates = panel['beta_rates_252'].groupby(level='date').transform(self._winsorize)
    beta_credit = panel['beta_credit_252'].groupby(level='date').transform(self._winsorize)
    term_oil = beta_oil * z_oil_p
    term_usd = beta_usd * z_usd_p
    term_rate = beta_rates * z_rate_p
    term_cred = beta_credit * z_cred_p
    terms = pd.concat(
        [term_oil.rename('t_oil'), term_usd.rename('t_usd'),
         term_rate.rename('t_rate'), term_cred.rename('t_cred')],
        axis=1
    )
    valid_count = terms.notna().sum(axis=1)
    total = terms.sum(axis=1, skipna=True)
    result = total.where(valid_count >= 3)
    return result
macro_trend_exposure_alignmentotherAligns firm-level macro betas (oil, USD, rates, credit) with the persistent trend in those macro factors…

Aligns firm-level macro betas (oil, USD, rates, credit) with the persistent trend in those macro factors (expanding z-scored 12m changes), exploiting slow diffusion of cross-asset macro trends into equity cash-flow expectations.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    idx = panel.index
    macro_cols = {
        'oil': 'macro_oil_chg12m',
        'usd': 'macro_usd_chg12m',
        'rates': 'macro_dgs10_chg12m',
        'credit': 'macro_baa_spread_chg12m',
    }
    beta_cols = {
        'oil': 'beta_oil_252',
        'usd': 'beta_usd_252',
        'rates': 'beta_rates_252',
        'credit': 'beta_credit_252',
    }
    # Build date-level macro series (one value per date) and compute expanding z-scores
    z_terms = {}
    dates = idx.get_level_values('date')
    for key, col in macro_cols.items():
        if col in panel.columns:
            per_date = panel[col].groupby(level='date').first()
            per_date = per_date.sort_index()
            z_per_date = self._expanding_z(per_date)
            z_terms[key] = z_per_date.reindex(dates).to_numpy()
        else:
            z_terms[key] = np.zeros(len(idx))
    # Winsorize betas cross-sectionally per date
    beta_win = {}
    beta_missing_mask = pd.Series(False, index=idx)
    for key, col in beta_cols.items():
        if col in panel.columns:
            b = panel[col]
            beta_missing_mask = beta_missing_mask | b.isna()
            b_w = self._winsorize_by_date(b)
            beta_win[key] = b_w
        else:
            beta_missing_mask = pd.Series(True, index=idx)
            beta_win[key] = pd.Series(np.nan, index=idx)
    score = pd.Series(0.0, index=idx)
    for key in macro_cols.keys():
        b = beta_win[key].fillna(0.0)
        z = pd.Series(z_terms[key], index=idx)
        score = score + b * z
    score = score.where(~beta_missing_mask, np.nan)
    return score
oil_beta_trend_interactionotherInteraction of trailing stock beta to oil price moves with a 12m oil price trend regime scaler

Interaction of trailing stock beta to oil price moves with a 12m oil price trend regime scaler. Long high-oil-beta stocks when oil has trended up over the past year, short them when oil has trended down, capturing gradual diffusion of commodity trends into cash-flow-exposed equity valuations.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta_oil = panel['beta_oil_252']
    corr_oil = panel['corr_oil_252']
    oil_chg12m = panel['macro_oil_chg12m']
    oil_trend = np.tanh(oil_chg12m / 20.0)
    exposure = beta_oil.where(corr_oil.abs() > 0.05)
    def _zscore(x):
        std = x.std()
        if std is None or std == 0 or pd.isna(std):
            return pd.Series(np.nan, index=x.index)
        return (x - x.median()) / std
    exposure_z = exposure.groupby(level='date').transform(_zscore)
    signal = exposure_z * oil_trend
    signal = signal.reindex(panel.index)
    return signal
pre_earnings_filing_clock_premiumotherWithin-firm earnings-announcement-clock seasonality: proxies proximity to the next expected SEC filing using…

Within-firm earnings-announcement-clock seasonality: proxies proximity to the next expected SEC filing using days since the last point-in-time filing date. Peaks when days_since_filing is near 85 (i.e., the next filing is likely due within the coming month), capturing the documented earnings announcement premium driven by announcement-risk compensation and attention scarcity.

Category otherDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    datekey = pd.to_datetime(panel["fund_datekey"], errors="coerce")
    asof = pd.to_datetime(panel["asof_date"], errors="coerce")
    valid_dates = datekey.notna() & asof.notna()
    days_since_filing = (asof - datekey).dt.days
    cycle_position = -(days_since_filing - 85).abs().astype(float)
    in_range = (days_since_filing >= 20) & (days_since_filing <= 200)
    signal = cycle_position.where(valid_dates & in_range, np.nan)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal
predicted_dividend_month_premiumotherPredicts dividend-clientele driven price pressure by inferring implied monthly dividend yield from the wedge…

Predicts dividend-clientele driven price pressure by inferring implied monthly dividend yield from the wedge between adjusted and raw close returns, then projecting it forward 12 months using sticky dividend payment calendars, with support confirmation from recent dividend history.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    closeadj = panel['closeadj']
    close = panel['close']
    grp_adj = closeadj.groupby(level='ticker')
    grp_close = close.groupby(level='ticker')
    closeadj_prev = grp_adj.shift(1)
    close_prev = grp_close.shift(1)
    valid = (
        closeadj.notna() & closeadj_prev.notna() & (closeadj_prev != 0)
        & close.notna() & close_prev.notna() & (close_prev != 0)
    )
    radj = (closeadj / closeadj_prev) - 1.0
    rraw = (close / close_prev) - 1.0
    diff = radj - rraw
    impl_yld = diff.clip(lower=0.0, upper=0.05)
    impl_yld = impl_yld.where(valid, np.nan)
    # split artifact detection
    split_artifact = diff.abs() > 0.5
    impl_yld = impl_yld.where(~split_artifact, np.nan)
    # need sufficient history for shift(12): closeadj/close shifted 12 must exist
    closeadj_12 = grp_adj.shift(12)
    close_12 = grp_close.shift(12)
    closeadj_13 = grp_adj.shift(13)
    close_13 = grp_close.shift(13)
    hist_ok = (
        closeadj_12.notna() & closeadj_13.notna()
        & close_12.notna() & close_13.notna()
    )
    impl_yld_grp = impl_yld.groupby(level='ticker')
    pred = impl_yld_grp.shift(12)
    pred = pred.where(hist_ok, np.nan)
    shift3 = impl_yld_grp.shift(3)
    shift12 = impl_yld_grp.shift(12)
    support_cond = (shift3 > 0) | (shift12 > 0)
    support = support_cond.astype(float)
    support = support.where(shift3.notna() | shift12.notna(), np.nan)
    score = pred * support
    # no dividend history in past 12m -> score 0
    # define "dividend history in past 12m" as any positive impl_yld in trailing 12 shifts (1..12)
    pos_yld = (impl_yld > 0).astype(float)
    pos_yld_grp = pos_yld.groupby(level='ticker')
    rolling_hist = pos_yld_grp.transform(
        lambda s: s.shift(1).rolling(window=12, min_periods=1).max()
    )
    no_history = rolling_hist.fillna(0) == 0
    score = score.where(~no_history, 0.0)
    score = score.reindex(panel.index)
    return score
rate_beta_duration_mismatchotherMismatch between valuation-implied duration and realized rate beta, aligned with the sign of the trailing 12m…

Mismatch between valuation-implied duration and realized rate beta, aligned with the sign of the trailing 12m change in 10y yields; signal is masked where the stock’s rate correlation is weak.

Category otherDirection Long the high scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    pb = panel['pb']
    ps = panel['ps']
    divyield = panel['divyield'].fillna(0)
    beta_rates = panel['beta_rates_252']
    corr_rates = panel['corr_rates_252']
    macro_chg = panel['macro_dgs10_chg12m']
    def rk(x):
        return x.groupby(level='date').rank(pct=True)
    D = (rk(pb) + rk(ps) + (1 - rk(divyield))) / 3
    beta_mean = beta_rates.groupby(level='date').transform('mean')
    beta_std = beta_rates.groupby(level='date').transform('std')
    zb = (beta_rates - beta_mean) / beta_std.where(beta_std > 0)
    D_std = D.groupby(level='date').transform('std')
    zD = (D - 0.5) / D_std.where(D_std > 0)
    mismatch = zb + zD
    signal = mismatch * np.sign(macro_chg)
    invalid = (
        pb.isna()
        | ps.isna()
        | beta_rates.isna()
        | corr_rates.isna()
        | macro_chg.isna()
        | (corr_rates.abs() < 0.02)
    )
    signal = signal.where(~invalid, np.nan)
    return signal.reindex(panel.index)
relative_month_of_year_seasonalityotherStock-level calendar month-of-year seasonality: average return in the same calendar month over the past 1-5…

Stock-level calendar month-of-year seasonality: average return in the same calendar month over the past 1-5 years, benchmarked against the average return in adjacent calendar months to strip out level/drift effects. Higher relative same-month seasonality predicts higher future return.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    adj_lags = [11, 13, 23, 25, 35, 37, 47, 49]
    same_shifts = [grp.shift(k) for k in same_lags]
    adj_shifts = [grp.shift(k) for k in adj_lags]
    same_df = pd.concat(same_shifts, axis=1)
    adj_df = pd.concat(adj_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    adj_count = adj_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    adj_mean = adj_df.mean(axis=1)
    signal = same_mean - adj_mean
    valid = (same_count >= 3) & (adj_count >= 5)
    signal = signal.where(valid, np.nan)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal
residual_same_month_seasonalityotherHeston-Sadka same-calendar-month seasonality: average of a stock’s ret_1m in the same calendar month over the…

Heston-Sadka same-calendar-month seasonality: average of a stock’s ret_1m in the same calendar month over the trailing 5 years, minus its average ret_12m/12 over the same lags to strip out level/momentum, isolating a firm-specific seasonal return component.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel.groupby(level='ticker')
    ret1m_lags = [g['ret_1m'].shift(k) for k in (12, 24, 36, 48, 60)]
    ret12m_lags = [g['ret_12m'].shift(k) for k in (12, 24, 36, 48, 60)]
    ret1m_df = pd.concat(ret1m_lags, axis=1)
    ret12m_df = pd.concat(ret12m_lags, axis=1)
    valid_count = ret1m_df.notna().sum(axis=1)
    seas = ret1m_df.mean(axis=1, skipna=True)
    seas = seas.where(valid_count >= 3, np.nan)
    base = ret12m_df.mean(axis=1, skipna=True) / 12.0
    base_valid = ret12m_df.notna().any(axis=1)
    base = base.where(base_valid, 0.0)
    signal = seas - base
    signal = signal.where(seas.notna(), np.nan)
    signal.name = self.name
    return signal.reindex(panel.index)
same_calendar_month_seasonalityotherSeasonality factor based on Heston and Sadka (2008): averages a stock’s same-calendar-month historical…

Seasonality factor based on Heston and Sadka (2008): averages a stock’s same-calendar-month historical 1-month returns lagged 12, 24, 36, 48, and 60 months, winsorized cross-sectionally at the 1st/99th percentile per date, requiring at least 3 non-missing lags.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    lags = [12, 24, 36, 48, 60]
    ret = panel["ret_1m"]
    winsorized_lags = []
    for k in lags:
        r_k = ret.groupby(level="ticker").shift(k)
        def _winsorize(s: pd.Series) -> pd.Series:
            valid = s.dropna()
            if valid.empty:
                return s
            lo = valid.quantile(0.01)
            hi = valid.quantile(0.99)
            return s.clip(lower=lo, upper=hi)
        r_k_w = r_k.groupby(level="date").transform(_winsorize)
        winsorized_lags.append(r_k_w)
    lag_df = pd.concat(winsorized_lags, axis=1)
    lag_df.columns = [f"lag_{k}" for k in lags]
    valid_count = lag_df.notna().sum(axis=1)
    mean_ret = lag_df.mean(axis=1, skipna=True)
    signal = mean_ret.where(valid_count >= 3, np.nan)
    signal.name = self.name
    return signal
same_month_return_seasonalityotherSame-calendar-month return seasonality: average of ret_1m at lags of 12,24,36,48,60 months (same calendar…

Same-calendar-month return seasonality: average of ret_1m at lags of 12,24,36,48,60 months (same calendar month) minus average of ret_1m at all other lags 1..60 (excluding multiples of 12). Requires at least 3 non-missing same-month observations.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    other_lags = [k for k in range(1, 61) if k % 12 != 0]
    same_shifts = [grp.shift(k) for k in same_lags]
    other_shifts = [grp.shift(k) for k in other_lags]
    same_df = pd.concat(same_shifts, axis=1)
    other_df = pd.concat(other_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    other_mean = other_df.mean(axis=1)
    signal = same_mean - other_mean
    valid = (same_count >= 3) & other_mean.notna()
    signal = signal.where(valid, np.nan)
    return signal.reindex(panel.index)
same_month_return_seasonality_spreadotherFirm-level calendar-month return seasonality: averages a stock’s own ret_1m in the same calendar month over…

Firm-level calendar-month return seasonality: averages a stock’s own ret_1m in the same calendar month over the last 1-5 years (lags 12,24,36,48,60) and differences it against the average return in non-anniversary months (lags 3,6,9,15,18,21,27,33), isolating a seasonal effect distinct from generic long-horizon momentum/reversal.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    seasonal_lags = [12, 24, 36, 48, 60]
    nonseasonal_lags = [3, 6, 9, 15, 18, 21, 27, 33]
    seasonal_shifts = [g.shift(k) for k in seasonal_lags]
    nonseasonal_shifts = [g.shift(k) for k in nonseasonal_lags]
    seasonal_df = pd.concat(seasonal_shifts, axis=1)
    nonseasonal_df = pd.concat(nonseasonal_shifts, axis=1)
    seasonal_count = seasonal_df.notna().sum(axis=1)
    nonseasonal_count = nonseasonal_df.notna().sum(axis=1)
    seasonal_mean = seasonal_df.mean(axis=1, skipna=True)
    nonseasonal_mean = nonseasonal_df.mean(axis=1, skipna=True)
    seasonal_mean = seasonal_mean.where(seasonal_count >= 3)
    nonseasonal_mean = nonseasonal_mean.where(nonseasonal_count >= 4)
    signal = seasonal_mean - nonseasonal_mean
    signal = signal.reindex(panel.index)
    return signal
same_month_seasonal_persistenceotherSame-calendar-month seasonal persistence: average of a stock’s ret_1m lagged by 12,24,36,48,60 months minus…

Same-calendar-month seasonal persistence: average of a stock’s ret_1m lagged by 12,24,36,48,60 months minus average of ret_1m lagged by 13,25,37 months (adjacent-month control), requiring at least 3 non-NaN seasonal lags.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    seas_lags = [12, 24, 36, 48, 60]
    nonseas_lags = [13, 25, 37]
    seas_shifts = [g.shift(k) for k in seas_lags]
    seas_df = pd.concat(seas_shifts, axis=1)
    seas_count = seas_df.notna().sum(axis=1)
    seas_mean = seas_df.mean(axis=1, skipna=True)
    seas_mean = seas_mean.where(seas_count >= 3)
    nonseas_shifts = [g.shift(k) for k in nonseas_lags]
    nonseas_df = pd.concat(nonseas_shifts, axis=1)
    nonseas_mean = nonseas_df.mean(axis=1, skipna=True)
    nonseas_mean = nonseas_mean.fillna(0.0)
    score = seas_mean - nonseas_mean
    score = score.where(seas_count >= 3)
    score = score.reindex(panel.index)
    score.name = self.name
    return score
same_month_seasonality_residualotherFirm-specific calendar-month seasonality: average return in the same calendar month over the past 1-5 years,…

Firm-specific calendar-month seasonality: average return in the same calendar month over the past 1-5 years, minus the average return of adjacent months in those same years, isolating a pure seasonal component net of slow-moving momentum/reversal.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    adj_lags = [11, 13, 23, 25, 35, 37, 47, 49]
    same_shifts = [grp.shift(k) for k in same_lags]
    adj_shifts = [grp.shift(k) for k in adj_lags]
    same_df = pd.concat(same_shifts, axis=1)
    adj_df = pd.concat(adj_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1, skipna=True)
    adj_mean = adj_df.mean(axis=1, skipna=True)
    adj_count = adj_df.notna().sum(axis=1)
    signal = same_mean - adj_mean
    valid = (same_count >= 3) & (adj_count > 0)
    signal = signal.where(valid, np.nan)
    return signal.reindex(panel.index)
same_quarter_earnings_seasonalityotherSeasonal earnings anticipation signal: compares net income from the same fiscal quarter one year ago (about…

Seasonal earnings anticipation signal: compares net income from the same fiscal quarter one year ago (about to be reported again) to the firm’s trailing four-quarter average net income, scaled by assets from that period. Captures underreaction to persistent seasonal earnings patterns (Chang, Hartzmark, Solomon, Sosyura 2017) distinct from generic momentum or SUE.

Category otherDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    ni = panel['netinc']
    a = panel['assets']
    grp = ni.groupby(level='ticker')
    seasonal_lag = grp.shift(12)
    lags = [grp.shift(k) for k in [3, 6, 9, 12]]
    lag_df = pd.concat(lags, axis=1)
    lag_df.columns = ['l3', 'l6', 'l9', 'l12']
    count_valid = lag_df.notna().sum(axis=1)
    base = lag_df.mean(axis=1, skipna=True)
    base = base.where(count_valid >= 3, np.nan)
    assets_lag12 = a.groupby(level='ticker').shift(12)
    denom = assets_lag12.where(assets_lag12 > 0, np.nan)
    raw = (seasonal_lag - base) / denom
    def winsorize(x):
        valid = x.dropna()
        if valid.empty:
            return x
        lo, hi = valid.quantile(0.01), valid.quantile(0.99)
        return x.clip(lower=lo, upper=hi)
    raw = raw.groupby(level='date').transform(winsorize)
    return raw.reindex(panel.index)
tax_loss_overhang_seasonalotherCalendar-conditioned tax-loss-selling reversal: identifies loss-overhang stocks (large distance from 52-week…

Calendar-conditioned tax-loss-selling reversal: identifies loss-overhang stocks (large distance from 52-week high with negative trailing 12m return), tilted toward smaller caps, and shorts them during Oct-Nov selling pressure while going long at end of December to capture the January rebound.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    high_ratio = panel['high_ratio']
    ret_12m = panel['ret_12m']
    marketcap = panel['marketcap']
    valid = high_ratio.notna() & ret_12m.notna() & marketcap.notna() & (marketcap > 0)
    overhang = (1 - high_ratio) * (ret_12m < 0).astype(float)
    neg_log_mcap = -np.log(marketcap.where(marketcap > 0))
    rank = neg_log_mcap.groupby(level='date').rank(pct=True)
    size_tilt = 1 + rank
    ov = overhang * size_tilt
    ov = ov.where(valid, np.nan)
    dates = panel.index.get_level_values('date')
    month = pd.to_datetime(dates).month
    w = pd.Series(0.0, index=panel.index)
    w[month == 12] = 1.0
    w[(month == 10) | (month == 11)] = -1.0
    score = w * ov
    # months where w==0 -> score should be 0, not NaN, even if ov is NaN
    score = score.where(~((w == 0)), 0.0)
    return score
tax_loss_selling_seasonal_reversalotherTax-loss selling seasonal reversal: stocks with large embedded losses, far from 52-week highs, high…

Tax-loss selling seasonal reversal: stocks with large embedded losses, far from 52-week highs, high volatility, and small market cap face price pressure from taxable-investor loss harvesting in Nov-Dec, which reverses in January. Score is negative loss-overhang in Nov/Dec (anticipating further pressure downward) and positive loss-overhang in January (anticipating rebound).

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    high_ratio = panel['high_ratio']
    ret_12m = panel['ret_12m']
    marketcap = panel['marketcap']
    vol_252 = panel['vol_252']
    valid = (
        (marketcap > 0)
        & high_ratio.gt(0) & high_ratio.le(1)
        & (vol_252 > 0)
        & ret_12m.notna()
    )
    log_mc = np.log(marketcap.where(marketcap > 0))
    loss_overhang = (
        (1 - high_ratio)
        * (ret_12m < 0).astype(float)
        * vol_252
        / log_mc
    )
    loss_overhang = loss_overhang.where(valid)
    month = panel.index.get_level_values('date').month
    month = pd.Series(month, index=panel.index)
    score = pd.Series(np.nan, index=panel.index, dtype=float)
    mask_pressure = month.isin([11, 12])
    mask_rebound = month.eq(1)
    score = score.where(~mask_pressure, -loss_overhang)
    score = score.where(~mask_rebound, loss_overhang)
    return score
tax_loss_selling_turn_of_year_pressureotherCapital-loss overhang (max(0,-ret_12m)) scaled by proximity-to-low (1 - high_ratio) and volatility, applied…

Capital-loss overhang (max(0,-ret_12m)) scaled by proximity-to-low (1 - high_ratio) and volatility, applied only in Nov/Dec: shorted in November while tax-loss selling pressure is ongoing, bought at end-December to capture the January rebound.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret_12m = panel['ret_12m']
    high_ratio = panel['high_ratio']
    vol_252 = panel['vol_252']
    valid = ret_12m.notna() & high_ratio.notna() & vol_252.notna()
    pressure = (-ret_12m).clip(lower=0.0) * (1.0 - high_ratio) * vol_252
    month = panel.index.get_level_values('date').month
    month = pd.Series(month, index=panel.index)
    signal = pd.Series(np.nan, index=panel.index, dtype=float)
    dec_mask = valid & (month == 12)
    nov_mask = valid & (month == 11)
    signal[dec_mask] = pressure[dec_mask]
    signal[nov_mask] = -pressure[nov_mask]
    return signal
tax_loss_selling_turn_of_year_reversalotherTax-loss-selling turn-of-year reversal: stocks far below their 52-week high with poor 12-1 momentum…

Tax-loss-selling turn-of-year reversal: stocks far below their 52-week high with poor 12-1 momentum experience selling pressure from taxable investors harvesting losses in Nov-Dec, which reverses in January. Signal is positive in January, negative in Nov/Dec, and zero otherwise, scaled by an embedded capital-loss proxy normalized by log market cap.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    high_ratio = panel['high_ratio']
    ret_12_1 = panel['ret_12_1']
    marketcap = panel['marketcap']
    valid = (
        high_ratio.notna() & ret_12_1.notna() & marketcap.notna()
        & (marketcap > 1)
        & (high_ratio > 0) & (high_ratio <= 1)
    )
    log_mc = np.log(marketcap.where(valid))
    neg_ret = (-ret_12_1).clip(lower=0.0)
    embedded_loss = (1 - high_ratio) * (1 + neg_ret) / log_mc
    embedded_loss = embedded_loss.where(valid)
    dates = panel.index.get_level_values('date')
    month = pd.Series(dates, index=panel.index).dt.month
    mult = pd.Series(0.0, index=panel.index)
    mult[month == 1] = 1.0
    mult[month.isin([11, 12])] = -1.0
    raw = mult * embedded_loss
    raw = raw.where(valid)
    return raw
turn_of_year_tax_loss_pressureotherTax-loss-selling overhang: stocks far below 52-week high, volatile, and down over the trailing year…

Tax-loss-selling overhang: stocks far below 52-week high, volatile, and down over the trailing year experience predictable selling pressure into year-end (Nov/Dec) as taxable investors realize losses, and a mechanical reversal in January when the selling pressure abates. Score is negative overhang in Nov/Dec (short loss-overhang names) and positive overhang in January (own loss-overhang names), NaN otherwise.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    high_ratio = panel['high_ratio']
    ret_12m = panel['ret_12m']
    vol_252 = panel['vol_252']
    valid = high_ratio.notna() & ret_12m.notna() & vol_252.notna()
    loss_flag = (ret_12m < 0).astype(float)
    overhang = (1.0 - high_ratio) * vol_252 * loss_flag
    overhang = overhang.where(valid, np.nan)
    dates = panel.index.get_level_values('date')
    month = pd.Series(dates, index=panel.index).dt.month
    score = pd.Series(np.nan, index=panel.index, dtype=float)
    nov_dec_mask = month.isin([10, 11])
    jan_mask = month == 12
    score = score.where(~nov_dec_mask, -overhang)
    score = score.where(~jan_mask, overhang)
    return score
turn_of_year_tax_loss_rebound_intensityotherSeasonally-gated tax-loss selling rebound signal: combines proximity to 52-week low, poor 12-month returns,…

Seasonally-gated tax-loss selling rebound signal: combines proximity to 52-week low, poor 12-month returns, and high volatility (scaled inversely by log market cap) into a loss-selling pressure measure. This pressure is expressed long in Nov/Dec/Jan (tax-loss selling and January rebound window) and reversed (shorted) in May/Jun/Jul (mid-year window-dressing/momentum dominance), zero otherwise.

Category otherDirection Long the high scoresData price
def compute(self, panel) -> pd.Series:
    ret_12m = panel['ret_12m']
    high_ratio = panel['high_ratio']
    vol_252 = panel['vol_252']
    marketcap = panel['marketcap']
    valid = (
        ret_12m.notna()
        & high_ratio.notna()
        & vol_252.notna()
        & marketcap.notna()
        & (marketcap > 0)
    )
    log_mktcap = np.log(marketcap.where(marketcap > 0))
    neg_ret = (-ret_12m).clip(lower=0)
    pressure = (1 - high_ratio) * neg_ret * vol_252 / log_mktcap
    dates = panel.index.get_level_values('date')
    month = pd.Series(dates, index=panel.index).dt.month
    gate = pd.Series(0.0, index=panel.index)
    gate[month.isin([11, 12, 1])] = 1.0
    gate[month.isin([5, 6, 7])] = -0.5
    score = gate * pressure
    score = score.where(valid, np.nan)
    return score
usd_exposure_dollar_trend_interactionotherInteraction of a stock’s trailing USD beta with the prevailing 12m dollar trend (normalized by expanding…

Interaction of a stock’s trailing USD beta with the prevailing 12m dollar trend (normalized by expanding past-only volatility), capturing slow incorporation of currency exposure into prices conditioned on FX momentum.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    beta = panel['beta_usd_252'].copy()
    corr = panel['corr_usd_252']
    # winsorize beta at 1/99 pct cross-sectionally each date
    def winsorize(s):
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    beta_w = beta.groupby(level='date').transform(winsorize)
    # keep only where corr magnitude significant
    exposure = beta_w.where(corr.abs() > 0.05, np.nan)
    # date-level trend computation
    usd_chg = panel['macro_usd_chg12m'].groupby(level='date').first()
    usd_chg = usd_chg.sort_index()
    # expanding past-only std (exclude current date), min_periods for stability
    past_std = usd_chg.shift(1).expanding(min_periods=12).std()
    trend = usd_chg / past_std
    trend = trend.where(past_std > 0, np.nan)
    trend_full = trend.reindex(panel.index.get_level_values('date')).values
    trend_series = pd.Series(trend_full, index=panel.index)
    signal = exposure * trend_series
    return signal
vix_conditioned_reversal_flipotherShort-term reversal scaled by trailing volatility and conditioned on a VIX z-score: when VIX is unusually…

Short-term reversal scaled by trailing volatility and conditioned on a VIX z-score: when VIX is unusually elevated relative to its trailing history, reversal is strong and rewarded; when VIX is unusually low, the sign flips toward continuation.

Category otherDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    vix = panel['macro_vix'].groupby(level='date').first()
    vix = vix.sort_index()
    vix_shift = vix.shift(1)
    vix_mean = vix_shift.rolling(60, min_periods=24).mean()
    vix_std = vix_shift.rolling(60, min_periods=24).std()
    vz = (vix - vix_mean) / vix_std
    vz = vz.replace([np.inf, -np.inf], np.nan)
    vz = vz.clip(-2, 2)
    vz_mapped = panel.index.get_level_values('date').map(vz)
    vz_mapped = pd.Series(vz_mapped, index=panel.index)
    vol_252 = panel['vol_252']
    ret_1m = panel['ret_1m']
    vol_safe = vol_252.where(vol_252 > 0)
    scaled_rev = -ret_1m / vol_safe
    score = scaled_rev * vz_mapped
    score = score.where(vol_safe.notna() & ret_1m.notna() & vz_mapped.notna())
    return score
abnormal_reporting_lagqualityFiling delay (fund_datekey - fund_calendardate) abnormally long relative to the firm’s own history 12 months…

Filing delay (fund_datekey - fund_calendardate) abnormally long relative to the firm’s own history 12 months ago, combined with staleness of the current filing (asof_date - fund_datekey), predicts underperformance due to market underreaction to soft disclosure-timing signals of bad news.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    datekey = pd.to_datetime(panel['fund_datekey'])
    calendardate = pd.to_datetime(panel['fund_calendardate'])
    asof = pd.to_datetime(panel['asof_date'])
    valid = datekey.notna() & calendardate.notna() & asof.notna()
    lag_days = (datekey - calendardate).dt.days.astype(float)
    lag_days = lag_days.where(valid, np.nan)
    lag_days = lag_days.clip(lower=0, upper=400)
    stale_days = (asof - datekey).dt.days.astype(float)
    stale_days = stale_days.where(valid, np.nan)
    lag_shift12 = lag_days.groupby(level='ticker').shift(12)
    ab_lag = lag_days - lag_shift12
    ab_lag = ab_lag.where(lag_shift12.notna(), np.nan)
    z_ab_lag = _cs_z(ab_lag)
    z_stale = _cs_z(stale_days)
    score = 0.7 * z_ab_lag + 0.3 * z_stale
    score = score.where(valid & lag_shift12.notna(), np.nan)
    return score.reindex(panel.index)
accrualsqualitySloan accruals = (net income - operating cash flow) / assets; high accruals underperform

Sloan accruals = (net income - operating cash flow) / assets; high accruals underperform.

Category qualityDirection Long the low scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    assets = panel["assets"]
    return ((panel["netinc"] - panel["ncfo"]) / assets).where(assets > 0)
book_tax_gap_earnings_qualityqualityBook-tax gap earnings quality: firms with effective tax rates near peer median (i.e., not unusually low) and…

Book-tax gap earnings quality: firms with effective tax rates near peer median (i.e., not unusually low) and with book income confirmed by taxable income (high netinc/ebt conformity) exhibit more persistent earnings and higher future abnormal returns.

Category qualityDirection Long the high scoresData fundamental
def compute(self, panel) -> pd.Series:
    taxexp = panel['taxexp']
    ebt = panel['ebt']
    netinc = panel['netinc']
    revenue = panel['revenue']
    sector = panel['sector']
    valid = (
        ebt.notna() & (ebt > 0)
        & taxexp.notna()
        & revenue.notna() & (revenue > 0)
        & sector.notna()
    )
    etr = pd.Series(np.nan, index=panel.index)
    etr_raw = taxexp / ebt
    etr_raw = etr_raw.clip(lower=0, upper=0.6)
    etr[valid] = etr_raw[valid]
    conf = pd.Series(np.nan, index=panel.index)
    conf_raw = netinc / ebt
    conf[valid] = conf_raw[valid]
    date_level = panel.index.get_level_values('date')
    # Median etr within (date, sector), computed only over valid rows.
    etr_valid_only = etr.where(valid)
    group_keys = [date_level, sector]
    sector_median = etr_valid_only.groupby(group_keys).transform('median')
    raw = etr - sector_median
    # Cross-sectional zscore of conf within date, computed only over valid rows.
    conf_valid_only = conf.where(valid)
    grp = conf_valid_only.groupby(date_level)
    mean_c = grp.transform('mean')
    std_c = grp.transform('std')
    zscore_conf = (conf_valid_only - mean_c) / std_c.replace(0, np.nan)
    signal = raw - 0.25 * zscore_conf
    signal = signal.where(valid)
    return signal
cash_backed_earnings_surprisequalityStandardized unexpected earnings (SUE) combined with a penalty for accrual-driven surprise: isolates the…

Standardized unexpected earnings (SUE) combined with a penalty for accrual-driven surprise: isolates the cash-backed portion of earnings surprises which is expected to drift more persistently, while accrual-tainted surprises mean-revert.

Category qualityDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    epsdil = panel['epsdil']
    netinc = panel['netinc']
    ncfo = panel['ncfo']
    assets = panel['assets']
    g = epsdil.groupby(level='ticker')
    # SUE calculation
    d_eps = epsdil - g.shift(12)
    def rolling_std_24(s):
        return s.rolling(24, min_periods=8).std()
    d_eps_std = d_eps.groupby(level='ticker').transform(rolling_std_24)
    d_eps_std = d_eps_std.where(d_eps_std > 0)
    sue = d_eps / d_eps_std
    sue = sue.clip(-5, 5)
    # Accruals calculation
    acc = (netinc - ncfo) / assets
    acc = acc.where(assets > 0)
    acc = acc.clip(-0.5, 0.5)
    d_acc = acc - acc.groupby(level='ticker').shift(12)
    d_acc = d_acc.clip(-0.5, 0.5)
    def zscore_by_date(s):
        def _z(x):
            mu = x.mean()
            sd = x.std()
            if sd is None or sd == 0 or np.isnan(sd):
                return pd.Series(np.nan, index=x.index)
            return (x - mu) / sd
        return s.groupby(level='date').transform(_z)
    z_sue = zscore_by_date(sue)
    z_dacc = zscore_by_date(d_acc)
    score = z_sue - z_dacc
    return score.reindex(panel.index)
depreciation_conservatism_reservequalityAccounting conservatism reserve: firms with high depreciation rate (depamor/tangibles) and capital intensity…

Accounting conservatism reserve: firms with high depreciation rate (depamor/tangibles) and capital intensity (capex/assets) relative to industry peers understate earnings/book value, implying hidden value and future upward re-rating.

Category qualityDirection Long the high scoresData fundamental
def compute(self, panel) -> pd.Series:
    depamor = panel["depamor"]
    tangibles = panel["tangibles"]
    capex = panel["capex"]
    assets = panel["assets"]
    industry = panel["industry"]
    dep_rate = pd.Series(np.nan, index=panel.index)
    valid_tang = tangibles > 0
    dep_rate.loc[valid_tang] = depamor.loc[valid_tang] / tangibles.loc[valid_tang]
    capint = pd.Series(np.nan, index=panel.index)
    valid_assets = assets > 0
    capint.loc[valid_assets] = capex.loc[valid_assets] / assets.loc[valid_assets]
    valid = (
        valid_tang
        & valid_assets
        & depamor.notna()
        & tangibles.notna()
        & capex.notna()
        & assets.notna()
        & industry.notna()
    )
    dep_rate = dep_rate.where(valid)
    capint = capint.where(valid)
    df = pd.DataFrame({
        "dep_rate": dep_rate,
        "capint": capint,
        "industry": industry,
    })
    group_keys = [df.index.get_level_values("date"), df["industry"]]
    dep_med = df.groupby(group_keys)["dep_rate"].transform("median")
    capint_med = df.groupby(group_keys)["capint"].transform("median")
    score = (df["dep_rate"] - dep_med) + 0.5 * (df["capint"] - capint_med)
    score = score.where(valid)
    return score.reindex(panel.index)
earnings_smoothing_detectionqualityRatio of rolling volatility of net income (scaled by assets) to rolling volatility of operating cash flow…

Ratio of rolling volatility of net income (scaled by assets) to rolling volatility of operating cash flow (scaled by assets), per ticker over the trailing 36 months (min 24). A low ratio indicates aggressive earnings smoothing via accruals relative to underlying cash flow volatility, which predicts lower future returns; expected_sign=+1 (high ratio = less smoothing = better quality).

Category qualityDirection Long the high scoresData fundamental
def compute(self, panel) -> pd.Series:
    assets = panel['assets']
    valid_assets = assets.where(assets > 0)
    ni = panel['netinc'] / valid_assets
    cf = panel['ncfo'] / valid_assets
    ni = ni.replace([np.inf, -np.inf], np.nan)
    cf = cf.replace([np.inf, -np.inf], np.nan)
    sd_ni = ni.groupby(level='ticker').rolling(36, min_periods=24).std()
    sd_cf = cf.groupby(level='ticker').rolling(36, min_periods=24).std()
    sd_ni.index = sd_ni.index.droplevel(0)
    sd_cf.index = sd_cf.index.droplevel(0)
    sd_cf_valid = sd_cf.where(sd_cf > 0)
    signal = sd_ni / sd_cf_valid
    signal = signal.replace([np.inf, -np.inf], np.nan)
    signal = signal.reindex(panel.index)
    return signal
earnings_smoothing_opacityqualityRatio of trailing cash-flow volatility to trailing earnings volatility (both scaled by assets), capturing…

Ratio of trailing cash-flow volatility to trailing earnings volatility (both scaled by assets), capturing earnings smoothing/opacity from accrual management. Low sd_c/sd_e (smooth earnings vs volatile cash flows) indicates managed, low-information earnings and predicts lower future returns.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    assets = panel['assets']
    valid_assets = assets > 0
    ni = (panel['netinc'] / assets).where(valid_assets)
    cf = (panel['ncfo'] / assets).where(valid_assets)
    lags = [0, 3, 6, 9, 12, 15, 18, 21]
    ni_cols = []
    cf_cols = []
    for k in lags:
        ni_k = ni.groupby(level='ticker').shift(k)
        cf_k = cf.groupby(level='ticker').shift(k)
        # also require underlying raw fields present at that lag
        netinc_k = panel['netinc'].groupby(level='ticker').shift(k)
        ncfo_k = panel['ncfo'].groupby(level='ticker').shift(k)
        assets_k = panel['assets'].groupby(level='ticker').shift(k)
        valid_k = netinc_k.notna() & ncfo_k.notna() & assets_k.notna() & (assets_k > 0)
        ni_k = ni_k.where(valid_k)
        cf_k = cf_k.where(valid_k)
        ni_cols.append(ni_k)
        cf_cols.append(cf_k)
    ni_df = pd.concat(ni_cols, axis=1)
    cf_df = pd.concat(cf_cols, axis=1)
    ni_count = ni_df.notna().sum(axis=1)
    cf_count = cf_df.notna().sum(axis=1)
    sd_ni = ni_df.std(axis=1, ddof=1)
    sd_cf = cf_df.std(axis=1, ddof=1)
    valid = (ni_count >= 6) & (cf_count >= 6) & (sd_ni > 0)
    smoothing = (sd_cf / sd_ni).where(valid)
    score = np.log(smoothing.where(smoothing > 0))
    score = score.reindex(panel.index)
    return score
earnings_smoothing_vs_cashflow_volatilityqualityLeuz-Nanda-Wysocki style earnings-smoothing proxy: ratio of trailing 24-month rolling standard deviation of…

Leuz-Nanda-Wysocki style earnings-smoothing proxy: ratio of trailing 24-month rolling standard deviation of operating-cash-flow-to-assets to the rolling standard deviation of net-income-to-assets. High values indicate heavily smoothed earnings relative to underlying cash flow volatility, a marker of low earnings quality that predicts underperformance.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    assets = panel['assets']
    netinc = panel['netinc']
    ncfo = panel['ncfo']
    valid_assets = assets > 0
    ni_a = (netinc / assets).where(valid_assets)
    cfo_a = (ncfo / assets).where(valid_assets)
    df = pd.DataFrame({'ni_a': ni_a, 'cfo_a': cfo_a}, index=panel.index)
    # Ensure strictly backward-looking rolling computation: sort by date
    # within each ticker before applying rolling window operations.
    df = df.sort_index(level=['ticker', 'date'])
    def _roll_std(x: pd.Series) -> pd.Series:
        return x.rolling(window=24, min_periods=12).std()
    s_ni = df.groupby(level='ticker')['ni_a'].apply(_roll_std)
    s_cfo = df.groupby(level='ticker')['cfo_a'].apply(_roll_std)
    # groupby.apply with a Series-returning function may produce an extra
    # level in the index; align back to df's index safely.
    s_ni.index = df.index
    s_cfo.index = df.index
    signal = (s_cfo / s_ni).where(s_ni > 0)
    # Reindex to the original panel order.
    signal = signal.reindex(panel.index)
    return signal
earnings_smoothness_vs_cashflowqualityRatio of volatility of scaled net income to volatility of scaled operating cash flow, following…

Ratio of volatility of scaled net income to volatility of scaled operating cash flow, following Leuz-Nanda-Wysocki earnings smoothness proxy. High ratio (unmanaged earnings) predicts higher subsequent returns.

Category qualityDirection Long the high scoresData fundamental
def compute(self, panel) -> pd.Series:
    scale = panel['assets'].where(panel['assets'] > 0)
    e = panel['netinc'] / scale
    c = panel['ncfo'] / scale
    g_e = e.groupby(level='ticker')
    g_c = c.groupby(level='ticker')
    lags = [0, 3, 6, 9, 12, 15, 18, 21]
    E = pd.concat([g_e.shift(k) for k in lags], axis=1)
    C = pd.concat([g_c.shift(k) for k in lags], axis=1)
    sd_e = E.std(axis=1, ddof=1, skipna=True).where(E.count(axis=1) >= 6)
    sd_c = C.std(axis=1, ddof=1, skipna=True).where(C.count(axis=1) >= 6)
    ratio = sd_e / sd_c.where(sd_c > 0)
    ratio = ratio.where(sd_e.notna() & sd_c.notna() & (sd_c > 0))
    return ratio.reindex(panel.index)
effective_tax_rate_earnings_qualityqualityEffective tax rate (taxexp/ebt) as earnings-quality diagnostic: firms with high, rising cash-taxed earnings…

Effective tax rate (taxexp/ebt) as earnings-quality diagnostic: firms with high, rising cash-taxed earnings (high ETR level and increasing ETR) are flagged as higher quality and expected to outperform, while low/falling ETR firms (book-tax gap) are flagged as low quality earnings likely to mean-revert.

Category qualityDirection Long the high scoresData fundamental
def compute(self, panel) -> pd.Series:
    taxexp = panel['taxexp']
    ebt = panel['ebt']
    netinc = panel['netinc']
    assets = panel['assets']
    valid = (ebt > 0) & (netinc > 0) & (assets > 0)
    etr = taxexp / ebt.where(ebt > 0)
    etr = etr.where(valid)
    etr = etr.clip(lower=-0.2, upper=0.6)
    etr_lag = etr.groupby(level='ticker').shift(12)
    change = etr - etr_lag
    change = change.where(~(etr_lag.isna() & etr.notna()), 0.0)
    score = 0.6 * etr + 0.4 * change
    score = score.where(valid)
    return score.reindex(panel.index)
excess_speculative_volatilityqualityCompares price return volatility to earnings (ROA) volatility

Compares price return volatility to earnings (ROA) volatility. Stocks whose return volatility greatly exceeds their fundamental (earnings) volatility are driven by speculative/noise trading rather than fundamentals and tend to underperform. Score = z_cs(log(vol_252)) - z_cs(log(earn_vol + 1e-4)).

Category qualityDirection Long the low scoresData price, fundamental
def compute(self, panel) -> pd.Series:
    assets = panel['assets']
    netinc = panel['netinc']
    roa_q = pd.Series(np.nan, index=panel.index)
    valid_assets = assets > 0
    roa_q[valid_assets] = netinc[valid_assets] / assets[valid_assets]
    lags = [0, 3, 6, 9, 12, 15, 18, 21]
    shifted_list = []
    grouped = roa_q.groupby(level='ticker')
    for k in lags:
        shifted_list.append(grouped.shift(k))
    roa_df = pd.concat(shifted_list, axis=1)
    count_valid = roa_df.notna().sum(axis=1)
    earn_vol = roa_df.std(axis=1, ddof=1)
    earn_vol = earn_vol.where(count_valid >= 6, np.nan)
    vol_252 = panel['vol_252']
    log_vol = np.log(vol_252.where(vol_252 > 0))
    log_earn_vol = np.log(earn_vol + 1e-4)
    df = pd.DataFrame({'log_vol': log_vol, 'log_earn_vol': log_earn_vol})
    def zscore(s):
        mean = s.mean()
        std = s.std(ddof=0)
        if std is None or std == 0 or pd.isna(std):
            return pd.Series(np.nan, index=s.index)
        return (s - mean) / std
    z_vol = df['log_vol'].groupby(level='date').transform(zscore)
    z_earn_vol = df['log_earn_vol'].groupby(level='date').transform(zscore)
    score = z_vol - z_earn_vol
    score = score.where(log_vol.notna() & log_earn_vol.notna(), np.nan)
    return score
financial_slack_under_tighteningqualityBalance-sheet slack (cash + 4*OCF - current debt, scaled by assets) cross-sectionally ranked and conditioned…

Balance-sheet slack (cash + 4*OCF - current debt, scaled by assets) cross-sectionally ranked and conditioned on a past-only expanding z-score of the 12m change in fed funds rate and Baa spread, so the slack premium is amplified in monetary/credit tightening regimes and dampened/reversed in easing regimes.

Category qualityDirection Long the high scoresData fundamental, macro
def compute(self, panel) -> pd.Series:
    cashneq = panel['cashneq']
    ncfo = panel['ncfo']
    debtc = panel['debtc']
    assets = panel['assets']
    valid = (
        assets.notna() & (assets > 0)
        & cashneq.notna() & ncfo.notna() & debtc.notna()
    )
    slack = (cashneq + 4.0 * ncfo - debtc) / assets
    slack = slack.where(valid)
    def rank_z(s: pd.Series) -> pd.Series:
        r = s.rank(pct=True)
        mean = r.mean()
        std = r.std()
        if std is None or std == 0 or pd.isna(std):
            return pd.Series(np.nan, index=s.index)
        return (r - mean) / std
    slack_z = slack.groupby(level='date').transform(rank_z)
    # date-level macro series
    ff_chg = panel['macro_fedfunds_chg12m'].groupby(level='date').first().fillna(0.0)
    baa_chg = panel['macro_baa_spread_chg12m'].groupby(level='date').first().fillna(0.0)
    def expanding_z(s: pd.Series) -> pd.Series:
        s = s.sort_index()
        mean = s.expanding().mean().shift(1)
        std = s.expanding().std().shift(1)
        z = (s - mean) / std
        z = z.where(std > 0)
        return z
    ff_z = expanding_z(ff_chg)
    baa_z = expanding_z(baa_chg)
    tight = (0.6 * ff_z + 0.4 * baa_z).clip(-2, 2)
    tight = tight.fillna(0.0)
    tight_full = slack_z.index.get_level_values('date').map(tight)
    tight_full = pd.Series(tight_full.values, index=slack_z.index)
    signal = slack_z * tight_full
    signal = signal.where(slack_z.notna())
    return signal
gross_profitabilityqualityNovy-Marx gross profitability = gross profit / total assets

Novy-Marx gross profitability = gross profit / total assets.

Category qualityDirection Long the high scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    assets = panel["assets"]
    return (panel["gp"] / assets).where(assets > 0)
hidden_credit_risk_mismatchqualityIdentifies highly levered firms whose equity shows little or no measured sensitivity to credit spread changes…

Identifies highly levered firms whose equity shows little or no measured sensitivity to credit spread changes (near-zero/positive corr to credit), combined with weak liquidity, indicating dormant/unpriced default risk that tends to resolve violently during funding shocks and is associated with low future returns (distress-risk anomaly).

Category qualityDirection Long the low scoresData price, fundamental, macro
def compute(self, panel) -> pd.Series:
    debt = panel['debt']
    cashneq = panel['cashneq']
    marketcap = panel['marketcap']
    currentratio = panel['currentratio']
    corr_credit = panel['corr_credit_252']
    valid = (
        debt.notna()
        & cashneq.notna()
        & marketcap.notna()
        & (marketcap > 0)
        & currentratio.notna()
        & corr_credit.notna()
    )
    netdebt_mcap = pd.Series(np.nan, index=panel.index)
    netdebt_mcap.loc[valid] = (debt[valid] - cashneq[valid]) / marketcap[valid]
    currentratio_valid = pd.Series(np.nan, index=panel.index)
    currentratio_valid.loc[valid] = currentratio[valid]
    corr_credit_valid = pd.Series(np.nan, index=panel.index)
    corr_credit_valid.loc[valid] = corr_credit[valid]
    score = pd.Series(np.nan, index=panel.index)
    dates = panel.index.get_level_values('date')
    def process_group(idx):
        g_valid = valid.loc[idx]
        if not g_valid.any():
            return pd.Series(np.nan, index=idx)
        g_netdebt = netdebt_mcap.loc[idx][g_valid]
        g_currentratio = currentratio_valid.loc[idx][g_valid]
        g_corr_credit = corr_credit_valid.loc[idx][g_valid]
        g_netdebt_w = _winsorize(g_netdebt)
        g_currentratio_w = _winsorize(g_currentratio)
        lev_z = _cs_zscore(g_netdebt_w)
        liq_z = -_cs_zscore(g_currentratio_w)
        credit_ins_z = _cs_zscore(g_corr_credit)
        g_score = 0.5 * lev_z + 0.25 * liq_z + 0.5 * credit_ins_z
        result = pd.Series(np.nan, index=idx)
        result.loc[g_score.index] = g_score
        return result
    score = panel.groupby(level='date', group_keys=False).apply(
        lambda g: process_group(g.index)
    )
    score = score.reindex(panel.index)
    return score
hidden_dilution_sbc_yieldqualityScales share-based compensation by market cap to estimate the annual dilution cost investors bear that is…

Scales share-based compensation by market cap to estimate the annual dilution cost investors bear that is hidden in non-GAAP earnings and operating cash flow. High SBC yield, adjusted for a cash-flow quality check, predicts lower future returns.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    ncfo = panel['ncfo']
    assets = panel['assets']
    valid = sbcomp.notna() & marketcap.notna() & (marketcap > 0) & (sbcomp >= 0)
    sbc_yield = (4.0 * sbcomp) / marketcap.where(marketcap > 0)
    cash_check_raw = (4.0 * (ncfo - sbcomp)) / assets.where(assets > 0)
    cash_check = cash_check_raw.where(
        ncfo.notna() & assets.notna() & (assets > 0), 0.0
    )
    cash_check_clipped = cash_check.clip(lower=-0.5, upper=0.5)
    signal = sbc_yield - 0.5 * cash_check_clipped
    signal = signal.where(valid, np.nan)
    return signal
hidden_equity_dilution_costqualitySBC-to-market and SBC-to-operating-cash-flow burden, demeaned within sector, capturing hidden dilution cost…

SBC-to-market and SBC-to-operating-cash-flow burden, demeaned within sector, capturing hidden dilution cost that the market underreacts to.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    sector = panel['sector']
    ncfo = panel['ncfo']
    sbc_ann = sbcomp * 4.0
    sbc_ann = sbc_ann.where(sbcomp >= 0)
    valid_mc = marketcap.where(marketcap > 0)
    burden = sbc_ann / valid_mc
    burden = burden.where(valid_mc.notna())
    ncfo_ann = ncfo * 4.0
    abs_ncfo_ann = ncfo_ann.abs()
    intensity2 = sbc_ann / abs_ncfo_ann
    intensity2 = intensity2.where(abs_ncfo_ann > 0)
    # winsorize intensity2 at cross-sectional 1/99 pct per date
    def winsorize(s):
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    intensity2_w = intensity2.groupby(level='date').transform(
        lambda s: winsorize(s) if s.notna().sum() > 0 else s
    )
    # base drop conditions
    valid_mask = (sbcomp.notna() & (sbcomp >= 0) &
                  marketcap.notna() & (marketcap > 0) &
                  sector.notna())
    burden_valid = burden.where(valid_mask)
    intensity2_valid = intensity2_w.where(valid_mask)
    # exclude stock-dates where both inputs are NaN
    both_nan = burden_valid.isna() & intensity2_valid.isna()
    def cs_rank(s):
        return s.groupby(level='date').rank(pct=True)
    rank_burden = cs_rank(burden_valid)
    rank_intensity2 = cs_rank(intensity2_valid)
    raw = 0.7 * rank_burden.fillna(0) + 0.3 * rank_intensity2.fillna(0)
    weight_sum = 0.7 * rank_burden.notna().astype(float) + 0.3 * rank_intensity2.notna().astype(float)
    raw = raw / weight_sum.replace(0, np.nan)
    raw = raw.where(~both_nan)
    raw = raw.where(valid_mask)
    df = pd.DataFrame({'raw': raw, 'sector': sector})
    median_by_sector = df.groupby([df.index.get_level_values('date'), 'sector'])['raw'].transform('median')
    score = raw - median_by_sector
    score = score.where(valid_mask & ~both_nan)
    return score.reindex(panel.index)
inflation_passthrough_margin_expansionqualityYear-over-year gross margin expansion, weighted more heavily when aggregate inflation is high, capturing…

Year-over-year gross margin expansion, weighted more heavily when aggregate inflation is high, capturing pricing power that lets firms pass through cost shocks. High inflation regimes better reveal true pricing power, so margin gains carry more signal then.

Category qualityDirection Long the high scoresData fundamental, macro
def compute(self, panel) -> pd.Series:
    grossmargin = panel['grossmargin']
    gp = panel['gp']
    revenue = panel['revenue']
    gm_primary_valid = grossmargin.notna() & (grossmargin > -1) & (grossmargin < 2)
    gm_fallback = gp / revenue.where(revenue > 0)
    gm = grossmargin.where(gm_primary_valid, gm_fallback)
    dgm = gm - gm.groupby(level='ticker').shift(12)
    macro_cpi = panel['macro_cpi']
    macro_cpi_chg12m = panel['macro_cpi_chg12m']
    denom = macro_cpi - macro_cpi_chg12m
    infl = macro_cpi_chg12m / denom.where(denom > 0)
    infl = infl.where(denom.notna() & macro_cpi_chg12m.notna())
    # date-level series
    infl_by_date = infl.groupby(level='date').first()
    infl_by_date = infl_by_date.sort_index()
    # expanding past-only percentile rank (using only prior dates, excluding current)
    w_t = pd.Series(index=infl_by_date.index, dtype=float)
    vals = infl_by_date.values
    for i in range(len(vals)):
        if i == 0:
            w_t.iloc[i] = np.nan
            continue
        past = vals[:i]
        past_valid = past[~np.isnan(past)]
        cur = vals[i]
        if np.isnan(cur) or len(past_valid) == 0:
            w_t.iloc[i] = np.nan
        else:
            rank = (past_valid < cur).sum() / len(past_valid)
            w_t.iloc[i] = rank
    w_t_full = infl.index.get_level_values('date').map(w_t)
    w_t_full = pd.Series(w_t_full, index=infl.index)
    score = dgm * (0.25 + w_t_full)
    valid = dgm.notna() & infl.notna() & w_t_full.notna()
    score = score.where(valid)
    return score.reindex(panel.index)
net_trade_credit_expansionqualityChange in net trade credit (receivables minus payables scaled by revenue) over the trailing 12 months

Change in net trade credit (receivables minus payables scaled by revenue) over the trailing 12 months. Rising net trade credit signals loosening credit standards and deteriorating earnings quality, predicting underperformance.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    receivables = panel['receivables']
    payables = panel['payables']
    revenue = panel['revenue']
    valid = receivables.notna() & payables.notna() & revenue.notna() & (revenue > 0)
    ntc = pd.Series(np.nan, index=panel.index)
    ntc[valid] = (receivables[valid] - payables[valid]) / revenue[valid]
    ntc_lag = ntc.groupby(level='ticker').shift(12)
    score = ntc - ntc_lag
    return score
operating_leverage_industrial_cyclequalityInteraction of firm fixed-cost operating leverage (depreciation/revenue and non-current asset intensity) with…

Interaction of firm fixed-cost operating leverage (depreciation/revenue and non-current asset intensity) with the industrial production cycle relative to its long-run trailing average. High operating-leverage firms are predicted to outperform when the industrial cycle is accelerating above its long-run norm, and underperform when decelerating.

Category qualityDirection Long the high scoresData fundamental, macro
def compute(self, panel) -> pd.Series:
    depamor = panel['depamor']
    revenue = panel['revenue']
    assets = panel['assets']
    assetsc = panel['assetsc']
    # component 1: depreciation intensity relative to revenue
    dep_ratio = depamor / revenue.where(revenue > 0)
    dep_ratio = dep_ratio.where(revenue > 0)
    # component 2: non-current asset intensity
    fixed_ratio = (assets - assetsc) / assets.where(assets > 0)
    fixed_ratio = fixed_ratio.where(assets > 0)
    dates = panel.index.get_level_values('date')
    rank1 = dep_ratio.groupby(dates).rank(pct=True)
    rank2 = fixed_ratio.groupby(dates).rank(pct=True)
    oper_lev = 0.5 * rank1 + 0.5 * rank2
    # demean within date to [-0.5, 0.5]
    oper_lev = oper_lev - oper_lev.groupby(dates).transform('mean')
    # macro cycle at date level (past-only expanding mean)
    macro = panel['macro_indpro_chg12m'].groupby(dates).first()
    macro = macro.sort_index()
    macro_mean = macro.expanding(min_periods=12).mean()
    cyc = macro - macro_mean
    cyc = cyc.clip(-4, 4)
    cyc_aligned = dates.map(cyc)
    cyc_series = pd.Series(cyc_aligned, index=panel.index)
    signal = oper_lev * cyc_series
    return signal.reindex(panel.index)
sbc_adjusted_cashflow_yieldqualitySBC-adjusted free cash flow yield: TTM FCF minus TTM |SBC| divided by market cap, capturing the wedge between…

SBC-adjusted free cash flow yield: TTM FCF minus TTM |SBC| divided by market cap, capturing the wedge between headline FCF and shareholder cash flow after charging equity-based compensation as a real expense.

Category qualityDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    fcf = panel['fcf']
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    ttm_fcf = self._ttm(fcf)
    ttm_sbc = self._ttm(sbcomp)
    adj = ttm_fcf - ttm_sbc.abs()
    valid = (
        marketcap.notna() & (marketcap > 0) &
        ttm_fcf.notna() & ttm_sbc.notna()
    )
    signal = pd.Series(np.nan, index=panel.index, dtype=float)
    signal[valid] = adj[valid] / marketcap[valid]
    def winsorize(x: pd.Series) -> pd.Series:
        if x.notna().sum() == 0:
            return x
        lo = x.quantile(0.01)
        hi = x.quantile(0.99)
        return x.clip(lower=lo, upper=hi)
    signal = signal.groupby(level='date').transform(winsorize)
    return signal
sbc_inflated_cash_earningsqualitySBC-intensive firms mechanically inflate cash flow/earnings by treating stock-based compensation as a…

SBC-intensive firms mechanically inflate cash flow/earnings by treating stock-based compensation as a non-cash add-back; high SBC relative to operating cash flow or market cap signals lower earnings quality and future dilution burden, predicting underperformance.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    ncfo = panel['ncfo']
    marketcap = panel['marketcap']
    sbc_ttm = _ttm(sbcomp)
    cfo_ttm = _ttm(ncfo)
    burden1 = pd.Series(np.nan, index=panel.index)
    valid_cfo = cfo_ttm.notna() & (cfo_ttm > 0) & sbc_ttm.notna()
    burden1[valid_cfo] = sbc_ttm[valid_cfo] / cfo_ttm[valid_cfo]
    burden2 = pd.Series(np.nan, index=panel.index)
    valid_mc = marketcap.notna() & (marketcap > 0) & sbc_ttm.notna()
    burden2[valid_mc] = sbc_ttm[valid_mc] / marketcap[valid_mc]
    z1 = _zscore_by_date(burden1)
    z2 = _zscore_by_date(burden2)
    signal = 0.5 * z1 + 0.5 * z2
    return signal
sbc_inflated_operating_cash_flowqualityQuality-of-earnings signal that flags firms whose operating cash flow and free cash flow are heavily inflated…

Quality-of-earnings signal that flags firms whose operating cash flow and free cash flow are heavily inflated by non-cash share-based compensation add-backs relative to true operating cash generation and revenue. High SBC intensity implies overstated cash-flow quality and ongoing dilution; expected to negatively predict future returns.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    ncfo = panel['ncfo']
    revenue = panel['revenue']
    valid = sbcomp.notna() & revenue.notna() & (revenue > 0)
    denom = pd.concat(
        [ncfo.abs(), 0.05 * revenue.abs()], axis=1
    ).max(axis=1)
    intensity = pd.Series(np.nan, index=panel.index)
    mask_intensity = valid & denom.notna() & (denom > 0)
    intensity.loc[mask_intensity] = sbcomp.loc[mask_intensity] / denom.loc[mask_intensity]
    alt = pd.Series(np.nan, index=panel.index)
    alt.loc[valid] = sbcomp.loc[valid] / revenue.loc[valid]
    def rank_pct(s: pd.Series) -> pd.Series:
        return s.groupby(level='date').rank(pct=True, na_option='keep')
    rank_intensity = rank_pct(intensity)
    rank_alt = rank_pct(alt)
    signal = 0.5 * rank_intensity + 0.5 * rank_alt
    signal = signal.where(valid)
    return signal
sticky_cost_downturn_dragqualityCost stickiness signal: when revenue declines, firms that fail to cut operating costs proportionally…

Cost stickiness signal: when revenue declines, firms that fail to cut operating costs proportionally (opex+cor relative to revenue, log-change difference) carry hidden operating leverage and future earnings drag. Defined only for revenue-declining firms; higher stickiness predicts lower returns.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    revenue = panel["revenue"]
    opex = panel["opex"]
    cor = panel["cor"]
    opex_tot = opex.where(cor.isna(), opex + cor)
    grp = opex_tot.groupby(level="ticker")
    rev_grp = revenue.groupby(level="ticker")
    rev_lag = rev_grp.shift(12)
    opex_lag = grp.shift(12)
    rev_valid = (revenue > 0) & (rev_lag > 0)
    opex_valid = (opex_tot > 0) & (opex_lag > 0)
    drev = pd.Series(np.nan, index=panel.index)
    drev[rev_valid] = np.log(revenue[rev_valid] / rev_lag[rev_valid])
    dcost = pd.Series(np.nan, index=panel.index)
    dcost[opex_valid] = np.log(opex_tot[opex_valid] / opex_lag[opex_valid])
    stickiness = dcost - drev
    decline_mask = drev < 0
    stickiness = stickiness.where(decline_mask & drev.notna() & dcost.notna())
    return stickiness
stock_comp_burden_yieldqualityTrailing-twelve-month share-based compensation scaled by market cap, demeaned within (date, sector), as a…

Trailing-twelve-month share-based compensation scaled by market cap, demeaned within (date, sector), as a proxy for hidden non-cash compensation burden that overstates reported cash flow quality.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    sector = panel['sector']
    g = sbcomp.groupby(level='ticker')
    lag0 = g.shift(0)
    lag3 = g.shift(3)
    lag6 = g.shift(6)
    lag9 = g.shift(9)
    valid_lags = lag0.notna() & lag3.notna() & lag6.notna() & lag9.notna()
    ttm_sbc = lag0 + lag3 + lag6 + lag9
    valid_mcap = marketcap.notna() & (marketcap > 0)
    valid = valid_lags & valid_mcap & (ttm_sbc >= 0)
    burden = pd.Series(np.nan, index=panel.index, dtype=float)
    burden.loc[valid] = ttm_sbc.loc[valid] / marketcap.loc[valid]
    dates = panel.index.get_level_values('date')
    def cap_99(s):
        valid_vals = s.dropna()
        if valid_vals.empty:
            return s
        cap = valid_vals.quantile(0.99)
        return s.clip(upper=cap)
    burden = burden.groupby(dates).transform(cap_99)
    min_group_size = 3
    group_keys = [dates, sector]
    grp = burden.groupby(group_keys)
    sector_mean = grp.transform('mean')
    sector_count = burden.notna().groupby(group_keys).transform('sum')
    signal = burden - sector_mean
    signal = signal.where(burden.notna() & (sector_count >= min_group_size), np.nan)
    signal.name = "stock_comp_burden_yield"
    return signal
stock_comp_dilution_earnings_overstatementqualityFlags firms whose profitability/cash flow appear inflated by non-cash stock-based compensation (SBC intensity…

Flags firms whose profitability/cash flow appear inflated by non-cash stock-based compensation (SBC intensity relative to gross profit/revenue, SBC relative to operating cash flow) and whose basic-vs-diluted EPS wedge reveals equity overhang. High scores indicate hidden dilution/earnings overstatement and are expected to underperform (expected_sign=-1).

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    gp = panel['gp']
    revenue = panel['revenue']
    ncfo = panel['ncfo']
    eps = panel['eps']
    epsdil = panel['epsdil']
    # sbc_int: sbcomp / gp where gp > 0 and sbcomp >= 0, fallback sbcomp / revenue where revenue > 0
    sbc_int = pd.Series(np.nan, index=panel.index)
    valid_gp = (gp > 0) & (sbcomp >= 0)
    sbc_int.loc[valid_gp] = sbcomp.loc[valid_gp] / gp.loc[valid_gp]
    valid_rev_fallback = (~valid_gp) & (revenue > 0) & (sbcomp >= 0)
    sbc_int.loc[valid_rev_fallback] = sbcomp.loc[valid_rev_fallback] / revenue.loc[valid_rev_fallback]
    # cf_inflation: sbcomp / ncfo where ncfo > 0
    cf_inflation = pd.Series(np.nan, index=panel.index)
    valid_ncfo = ncfo > 0
    cf_inflation.loc[valid_ncfo] = sbcomp.loc[valid_ncfo] / ncfo.loc[valid_ncfo]
    # wedge: (eps - epsdil) / abs(eps) where abs(eps) > 0.01
    wedge = pd.Series(np.nan, index=panel.index)
    valid_eps = eps.abs() > 0.01
    wedge.loc[valid_eps] = (eps.loc[valid_eps] - epsdil.loc[valid_eps]) / eps.loc[valid_eps].abs()
    df = pd.DataFrame({
        'sbc_int': sbc_int,
        'cf_inflation': cf_inflation,
        'wedge': wedge,
    }, index=panel.index)
    dates = panel.index.get_level_values('date')
    ranks = df.groupby(dates).rank(pct=True)
    score = ranks.mean(axis=1, skipna=True)
    all_nan_mask = df.isna().all(axis=1)
    score = score.mask(all_nan_mask, np.nan)
    return score
stock_comp_earnings_dilutionqualityScales share-based compensation by revenue and by absolute operating cash flow to capture stock-comp-driven…

Scales share-based compensation by revenue and by absolute operating cash flow to capture stock-comp-driven earnings/cash-flow dilution. High values indicate heavy reliance on SBC that inflates cash flow and adjusted earnings while diluting shareholders, a lower quality-of-earnings signal expected to underperform.

Category qualityDirection Long the low scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    sbcomp = panel['sbcomp']
    revenue = panel['revenue']
    ncfo = panel['ncfo']
    revenue_valid = revenue.where(revenue > 0)
    sbc_rev = sbcomp / revenue_valid
    sbc_rev = sbc_rev.where(revenue_valid.notna() & sbcomp.notna())
    ncfo_abs = ncfo.abs()
    ncfo_valid = ncfo_abs.where(ncfo_abs > 0)
    sbc_ocf = sbcomp / ncfo_valid
    sbc_ocf = sbc_ocf.where(ncfo_valid.notna() & sbcomp.notna())
    r1 = sbc_rev.groupby(level='date').rank(pct=True)
    r2 = sbc_ocf.groupby(level='date').rank(pct=True)
    signal = (r1 + r2) / 2
    signal = signal.where(~(r1.isna() & r2.isna()))
    return signal
trade_credit_stretch_qualityqualityCombines 12-month changes in days payable outstanding (payables/cor) and days sales outstanding…

Combines 12-month changes in days payable outstanding (payables/cor) and days sales outstanding (receivables/revenue) plus an accrual-quality tilt ((netinc-ncfo)/assets) to detect firms stretching trade credit terms to flatter earnings; expected_sign=-1 since higher stretch predicts lower returns.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    payables = panel['payables']
    cor = panel['cor']
    receivables = panel['receivables']
    revenue = panel['revenue']
    assets = panel['assets']
    ncfo = panel['ncfo']
    netinc = panel['netinc']
    dpo = pd.Series(np.nan, index=panel.index)
    mask_cor = cor > 0
    dpo[mask_cor] = payables[mask_cor] / cor[mask_cor]
    dso = pd.Series(np.nan, index=panel.index)
    mask_rev = revenue > 0
    dso[mask_rev] = receivables[mask_rev] / revenue[mask_rev]
    dpo_lag = dpo.groupby(level='ticker').shift(12)
    dso_lag = dso.groupby(level='ticker').shift(12)
    d_dpo = dpo - dpo_lag
    d_dso = dso - dso_lag
    d_dpo = d_dpo.where(dpo.notna() & dpo_lag.notna())
    d_dso = d_dso.where(dso.notna() & dso_lag.notna())
    accrual = pd.Series(np.nan, index=panel.index)
    mask_assets = assets > 0
    accrual[mask_assets] = (netinc[mask_assets] - ncfo[mask_assets]) / assets[mask_assets]
    def cross_sectional_rank(s):
        return s.groupby(level='date').rank(pct=True)
    rank_dpo = cross_sectional_rank(d_dpo)
    rank_dso = cross_sectional_rank(d_dso)
    rank_accrual = cross_sectional_rank(accrual)
    def neutral_fill(rank_series):
        median_rank = rank_series.groupby(level='date').transform('median')
        return rank_series.fillna(median_rank)
    rank_dpo_filled = neutral_fill(rank_dpo)
    rank_dso_filled = neutral_fill(rank_dso)
    rank_accrual_filled = neutral_fill(rank_accrual)
    signal = rank_dpo_filled + rank_dso_filled + rank_accrual_filled
    return signal
transitory_nonoperating_income_intensityqualityAbsolute magnitude of the gap between pre-tax income (ebt) and operating income (opinc), scaled by total…

Absolute magnitude of the gap between pre-tax income (ebt) and operating income (opinc), scaled by total assets, as a proxy for reliance on transitory non-operating items. Higher magnitude implies lower earnings persistence and predicts lower subsequent returns.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    ebt = panel['ebt']
    opinc = panel['opinc']
    assets = panel['assets']
    revenue = panel['revenue']
    denom = assets.where(assets > 0)
    nonop = ebt - opinc
    raw = nonop.abs() / denom
    valid = (
        ebt.notna()
        & opinc.notna()
        & assets.notna()
        & revenue.notna()
        & (assets > 0)
        & (revenue.abs() > 0)
    )
    raw = raw.where(valid)
    def _winsorize(s: pd.Series) -> pd.Series:
        if s.dropna().empty:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    result = raw.groupby(level='date').transform(_winsorize)
    return result
working_capital_release_under_tighteningqualityInteracts a firm-level working-capital-efficiency improvement (declining net operating working capital scaled…

Interacts a firm-level working-capital-efficiency improvement (declining net operating working capital scaled by assets, demeaned cross-sectionally) with a macro tightening intensity gauge (rising 2y yield, fed funds, and credit spreads), scaled by revenue growth. Captures funding-relief quality of earnings that binds only in tightening regimes.

Category qualityDirection Long the high scoresData fundamental, macro
def compute(self, panel) -> pd.Series:
    receivables = panel['receivables']
    inventory = panel['inventory']
    payables = panel['payables']
    assets = panel['assets']
    revenue = panel['revenue']
    assets_safe = assets.where(assets > 0)
    nowc = (receivables + inventory - payables) / assets_safe
    nowc_lag12 = nowc.groupby(level='ticker').shift(12)
    d_nowc = nowc - nowc_lag12
    revenue_lag12 = revenue.groupby(level='ticker').shift(12)
    rev_g = (revenue / revenue_lag12 - 1).where(revenue_lag12 > 0)
    rev_g_clipped = rev_g.clip(-0.5, 0.5)
    # cross-sectional median of d_nowc per date, computed only from non-NaN obs
    median_d_nowc = d_nowc.groupby(level='date').transform(
        lambda x: x.median(skipna=True)
    )
    wc_release = -(d_nowc - median_d_nowc)
    # Macro tightening intensity: date-level series
    dates = panel.index.get_level_values('date')
    macro_dgs2 = panel['macro_dgs2_chg12m'].groupby(level='date').first()
    macro_fedfunds = panel['macro_fedfunds_chg12m'].groupby(level='date').first()
    macro_baa = panel['macro_baa_spread_chg12m'].groupby(level='date').first()
    z_dgs2 = self._pastonly_zscore(macro_dgs2)
    z_fedfunds = self._pastonly_zscore(macro_fedfunds)
    z_baa = self._pastonly_zscore(macro_baa)
    tighten = (z_dgs2 + z_fedfunds + z_baa).reindex(dates).values
    tighten_series = pd.Series(tighten, index=panel.index)
    score = wc_release * tighten_series * (1 + rev_g_clipped)
    # NaN propagation for invalid inputs
    invalid_mask = (
        receivables.isna() | inventory.isna() | payables.isna() |
        assets.isna() | (assets <= 0) |
        revenue.isna() | nowc_lag12.isna() | revenue_lag12.isna() |
        (revenue_lag12 <= 0) | tighten_series.isna()
    )
    score = score.where(~invalid_mask)
    return score
zero_earnings_threshold_managementqualityDetects firms with small positive ROA suspiciously close to the zero-earnings threshold (Burgstahler-Dichev…

Detects firms with small positive ROA suspiciously close to the zero-earnings threshold (Burgstahler-Dichev discontinuity), suggesting earnings management to avoid reporting losses. Signal is a Gaussian kernel bump centered at zero-plus, amplified when net income exceeds operating cash flow (weak cash backing). Higher score implies higher suspicion of manipulation and is expected to predict lower future returns.

Category qualityDirection Long the low scoresData fundamental
def compute(self, panel) -> pd.Series:
    assets = panel['assets']
    netinc = panel['netinc']
    ncfo = panel['ncfo']
    revenue = panel['revenue']
    valid_assets = assets.where(assets > 0)
    x = netinc / valid_assets
    bump = np.exp(-0.5 * (x / 0.004) ** 2) * (x > 0)
    cash_gap = ((netinc - ncfo) / valid_assets).clip(-0.5, 0.5)
    cash_gap_rank = cash_gap.groupby(level='date').rank(pct=True)
    signal = bump * (1 + cash_gap_rank)
    invalid = (
        assets.isna() | (assets <= 0)
        | revenue.isna() | (revenue <= 0)
        | netinc.isna()
        | ncfo.isna()
        | x.isna()
    )
    signal = signal.where(~invalid, np.nan)
    return signal
attention_shock_at_the_highssizeInteraction of abnormal dollar-volume (log dvol_21 vs trailing 12m average) with proximity to 52-week high;…

Interaction of abnormal dollar-volume (log dvol_21 vs trailing 12m average) with proximity to 52-week high; captures attention-driven speculative buying pressure at the highs that tends to mean-revert.

Category sizeDirection Long the low scoresData price
def compute(self, panel) -> pd.Series:
    dvol = panel['dvol_21']
    high_ratio = panel['high_ratio']
    valid_dvol = dvol > 0
    lv = np.log(dvol.where(valid_dvol))
    grp = lv.groupby(level='ticker')
    lags = [grp.shift(k) for k in range(1, 13)]
    lag_df = pd.concat(lags, axis=1)
    valid_count = lag_df.notna().sum(axis=1)
    baseline = lag_df.mean(axis=1)
    abn = lv - baseline
    abn = abn.where(valid_count >= 9)
    # mask invalid rows
    mask = valid_dvol & high_ratio.notna() & abn.notna()
    abn = abn.where(mask)
    hr = high_ratio.where(mask)
    def pct_rank(s):
        return s.groupby(level='date').rank(pct=True)
    a = pct_rank(abn)
    h = pct_rank(hr)
    signal = a * h
    signal = signal.where(mask)
    return signal.reindex(panel.index)
conditional_size_credit_cyclesizeSize tilt (small minus large) conditioned on the credit cycle: the tilt is amplified when the Baa credit…

Size tilt (small minus large) conditioned on the credit cycle: the tilt is amplified when the Baa credit spread is trending down (easing) and flipped when it is trending up (tightening), based on the 12-month change in the Baa spread with an expanding, past-only z-score.

Category sizeDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    marketcap = panel['marketcap']
    baa_chg = panel['macro_baa_spread_chg12m']
    # Per-date macro series (broadcast across tickers, same value per date)
    macro_by_date = baa_chg.groupby(level='date').first()
    macro_by_date = macro_by_date.sort_index()
    # Expanding past-only mean/std (min 36 obs), shifted by 1 to avoid look-ahead
    shifted = macro_by_date.shift(1)
    exp_mean = shifted.expanding(min_periods=36).mean()
    exp_std = shifted.expanding(min_periods=36).std()
    zx = (macro_by_date - exp_mean) / exp_std
    zx = zx.where(exp_std > 0, np.nan)
    zx_clipped = zx.clip(-2, 2)
    easing = -zx_clipped
    # Only valid where macro_baa_spread_chg12m itself is present
    easing = easing.where(macro_by_date.notna(), np.nan)
    # Broadcast easing to panel index
    easing_panel = panel.index.get_level_values('date').map(easing)
    easing_panel = pd.Series(easing_panel, index=panel.index)
    # Size tilt: -log(marketcap), demeaned per date, marketcap>0 only
    valid_mc = marketcap.where(marketcap > 0, np.nan)
    log_size = -np.log(valid_mc)
    date_mean = log_size.groupby(level='date').transform('mean')
    size_tilt = log_size - date_mean
    score = size_tilt * easing_panel
    # Enforce missing-data policy
    score = score.where(marketcap > 0, np.nan)
    score = score.where(easing_panel.notna(), np.nan)
    return score
size_premium_credit_conditions_switchsizeSize premium (-log(marketcap)) whose sign and magnitude are conditioned on the 12m change in the Baa-Treasury…

Size premium (-log(marketcap)) whose sign and magnitude are conditioned on the 12m change in the Baa-Treasury credit spread: when spreads have tightened over the trailing 12 months (credit conditions improving), the small-cap premium is enhanced; when spreads have widened (credit conditions worsening), the size tilt is reversed. cond = tanh(-macro_baa_spread_chg12m / 0.5).

Category sizeDirection Long the high scoresData price, macro
def compute(self, panel) -> pd.Series:
    marketcap = panel['marketcap']
    baa_chg = panel['macro_baa_spread_chg12m']
    dvol = panel.get('dvol_21')
    valid = marketcap.notna() & (marketcap > 0) & baa_chg.notna()
    if dvol is not None:
        valid = valid & dvol.notna()
    log_mcap = np.log(marketcap.where(valid))
    size_score = -log_mcap
    # Winsorize size_score cross-sectionally (by date) at 1%/99%
    def _winsorize(s: pd.Series) -> pd.Series:
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    size_score_w = size_score.groupby(level='date').transform(_winsorize)
    cond = np.tanh(-baa_chg / 0.5)
    signal = cond * size_score_w
    signal = signal.where(valid)
    return signal.reindex(panel.index)
turnover_shock_price_run_reversalsizeAbnormal turnover (dollar volume scaled by market cap) relative to a stock’s own trailing 12-month history,…

Abnormal turnover (dollar volume scaled by market cap) relative to a stock’s own trailing 12-month history, interacted with 1-month return, proxies for attention-driven overshoot that subsequently reverses.

Category sizeDirection Long the low scoresData price
def compute(self, panel) -> pd.Series:
    marketcap = panel['marketcap']
    dvol_21 = panel['dvol_21']
    ret_1m = panel['ret_1m']
    valid_denom = (marketcap > 0) & (dvol_21 > 0)
    turn = pd.Series(np.nan, index=panel.index, dtype=float)
    turn.loc[valid_denom] = dvol_21.loc[valid_denom] / marketcap.loc[valid_denom]
    lturn = np.log(turn)
    grouped = lturn.groupby(level='ticker')
    base = grouped.shift(1).groupby(level='ticker').rolling(12, min_periods=6).mean()
    base = base.reset_index(level=0, drop=True)
    base = base.reindex(panel.index)
    abn_turn = lturn - base
    raw = abn_turn * ret_1m
    raw = raw.where(abn_turn.notna() & ret_1m.notna() & base.notna())
    return raw
book_to_marketvalueFama-French HML: book-to-market = 1/pb (high = cheap = long)

Fama-French HML: book-to-market = 1/pb (high = cheap = long).

Category valueDirection Long the high scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    pb = panel["pb"]
    return (1.0 / pb).where(pb > 0)
cash_vs_accounting_value_divergencevalueSpread between cross-sectional rank of cash-flow yield (FCF/marketcap) and accounting-earnings yield…

Spread between cross-sectional rank of cash-flow yield (FCF/marketcap) and accounting-earnings yield (netinc/marketcap), industry-neutralized within date (fallback to full cross-section median if industry missing). Captures mispricing where GAAP earnings are depressed by non-cash charges relative to cash generation.

Category valueDirection Long the high scoresData price, fundamental
def compute(self, panel) -> pd.Series:
    fcf = panel["fcf"]
    netinc = panel["netinc"]
    marketcap = panel["marketcap"]
    industry = panel["industry"]
    valid = (
        marketcap.notna()
        & (marketcap > 0)
        & fcf.notna()
        & netinc.notna()
    )
    cash_yield = pd.Series(np.nan, index=panel.index, dtype=float)
    acct_yield = pd.Series(np.nan, index=panel.index, dtype=float)
    cash_yield.loc[valid] = fcf.loc[valid] / marketcap.loc[valid]
    acct_yield.loc[valid] = netinc.loc[valid] / marketcap.loc[valid]
    date_level = panel.index.get_level_values("date")
    r1 = cash_yield.groupby(date_level).rank(pct=True)
    r2 = acct_yield.groupby(date_level).rank(pct=True)
    div = r1 - r2
    div = div.where(valid)
    # Industry-date median (fallback to date-level median if industry missing)
    has_industry = industry.notna()
    industry_group_key = pd.Series(
        np.where(has_industry, industry.astype(str), "__NA__"),
        index=panel.index,
    )
    combined_key = pd.MultiIndex.from_arrays(
        [date_level, industry_group_key.values], names=["__date__", "__ind__"]
    )
    industry_median = div.groupby(combined_key).transform("median")
    date_median = div.groupby(date_level).transform("median")
    baseline = pd.Series(
        np.where(has_industry.values, industry_median.values, date_median.values),
        index=panel.index,
    )
    score = div - baseline
    score = score.where(valid)
    return score
earnings_yieldvalueEarnings yield = 1/pe (high = cheap = long)

Earnings yield = 1/pe (high = cheap = long).

Category valueDirection Long the high scores
def compute(self, panel: pd.DataFrame) -> pd.Series:
    pe = panel["pe"]
    return (1.0 / pe).where(pe > 0)
equity_duration_rate_trend_interactionvalueFundamental equity-duration proxy (high PB, low dividend yield, low FCF yield) interacted with the trailing…

Fundamental equity-duration proxy (high PB, low dividend yield, low FCF yield) interacted with the trailing 12m change in the 10y Treasury yield. Long-duration growth equity is favored when yields have been falling and disfavored when yields have been rising, conditioning a value/duration tilt on the macro rate trend.

Category valueDirection Long the high scoresData fundamental, price, macro
def compute(self, panel) -> pd.Series:
    pb = panel['pb']
    divyield = panel['divyield']
    fcf = panel['fcf']
    marketcap = panel['marketcap']
    macro_chg = panel['macro_dgs10_chg12m']
    valid_mc = marketcap > 0
    cfy = pd.Series(np.nan, index=panel.index, dtype=float)
    cfy[valid_mc] = fcf[valid_mc] / marketcap[valid_mc]
    valid = (
        valid_mc
        & pb.notna()
        & divyield.notna()
        & fcf.notna()
        & cfy.notna()
        & macro_chg.notna()
    )
    pb_v = pb.where(valid)
    divyield_v = divyield.where(valid)
    cfy_v = cfy.where(valid)
    rank_pb = pb_v.groupby(level='date').rank(pct=True)
    rank_neg_dy = (-divyield_v).groupby(level='date').rank(pct=True)
    rank_neg_cfy = (-cfy_v).groupby(level='date').rank(pct=True)
    dur = 0.4 * rank_pb + 0.3 * rank_neg_dy + 0.3 * rank_neg_cfy
    dur_mean = dur.groupby(level='date').transform('mean')
    dur_c = dur - dur_mean
    signal = -dur_c * macro_chg
    signal = signal.where(valid)
    return signal
retained_vs_contributed_capital_valuevalueValue signal splitting book equity into retained earnings (internally generated) vs contributed capital…

Value signal splitting book equity into retained earnings (internally generated) vs contributed capital (externally raised): (2*retearn - equity) / marketcap, winsorized at 1/99 pct cross-sectionally per date. Higher score (more retained-earnings-driven equity relative to market cap) predicts higher returns.

Category valueDirection Long the high scoresData fundamental, price
def compute(self, panel) -> pd.Series:
    equity = panel['equity']
    retearn = panel['retearn']
    marketcap = panel['marketcap']
    valid = (marketcap > 0) & equity.notna() & retearn.notna()
    raw = (2 * retearn - equity) / marketcap
    raw = raw.where(valid, np.nan)
    def _winsorize(s: pd.Series) -> pd.Series:
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    result = raw.groupby(level='date').transform(_winsorize)
    return result

Market-timing signals (49)

Scored once per month for the market as a whole. The oriented score sets the desired equity exposure rather than picking names.

aggregate_equity_issuance_market_timinginvestmentAggregate equity issuance market timing signal (Baker-Wurgler style): cross-sectional sum of positive net…

Aggregate equity issuance market timing signal (Baker-Wurgler style): cross-sectional sum of positive net equity issuance (ncfcommon) scaled by aggregate market cap, smoothed with a 12-month rolling (past-only) mean. High aggregate issuance signals overvaluation and predicts low subsequent market returns, so exposure should be reduced (expected_sign=-1).

Category investmentDirection High score → risk-off (cut exposure)Data fundamental, price
def compute(self, panel) -> pd.Series:
    mcap = panel['marketcap']
    ncf = panel['ncfcommon']
    valid = mcap.notna() & (mcap > 0)
    ncf_pos = ncf.where(ncf.notna() & (ncf > 0), 0.0)
    ncf_pos = ncf_pos.where(valid)
    mcap_valid = mcap.where(valid)
    num = ncf_pos.groupby(level='date').sum(min_count=1)
    den = mcap_valid.groupby(level='date').sum(min_count=1)
    count = valid.groupby(level='date').sum()
    iss = num / den.where(den > 0)
    iss = iss.where(count >= 10)
    iss = iss.sort_index()
    smooth = iss.rolling(window=12, min_periods=6).mean()
    return smooth
aggregate_inventory_overhang_timinginvestmentCross-sectional median inventory/revenue ratio aggregated over time, normalized by its own trailing 36-month…

Cross-sectional median inventory/revenue ratio aggregated over time, normalized by its own trailing 36-month mean/std to form an inventory overhang z-score, amplified when trailing 12m industrial production growth is negative (slowing IP cycle). High overhang during slowdowns signals margin compression and weaker forward equity returns.

Category investmentDirection High score → risk-off (cut exposure)Data fundamental, macro
from __future__ import annotations
import pandas as pd
import numpy as np
from llm_alpha.signals.base import BaseSignal
class AggregateInventoryOverhangTiming(BaseSignal):
    name = "aggregate_inventory_overhang_timing"
    category = "other"
    strategy_type = "macro_timing"
    expected_sign = -1
    description = (
        "Market-timing signal based on the cross-sectional median change in "
        "inventory-to-sales ratio (12m delta), expanding past-only z-scored, "
        "adjusted for the trend in industrial production growth. Rising "
        "inventory overhang combined with decelerating industrial production "
        "signals a destocking phase historically associated with poor "
        "equity returns."
    )
    data_sources = ('fundamental', 'macro')
    def compute(self, panel) -> pd.Series:
        df = panel[['inventory', 'revenue']].copy()
        valid = (df['inventory'] > 0) & (df['revenue'] > 0)
        x = pd.Series(np.nan, index=df.index)
        x[valid] = df.loc[valid, 'inventory'] / df.loc[valid, 'revenue']
        x_shift = x.groupby(level='ticker').shift(12)
        dx = x - x_shift
        valid_dx = dx.dropna()
        counts = valid_dx.groupby(level='date').size()
        agg = valid_dx.groupby(level='date').median()
        agg = agg.where(counts >= 100)
        agg = agg.sort_index()
        # expanding past-only z-score of agg
        agg_mean = agg.expanding().mean().shift(1)
        agg_std = agg.expanding().std().shift(1)
        agg_z = (agg - agg_mean) / agg_std
        agg_z = agg_z.where(agg_std > 0)
        # macro indpro chg12m at date level (broadcast per date)
        macro_indpro = panel['macro_indpro_chg12m'].groupby(level='date').mean()
        macro_indpro = macro_indpro.sort_index()
        macro_mean = macro_indpro.expanding().mean().shift(1)
        macro_std = macro_indpro.expanding().std().shift(1)
        macro_z = (macro_indpro - macro_mean) / macro_std
        macro_z = macro_z.where(macro_std > 0)
        # align indices
        all_dates = agg_z.index.union(macro_z.index)
        agg_z = agg_z.reindex(all_dates)
        macro_z = macro_z.reindex(all_dates)
        state = agg_z - 0.5 * macro_z
        return state
aggregate_inventory_sales_cycle_timinginvestmentAggregate (bottom-up, ex-Financials) inventory/sales ratio built from the equity panel’s fundamentals

Aggregate (bottom-up, ex-Financials) inventory/sales ratio built from the equity panel’s fundamentals. Rising aggregate inventory intensity (log ratio minus its 12-month lag) signals involuntary inventory buildup, which precedes production cuts and weak market returns, hence a negative expected sign for market timing.

Category investmentDirection High score → risk-off (cut exposure)Data fundamental
def compute(self, panel) -> pd.Series:
    df = panel[['inventory', 'revenue', 'sector']].copy()
    mask = (
        df['inventory'].notna()
        & df['revenue'].notna()
        & (df['inventory'] > 0)
        & (df['revenue'] > 0)
        & (df['sector'] != 'Financials')
    )
    df = df.loc[mask]
    grouped = df.groupby(level='date').agg(
        inv_sum=('inventory', 'sum'),
        rev_sum=('revenue', 'sum'),
    )
    ratio = grouped['inv_sum'] / grouped['rev_sum']
    ratio = ratio.where(grouped['rev_sum'] > 0)
    s = np.log(ratio)
    s = s.sort_index()
    raw = s - s.shift(12)
    return raw
credit_spread_timinglow_riskTime the market on credit conditions: lean in when the Baa-Treasury spread is tight, out when it is wide

Time the market on credit conditions: lean in when the Baa-Treasury spread is tight, out when it is wide.

Category low_riskDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel: pd.DataFrame) -> pd.Series:
    return panel["macro_baa_spread"].groupby(level="date").first()
cross_sectional_beta_dispersion_timinglow_riskMarket-timing signal based on cross-sectional dispersion of market betas among the most liquid names

Market-timing signal based on cross-sectional dispersion of market betas among the most liquid names. High dispersion (relative to mean beta) indicates a fragile, disagreement-laden market prone to weak/crash-risk future returns; compressed dispersion follows panic comovement and precedes recoveries. expected_sign=-1: high dispersion -> low exposure.

Category low_riskDirection High score → risk-off (cut exposure)Data price
def compute(self, panel) -> pd.Series:
    df = panel[['dvol_21', 'beta_252']].copy()
    df = df.dropna(subset=['dvol_21', 'beta_252'])
    df = df[df['dvol_21'] > 0]
    def per_date(g):
        g = g.sort_values('dvol_21', ascending=False)
        g = g.head(1500)
        n = len(g)
        if n < 30:
            return np.nan
        beta = g['beta_252']
        lo = beta.quantile(0.01)
        hi = beta.quantile(0.99)
        beta_w = beta.clip(lower=lo, upper=hi)
        disp = beta_w.std()
        mean_beta = beta_w.mean()
        if not (mean_beta > 0):
            return np.nan
        return disp / mean_beta
    result = df.groupby(level='date').apply(per_date)
    result = result.astype(float)
    result.index.name = 'date'
    return result
cross_sectional_return_dispersion_timinglow_riskCross-sectional dispersion of trailing 1-month returns, scaled by its own trailing 36-month average, smoothed…

Cross-sectional dispersion of trailing 1-month returns, scaled by its own trailing 36-month average, smoothed over 3 months, used as a market-timing signal. High dispersion signals elevated uncertainty/regime instability and predicts weak forward market returns (expected_sign=-1).

Category low_riskDirection High score → risk-off (cut exposure)Data price
def compute(self, panel) -> pd.Series:
    cols = ['ret_1m', 'ret_12m', 'marketcap', 'dvol_21']
    df = panel[cols].copy()
    # replace inf with nan and drop invalid rows
    df = df.replace([np.inf, -np.inf], np.nan)
    df = df.dropna(subset=cols)
    min_n = 30
    def winsorize(s: pd.Series) -> pd.Series:
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    def disp_for_date(g: pd.DataFrame):
        if len(g) < min_n:
            return np.nan
        w = winsorize(g['ret_1m'])
        return w.std()
    disp = df.groupby(level='date').apply(disp_for_date)
    disp.index = disp.index.get_level_values(0) if isinstance(disp.index, pd.MultiIndex) else disp.index
    disp = disp.sort_index()
    # trailing 36-month rolling std of ret_1m dispersion (past-only, shifted by 1)
    trailing_scale = disp.shift(1).rolling(window=36, min_periods=12).mean()
    scaled_disp = disp / trailing_scale.replace(0, np.nan)
    # rolling 3-month mean, past-only
    signal = scaled_disp.rolling(window=3, min_periods=1).mean()
    signal.name = "cross_sectional_return_dispersion_timing"
    return signal
halloween_volatility_scaled_timingmomentumHalloween/‘Sell in May’ seasonal timer scaled by inverse VIX regime (vs expanding past median) and…

Halloween/‘Sell in May’ seasonal timer scaled by inverse VIX regime (vs expanding past median) and downweighted in NBER recessions or rising-unemployment regimes.

Category momentumDirection High score → risk-on (raise exposure)Data macro, price
def compute(self, panel) -> pd.Series:
    vix = panel['macro_vix'].groupby(level='date').mean()
    usrec = panel['macro_usrec'].groupby(level='date').mean()
    unrate_chg = panel['macro_unrate_chg12m'].groupby(level='date').mean()
    dates = vix.index.sort_values()
    vix = vix.reindex(dates)
    usrec = usrec.reindex(dates)
    unrate_chg = unrate_chg.reindex(dates)
    # forward-fill at most 1 month for sporadic gaps
    vix = vix.ffill(limit=1)
    usrec = usrec.ffill(limit=1)
    unrate_chg = unrate_chg.ffill(limit=1)
    # fill remaining NaNs in usrec/unrate_chg with 0
    usrec = usrec.fillna(0.0)
    unrate_chg = unrate_chg.fillna(0.0)
    # seasonal component
    months = pd.Series(dates.month, index=dates)
    seas = months.apply(lambda m: 1.0 if m in {11, 12, 1, 2, 3, 4} else -0.5)
    # expanding past-only median of vix, min 24 obs
    vix_shifted = vix.shift(1)
    vixmed = vix_shifted.expanding(min_periods=24).median()
    count = vix_shifted.expanding(min_periods=1).count()
    ratio = vix / vixmed
    vscale = 1.0 / (1.0 + np.maximum(0.0, ratio - 1.0))
    vscale = vscale.where(count >= 24, 1.0)
    vscale = vscale.where(vixmed.notna() | (count < 24), 1.0)
    # recession adjustment
    unrate_clip = unrate_chg.clip(lower=0.0, upper=3.0)
    rec_adj = 1.0 - 0.5 * usrec - 0.25 * unrate_clip
    rec_adj = rec_adj.clip(lower=0.0, upper=1.0)
    exposure = seas * vscale * rec_adj
    # NaN where vix itself is fully missing after fallback
    exposure = exposure.where(vix.notna(), np.nan)
    exposure.name = self.name
    return exposure
market_breadth_high_ratio_timingmomentumMarket breadth signal based on the cross-sectional average and fraction of stocks trading near their 52-week…

Market breadth signal based on the cross-sectional average and fraction of stocks trading near their 52-week high (high_ratio). Broadening breadth predicts positive forward market returns; narrowing breadth (deterioration relative to its own trailing 12-month average) predicts weak forward returns.

Category momentumDirection High score → risk-on (raise exposure)Data price
def compute(self, panel) -> pd.Series:
    hr = panel['high_ratio']
    valid_count = hr.groupby(level='date').apply(lambda s: s.notna().sum())
    brd = hr.groupby(level='date').mean()
    pct_hi = hr.groupby(level='date').apply(lambda s: (s > 0.95).sum() / max(s.notna().sum(), 1))
    # invalidate months with insufficient valid tickers
    insufficient = valid_count < self.MIN_VALID
    brd = brd.where(~insufficient, np.nan)
    pct_hi = pct_hi.where(~insufficient, np.nan)
    brd = brd.sort_index()
    pct_hi = pct_hi.sort_index()
    # rolling 12-month mean, past-only (shifted 1)
    brd_roll_mean = brd.rolling(window=12, min_periods=12).mean().shift(1)
    pct_hi_roll_mean = pct_hi.rolling(window=12, min_periods=12).mean().shift(1)
    brd_dev = brd - brd_roll_mean
    pct_hi_dev = pct_hi - pct_hi_roll_mean
    def zscore_past(s: pd.Series) -> pd.Series:
        mean_past = s.expanding(min_periods=12).mean().shift(1)
        std_past = s.expanding(min_periods=12).std().shift(1)
        z = (s - mean_past) / std_past.replace(0, np.nan)
        return z
    z_brd = zscore_past(brd_dev)
    z_pct_hi = zscore_past(pct_hi_dev)
    raw = 0.5 * z_brd + 0.5 * z_pct_hi
    # require at least MIN_HISTORY prior months before emitting non-NaN exposure
    n_prior = pd.Series(np.arange(len(raw)), index=raw.index)
    raw = raw.where(n_prior >= self.MIN_HISTORY, np.nan)
    exposure = raw.fillna(0.0)
    return exposure
market_trend_timingmomentumTime-series trend: hold the market when its trailing 12m return is strong, lighten when weak

Time-series trend: hold the market when its trailing 12m return is strong, lighten when weak.

Category momentumDirection High score → risk-on (raise exposure)Data price
def compute(self, panel: pd.DataFrame) -> pd.Series:
    # market trend proxy = cross-sectional mean of trailing 12m return, per date
    return panel["ret_12m"].groupby(level="date").mean()
aggregate_illiquidity_shock_timingotherMarket-timing signal based on Amihud-style aggregate illiquidity shocks: computes cross-sectional median of…

Market-timing signal based on Amihud-style aggregate illiquidity shocks: computes cross-sectional median of |ret_1m|/dvol_21 across liquid stocks, log-transforms it, and measures its deviation from a trailing 12-month average (min 6 obs). Spikes in aggregate illiquidity relative to trend signal funding/liquidity stress and precede high subsequent market returns.

Category otherDirection High score → risk-on (raise exposure)Data price
def compute(self, panel) -> pd.Series:
    df = panel[['ret_1m', 'dvol_21', 'marketcap', 'closeunadj']].copy()
    valid = (
        df['dvol_21'].notna() & (df['dvol_21'] > 0) &
        df['ret_1m'].notna() &
        df['marketcap'].notna() &
        df['closeunadj'].notna()
    )
    df = df[valid]
    illiq = df['ret_1m'].abs() / df['dvol_21']
    illiq = illiq.replace([np.inf, -np.inf], np.nan).dropna()
    agg = illiq.groupby(level='date').median()
    agg = np.log(agg)
    agg = agg.sort_index()
    trailing_mean = agg.rolling(12, min_periods=6).mean().shift(1)
    exposure = agg - trailing_mean
    exposure.index.name = 'date'
    return exposure
aggregate_rollover_risk_funding_shock_timingotherAggregate corporate debt rollover fragility (median debtc/debt across tickers) deviated from its 36m rolling…

Aggregate corporate debt rollover fragility (median debtc/debt across tickers) deviated from its 36m rolling mean, interacted with a 12m funding shock (fed funds change + Baa spread change), to time market exposure. Short-maturity debt structures amplify sensitivity to rate/credit shocks, so higher exposure_score predicts lower forward returns.

Category otherDirection High score → risk-off (cut exposure)Data fundamental, macro
def compute(self, panel) -> pd.Series:
    debt = panel['debt']
    debtc = panel['debtc']
    valid = (debt > 0) & debtc.notna() & debt.notna()
    ratio = pd.Series(np.nan, index=panel.index)
    ratio[valid] = debtc[valid] / debt[valid]
    roll = ratio.groupby(level='date').median()
    roll = roll.sort_index()
    roll_mean = roll.rolling(window=36, min_periods=36).mean()
    roll_dev = roll - roll_mean.shift(1)
    macro_ff = panel['macro_fedfunds_chg12m'].groupby(level='date').first()
    macro_baa = panel['macro_baa_spread_chg12m'].groupby(level='date').first()
    macro_ff, macro_baa = macro_ff.reindex(roll.index), macro_baa.reindex(roll.index)
    shock = macro_ff + macro_baa
    exposure_score = roll_dev * shock
    return exposure_score
aggregate_turnover_sentiment_timingotherBaker-Stein style aggregate turnover sentiment timing: high market-wide turnover (dollar volume / market cap)…

Baker-Stein style aggregate turnover sentiment timing: high market-wide turnover (dollar volume / market cap) relative to its trailing multi-year norm signals sentiment-driven overpricing under short-sale constraints, forecasting LOW subsequent market returns. Exposure is set inverse to the deviation of log turnover from its trailing 36m mean.

Category otherDirection High score → risk-off (cut exposure)Data price
def compute(self, panel) -> pd.Series:
    dvol = panel['dvol_21']
    mcap = panel['marketcap']
    valid = dvol.notna() & mcap.notna() & (dvol > 0) & (mcap > 0)
    dollar_vol_monthly = (dvol * 21).where(valid)
    mcap_valid = mcap.where(valid)
    agg_dvol = dollar_vol_monthly.groupby(level='date').sum(min_count=1)
    agg_mcap = mcap_valid.groupby(level='date').sum(min_count=1)
    turn = agg_dvol / agg_mcap
    turn = turn.where(agg_mcap > 0)
    x = np.log(turn.where(turn > 0))
    x = x.sort_index()
    rolling_mean = x.shift(1).rolling(window=36, min_periods=24).mean()
    signal = x - rolling_mean
    exposure = -signal
    return exposure
average_systematic_comovement_timingotherPollet and Wilson (2010)-style average comovement timer: cross-sectional mean absolute trailing correlation…

Pollet and Wilson (2010)-style average comovement timer: cross-sectional mean absolute trailing correlation of stocks to VIX, credit, and rates macro shocks. High average comovement signals a high quantity of undiversifiable systematic risk, which should be compensated by higher subsequent market returns.

Category otherDirection High score → risk-on (raise exposure)Data price, macro
def compute(self, panel) -> pd.Series:
    cols = ['corr_vix_252', 'corr_credit_252', 'corr_rates_252']
    comps = []
    for col in cols:
        if col not in panel.columns:
            continue
        s = panel[col].dropna()
        abs_s = s.abs()
        grp = abs_s.groupby(level='date')
        mean_c = grp.mean()
        count_c = grp.count()
        mean_c = mean_c.where(count_c >= self.MIN_STOCKS, np.nan)
        comps.append(mean_c)
    if not comps:
        dates = panel.index.get_level_values('date').unique()
        return pd.Series(np.nan, index=dates)
    df = pd.concat(comps, axis=1)
    comove = df.mean(axis=1, skipna=True)
    exposure = comove
    return exposure
consumer_sentiment_credit_divergence_timingotherTimes equity exposure using the divergence between consumer sentiment (U

Times equity exposure using the divergence between consumer sentiment (U. Michigan) and corporate credit spreads (Baa - 10y): gloomy consumers combined with tight/stable credit spreads implies excess equity pessimism and predicts high forward returns, while euphoric sentiment with widening spreads signals hidden balance-sheet stress and predicts poor returns. A momentum overlay on credit spread and unemployment 12m changes refines the timing signal further.

Category otherDirection High score → risk-on (raise exposure)Data macro, price
def compute(self, panel) -> pd.Series:
    umcsent = panel['macro_umcsent'].groupby(level='date').mean().sort_index()
    baa_spread = panel['macro_baa_spread'].groupby(level='date').mean().sort_index()
    baa_chg12m = panel['macro_baa_spread_chg12m'].groupby(level='date').mean().sort_index()
    unrate_chg12m = panel['macro_unrate_chg12m'].groupby(level='date').mean().sort_index()
    umcsent = umcsent.ffill()
    baa_spread = baa_spread.ffill()
    baa_chg12m = baa_chg12m.ffill()
    unrate_chg12m = unrate_chg12m.ffill()
    s_sent = _zscore_pastonly(umcsent)
    s_credit = _zscore_pastonly(baa_spread)
    s_baa_chg = _zscore_pastonly(baa_chg12m)
    s_unrate_chg = _zscore_pastonly(unrate_chg12m)
    div = (-s_sent) - (-s_credit)
    div2 = div - 0.5 * s_baa_chg - 0.5 * s_unrate_chg
    exposure = div2.dropna()
    exposure.index.name = 'date'
    return exposure
credit_equity_divergence_timingotherTimes equity market exposure using the divergence between credit-spread improvement and equity-market momentum

Times equity market exposure using the divergence between credit-spread improvement and equity-market momentum. When credit spreads have been tightening while equities have lagged, exposure rises; the reverse divergence (spreads widening into equity strength) lowers exposure.

Category otherDirection High score → risk-on (raise exposure)Data macro, price
def compute(self, panel) -> pd.Series:
    macro_spread = panel['macro_baa_spread'].groupby(level='date').mean()
    ret_3m = panel['ret_3m']
    # market return proxy: cross-sectional mean of ret_3m per date, dropping NaN
    mkt = ret_3m.dropna().groupby(level='date').mean()
    # credit improvement: negative of 3-month change in spread
    macro_spread = macro_spread.sort_index()
    credit_imp = -(macro_spread - macro_spread.shift(3))
    credit_imp = credit_imp.dropna()
    # align dates
    common_dates = credit_imp.index.intersection(mkt.index)
    credit_imp = credit_imp.loc[common_dates].sort_index()
    mkt = mkt.loc[common_dates].sort_index()
    min_obs = 12
    def expanding_z(s: pd.Series) -> pd.Series:
        mean = s.expanding().mean()
        std = s.expanding().std()
        count = s.expanding().count()
        z = (s - mean) / std
        z = z.where((count >= min_obs) & (std > 0))
        return z
    z1 = expanding_z(credit_imp)
    z2 = expanding_z(mkt)
    exposure = z1 - z2
    exposure = exposure.reindex(common_dates)
    exposure.index.name = 'date'
    return exposure
credit_vol_complacency_divergence_timingotherCross-asset divergence between credit spread stress and equity implied volatility complacency

Cross-asset divergence between credit spread stress and equity implied volatility complacency. When Baa-Treasury credit spreads are historically wide relative to VIX, credit markets are pricing stress that equities have not yet discounted, predicting poor subsequent market returns; the converse signals fear-driven overreaction and a buying opportunity.

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    baa = panel['macro_baa_spread'].groupby(level='date').first().sort_index()
    vix = panel['macro_vix'].groupby(level='date').first().sort_index()
    baa_chg = panel['macro_baa_spread_chg12m'].groupby(level='date').first().sort_index()
    vix_chg = panel['macro_vix_chg12m'].groupby(level='date').first().sort_index()
    z_credit = self._past_z(baa)
    z_vix = self._past_z(vix)
    d_credit = self._past_z(baa_chg)
    d_vix = self._past_z(vix_chg)
    raw = 0.5 * (z_credit - z_vix) + 0.5 * (d_credit - d_vix)
    raw.name = self.name
    return raw
dollar_liquidity_cycle_timingotherTimes market exposure using the 12-month relative trend in the broad USD index, amplified by the…

Times market exposure using the 12-month relative trend in the broad USD index, amplified by the cross-sectional average absolute USD beta relative to its own expanding history. A strengthening dollar tightens global financial conditions and should be followed by weaker equity risk premia, especially when the equity universe is currently more FX-sensitive than usual; the effect is inverted (i.e. dollar depreciation with high FX-sensitivity favors risk-taking).

Category otherDirection High score → risk-on (raise exposure)Data macro, price
def compute(self, panel) -> pd.Series:
    # Date-level average absolute USD beta (ignore NaNs), requiring a minimum count
    beta_usd = panel['beta_usd_252']
    grp = beta_usd.abs().groupby(level='date')
    A_t = grp.mean()
    counts = grp.count()
    min_count = 5
    A_t = A_t.where(counts >= min_count)
    A_t = A_t.sort_index()
    # Expanding mean, past-only (shifted by 1 to avoid look-ahead), min 24 obs
    expanding_mean = A_t.expanding(min_periods=24).mean().shift(1)
    A_hat = A_t / expanding_mean.where(expanding_mean > 0)
    A_hat_clipped = A_hat.clip(lower=0.5, upper=2.0)
    # Macro USD series at date level (broadcast, take first/mean per date)
    macro_usd = panel['macro_usd'].groupby(level='date').mean().sort_index()
    macro_usd_chg12m = panel['macro_usd_chg12m'].groupby(level='date').mean().sort_index()
    macro_usd_lag12 = macro_usd.shift(12)
    trend = macro_usd_chg12m / macro_usd_lag12.where(macro_usd_lag12 > 0)
    exposure_raw = -trend * A_hat_clipped
    exposure_raw = exposure_raw.reindex(A_t.index)
    return exposure_raw
equity_fear_vs_credit_stress_divergence_timingotherCross-asset divergence between equity fear (VIX) and credit stress (Baa spread): z-scores each series over a…

Cross-asset divergence between equity fear (VIX) and credit stress (Baa spread): z-scores each series over a trailing 60m window (min 36m history) and takes their difference. High values mean equity implied volatility is elevated relative to what credit spreads imply, signalling unconfirmed panic/hedging pressure that should mean-revert, warranting higher market exposure; low values mean credit stress is not matched by equity vol, signalling unpriced deterioration and lower exposure.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    vix = panel["macro_vix"].groupby(level="date").mean().sort_index()
    baa = panel["macro_baa_spread"].groupby(level="date").mean().sort_index()
    vix = vix.ffill()
    baa = baa.ffill()
    min_p = 36
    window = 60
    vix_mean = vix.rolling(window=window, min_periods=min_p).mean()
    vix_std = vix.rolling(window=window, min_periods=min_p).std()
    baa_mean = baa.rolling(window=window, min_periods=min_p).mean()
    baa_std = baa.rolling(window=window, min_periods=min_p).std()
    z_v = (vix - vix_mean) / vix_std
    z_c = (baa - baa_mean) / baa_std
    z_v = z_v.where(vix_std > 0, np.nan)
    z_c = z_c.where(baa_std > 0, np.nan)
    signal = z_v - z_c
    signal = signal.where(vix.notna() & baa.notna())
    return signal
equity_rate_correlation_regime_timingotherTimes the market using the date-level average stock correlation to rate changes (corr_rates_252)

Times the market using the date-level average stock correlation to rate changes (corr_rates_252). Positive average correlation signals a growth-driven regime where bonds hedge equities and risk premia are earned; negative correlation signals an inflation/discount-rate regime with weaker forward equity returns. A small penalty is applied when 10y breakeven inflation expectations are elevated (past-only z-score).

Category otherDirection High score → risk-on (raise exposure)Data price, macro
def compute(self, panel) -> pd.Series:
    corr = panel['corr_rates_252'].dropna()
    m = corr.groupby(level='date').mean()
    # ensure full date index, forward-fill dates with no valid observations
    all_dates = panel.index.get_level_values('date').unique().sort_values()
    m = m.reindex(all_dates).ffill()
    exposure = m.rolling(3, min_periods=1).mean()
    # inflation penalty using macro_breakeven10, past-only z-score
    if 'macro_breakeven10' in panel.columns:
        be = panel['macro_breakeven10'].groupby(level='date').mean()
        be = be.reindex(all_dates).ffill()
        be_mean = be.expanding(min_periods=1).mean().shift(1)
        be_std = be.expanding(min_periods=1).std().shift(1)
        z = (be - be_mean) / be_std.where(be_std > 0)
        z = z.fillna(0.0)
        exposure = exposure - 0.25 * z
    exposure.name = self.name
    exposure.index.name = 'date'
    return exposure
equity_yield_vs_real_rate_gap_timingotherMarket-timing signal based on the gap between the cross-sectional median earnings yield of large-cap stocks…

Market-timing signal based on the gap between the cross-sectional median earnings yield of large-cap stocks and the real 10y rate (nominal 10y yield minus 10y breakeven inflation). A wide gap (equities cheap relative to real rates) predicts elevated forward market returns; the level is combined with a deviation from its trailing 36-month mean.

Category otherDirection High score → risk-on (raise exposure)Data price, fundamental, macro
def compute(self, panel) -> pd.Series:
    df = panel[['pe', 'marketcap', 'macro_dgs10', 'macro_breakeven10']].copy()
    # keep valid pe and marketcap
    valid = df['pe'].notna() & (df['pe'] > 0) & df['marketcap'].notna() & (df['marketcap'] > 0)
    df = df[valid]
    def median_top_half_ey(group):
        g = group.dropna(subset=['marketcap'])
        if len(g) == 0:
            return np.nan
        median_mc = g['marketcap'].median()
        top_half = g[g['marketcap'] >= median_mc]
        ey = 100.0 / top_half['pe']
        if len(ey) == 0:
            return np.nan
        return ey.median()
    ey_by_date = df.groupby(level='date').apply(median_top_half_ey)
    ey_by_date = ey_by_date.sort_index()
    # macro series at date level (broadcast, take first per date)
    dgs10 = panel['macro_dgs10'].groupby(level='date').first().sort_index()
    breakeven10 = panel['macro_breakeven10'].groupby(level='date').first().sort_index()
    # forward-fill from prior month-end (no future values)
    dgs10 = dgs10.ffill()
    breakeven10 = breakeven10.ffill()
    real_y = dgs10 - breakeven10
    # align all series on the common date index
    all_dates = ey_by_date.index.union(real_y.index).sort_values()
    ey_by_date = ey_by_date.reindex(all_dates)
    real_y = real_y.reindex(all_dates)
    gap = ey_by_date - real_y
    # if macro missing (dgs10 or breakeven10 not available even after ffill), gap is NaN -> skip
    gap = gap.where(real_y.notna() & ey_by_date.notna())
    # trailing 36-month mean of gap, past-only (exclude current observation)
    trailing_mean = gap.shift(1).rolling(window=36, min_periods=36).mean()
    trailing_count = gap.shift(1).rolling(window=36, min_periods=1).count()
    signal = gap.copy()
    has_enough = trailing_count >= 36
    adj = gap - trailing_mean
    signal = gap + 0.5 * adj.where(has_enough, 0.0)
    signal = signal.where(gap.notna())
    signal.name = self.name
    return signal
household_sentiment_fundamental_divergence_timingotherContrarian macro timer using the divergence between consumer sentiment (UMich) and hard real-economy…

Contrarian macro timer using the divergence between consumer sentiment (UMich) and hard real-economy fundamentals (industrial production growth and unemployment dynamics). When sentiment is unusually pessimistic relative to fundamentals, risk premia are elevated and expected returns are high; exposure is negative of this expanding-z-score gap.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    senti = panel['macro_umcsent'].groupby(level='date').mean()
    indpro_chg = panel['macro_indpro_chg12m'].groupby(level='date').mean()
    unrate_chg = panel['macro_unrate_chg12m'].groupby(level='date').mean()
    senti = senti.sort_index().ffill()
    indpro_chg = indpro_chg.sort_index().ffill()
    unrate_chg = unrate_chg.sort_index().ffill()
    first_valid = max(
        senti.first_valid_index(),
        indpro_chg.first_valid_index(),
        unrate_chg.first_valid_index(),
    )
    senti = senti.loc[first_valid:]
    indpro_chg = indpro_chg.loc[first_valid:]
    unrate_chg = unrate_chg.loc[first_valid:]
    z_senti = self._expanding_z(senti)
    z_indpro = self._expanding_z(indpro_chg)
    z_unrate_inv = self._expanding_z(-unrate_chg)
    fund = 0.5 * (z_indpro + z_unrate_inv)
    gap = z_senti - fund
    exposure = -gap
    exposure = exposure.reindex(senti.index)
    exposure = exposure.fillna(0.0)
    return exposure
labor_cycle_turning_point_timingotherMarket timing signal based on labor-market inflection points: combines the unemployment-rate gap from its…

Market timing signal based on labor-market inflection points: combines the unemployment-rate gap from its trailing 12-month trough (Sahm-rule style) with confirmation from industrial production growth and consumer sentiment growth. A rising unemployment gap off its trough signals elevated drawdown risk (negative exposure), while unemployment at its trough with expanding industrial production and improving sentiment signals low drawdown risk (positive exposure).

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    def z(x: pd.Series) -> pd.Series:
        mean = x.expanding(24).mean()
        std = x.expanding(24).std()
        out = (x - mean) / std
        out = out.where(std > 0)
        return out
    u = panel['macro_unrate'].groupby(level='date').first()
    u = u.sort_index()
    gap = u - u.rolling(12).min()
    ip = panel['macro_indpro'].groupby(level='date').first()
    ip = ip.sort_index()
    ipg = ip / ip.shift(12) - 1.0
    sent = panel['macro_umcsent'].groupby(level='date').first()
    sent = sent.sort_index()
    sg = sent / sent.shift(12) - 1.0
    z_gap = z(gap)
    z_ipg = z(ipg)
    z_sg = z(sg)
    exposure = -1.0 * z_gap + 0.5 * z_ipg + 0.25 * z_sg
    exposure.name = self.name
    return exposure
macro_momentum_diffusion_timingotherDate-level macro breadth/diffusion index counting how many independent macro dimensions (industrial…

Date-level macro breadth/diffusion index counting how many independent macro dimensions (industrial production, unemployment, credit spread, consumer sentiment, VIX, term spread) are improving over the trailing 12 months. Exposure is raised when macro breadth broadly improves and cut when several dimensions deteriorate together.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    components = [
        ('macro_indpro_chg12m', 1),
        ('macro_unrate_chg12m', -1),
        ('macro_baa_spread_chg12m', -1),
        ('macro_umcsent_chg12m', 1),
        ('macro_vix_chg12m', -1),
        ('macro_t10y2y_chg12m', 1),
    ]
    date_level_series = {}
    for col, _ in components:
        s = panel[col].groupby(level='date').mean()
        date_level_series[col] = s
    dates = None
    for s in date_level_series.values():
        if dates is None:
            dates = s.index
        else:
            dates = dates.union(s.index)
    dates = dates.sort_values()
    signed_sum = pd.Series(0.0, index=dates)
    count = pd.Series(0, index=dates)
    for col, weight in components:
        s = date_level_series[col].reindex(dates)
        sign_s = np.sign(s)
        valid = sign_s.notna()
        signed_sum = signed_sum.add(sign_s.where(valid, 0.0) * weight, fill_value=0.0)
        count = count.add(valid.astype(int), fill_value=0)
    exposure = signed_sum / count.replace(0, np.nan)
    exposure = exposure.where(count > 0, np.nan)
    return exposure
macro_pessimism_contrarian_market_timingotherContrarian macro timer: goes long equities when consumer sentiment is depressed relative to its own history…

Contrarian macro timer: goes long equities when consumer sentiment is depressed relative to its own history and unemployment has recently risen, harvesting a countercyclical equity risk premium. Built purely from survey/labor/industrial-production data, uncorrelated with credit-spread or volatility-based timers.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    cols = [
        "macro_umcsent",
        "macro_umcsent_chg12m",
        "macro_unrate_chg12m",
        "macro_indpro_chg12m",
    ]
    def first_per_date(col):
        return panel[col].groupby(level="date").first()
    data = pd.DataFrame({c: first_per_date(c) for c in cols})
    data = data.sort_index()
    data = data.ffill()
    def zexp(x: pd.Series) -> pd.Series:
        mean = x.expanding().mean().shift(1)
        std = x.expanding().std().shift(1)
        z = (x - mean) / std
        z = z.where(std > 0, np.nan)
        return z
    z_sent = zexp(data["macro_umcsent"])
    z_sent_chg = zexp(data["macro_umcsent_chg12m"])
    z_unrate_chg = zexp(data["macro_unrate_chg12m"])
    z_indpro_chg = zexp(data["macro_indpro_chg12m"])
    raw = (
        -z_sent
        - 0.5 * z_sent_chg
        + 0.5 * z_unrate_chg
        - 0.25 * z_indpro_chg
    )
    exposure = raw.clip(-2, 2)
    exposure = exposure.where(raw.notna(), 0.0)
    return exposure
macro_risk_on_breadth_contrarian_timingotherCounts breadth of risk-on macro trends (tightening credit spreads, falling VIX, steepening curve, rising…

Counts breadth of risk-on macro trends (tightening credit spreads, falling VIX, steepening curve, rising industrial production, rising consumer sentiment, rising oil) and returns the negative breadth as market exposure, contrarian to macro momentum: buy when macro consensus is uniformly deteriorating, de-risk when uniformly benign.

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    cols = [
        'macro_baa_spread_chg12m',
        'macro_vix_chg12m',
        'macro_t10y2y_chg12m',
        'macro_indpro_chg12m',
        'macro_umcsent_chg12m',
        'macro_oil_chg12m',
    ]
    date_series = {}
    for c in cols:
        s = panel[c].groupby(level='date').mean()
        s = s.sort_index()
        s = s.ffill()
        date_series[c] = s
    df = pd.DataFrame(date_series)
    def safe_sign(x):
        return x.apply(lambda v: 0.0 if pd.isna(v) else np.sign(v))
    breadth = (
        safe_sign(-df['macro_baa_spread_chg12m'])
        + safe_sign(-df['macro_vix_chg12m'])
        + safe_sign(df['macro_t10y2y_chg12m'])
        + safe_sign(df['macro_indpro_chg12m'])
        + safe_sign(df['macro_umcsent_chg12m'])
        + safe_sign(df['macro_oil_chg12m'])
    )
    breadth.name = self.name
    return breadth
oil_demand_vs_supply_shock_timingotherInteracts the cross-sectional average stock-oil correlation with the trailing oil price trend to distinguish…

Interacts the cross-sectional average stock-oil correlation with the trailing oil price trend to distinguish demand-driven vs supply-driven oil shocks, timing market exposure accordingly.

Category otherDirection High score → risk-on (raise exposure)Data price, macro
def compute(self, panel) -> pd.Series:
    min_obs = 12
    # Cross-sectional average correlation with oil, per date
    avgcorr_t = panel['corr_oil_252'].groupby(level='date').mean()
    avgcorr_t = avgcorr_t.sort_index()
    # Macro oil series (broadcast per date -> take date-level mean)
    macro_oil = panel['macro_oil'].groupby(level='date').mean().sort_index()
    macro_oil_chg12m = panel['macro_oil_chg12m'].groupby(level='date').mean().sort_index()
    # Oil trend measure: 12m change normalized by lagged price level
    macro_oil_lag12 = macro_oil.shift(12)
    o_raw = macro_oil_chg12m / macro_oil_lag12.where(macro_oil_lag12 > 0)
    # Past-only expanding z-score helper
    def past_only_zscore(s: pd.Series) -> pd.Series:
        mean_shifted = s.expanding().mean().shift(1)
        std_shifted = s.expanding().std().shift(1)
        count_shifted = s.expanding().count().shift(1)
        z = (s - mean_shifted) / std_shifted
        z = z.where((count_shifted >= min_obs) & (std_shifted > 0))
        return z
    c = past_only_zscore(avgcorr_t)
    o = past_only_zscore(o_raw)
    exposure = c * o
    # Neutralize (set to 0) where inputs are missing per policy
    invalid = avgcorr_t.isna() | macro_oil.isna() | macro_oil_chg12m.isna() | c.isna() | o.isna()
    exposure = exposure.where(~invalid, 0.0)
    exposure.name = self.name
    return exposure
oil_shock_demand_supply_timingotherCross-sectional average trailing correlation of stocks with oil classifies whether oil moves are…

Cross-sectional average trailing correlation of stocks with oil classifies whether oil moves are demand-driven (growth-friendly, positive correlation) or supply-driven (cost shock, negative correlation). Multiplying this revealed correlation by the recent oil trend gives a market-exposure signal: long equities during demand-led oil rallies, defensive during supply-led ones.

Category otherDirection High score → risk-on (raise exposure)Data price, macro
def compute(self, panel) -> pd.Series:
    mean_corr = panel['corr_oil_252'].groupby(level='date').mean()
    macro_oil = panel['macro_oil'].groupby(level='date').first()
    macro_oil_chg12m = panel['macro_oil_chg12m'].groupby(level='date').first()
    denom = macro_oil.where(macro_oil > 0)
    oil_mom = macro_oil_chg12m / denom
    oil_mom = oil_mom.clip(-1, 1)
    exposure = mean_corr * oil_mom
    # forward-fill exposure when oil_mom is NaN (macro_oil missing/invalid)
    exposure = exposure.where(oil_mom.notna())
    exposure = exposure.ffill()
    # remaining NaNs (e.g., at start) treated as neutral
    exposure = exposure.fillna(0.0)
    exposure.index.name = 'date'
    return exposure
profitless_market_cap_share_timingotherAggregate market-cap share held by firms with negative trailing twelve-month net income, measured as a…

Aggregate market-cap share held by firms with negative trailing twelve-month net income, measured as a deviation from its own 36-month trailing norm. Rising ‘froth’ share signals speculative excess and contrarian-low future market returns; exposure is the negative of the deviation (harness applies expected_sign).

Category otherDirection High score → risk-off (cut exposure)Data fundamental, price
def compute(self, panel) -> pd.Series:
    df = panel[['netinc', 'marketcap', 'closeunadj', 'dvol_21']].copy()
    # liquidity/data filters
    valid = (
        df['marketcap'].notna() & (df['marketcap'] > 0) &
        df['closeunadj'].notna() &
        df['dvol_21'].notna()
    )
    df = df[valid]
    # TTM net income via lags per ticker
    netinc = df['netinc']
    shift3 = netinc.groupby(level='ticker').shift(3)
    shift6 = netinc.groupby(level='ticker').shift(6)
    shift9 = netinc.groupby(level='ticker').shift(9)
    ttm_netinc = netinc + shift3 + shift6 + shift9
    ttm_valid = netinc.notna() & shift3.notna() & shift6.notna() & shift9.notna()
    df['ttm_netinc'] = ttm_netinc
    df = df[ttm_valid]
    # profitless flag
    df['profitless_mcap'] = np.where(df['ttm_netinc'] < 0, df['marketcap'], 0.0)
    grouped = df.groupby(level='date')
    sum_profitless = grouped['profitless_mcap'].sum()
    sum_total = grouped['marketcap'].sum()
    froth = sum_profitless / sum_total.where(sum_total > 0)
    froth = froth.sort_index()
    rolling_mean = froth.shift(1).rolling(window=36, min_periods=24).mean()
    signal = froth - rolling_mean
    exposure = -signal
    return exposure
real_policy_stance_timingotherReal policy stance timing: combines the real fed funds rate (fed funds minus trailing 12m CPI inflation) with…

Real policy stance timing: combines the real fed funds rate (fed funds minus trailing 12m CPI inflation) with the 12m change in fed funds to gauge monetary tightness. A high/rising real policy stance signals scarce liquidity and elevated discount rates, predicting lower forward equity returns; a deeply negative/easing stance predicts higher forward returns (expected_sign=-1).

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    cpi = panel['macro_cpi'].groupby(level='date').first().sort_index()
    fedfunds = panel['macro_fedfunds'].groupby(level='date').first().sort_index()
    fedfunds_chg12m = panel['macro_fedfunds_chg12m'].groupby(level='date').first().sort_index()
    cpi = cpi.ffill()
    fedfunds = fedfunds.ffill()
    fedfunds_chg12m = fedfunds_chg12m.ffill()
    cpi_lag12 = cpi.shift(12)
    infl = 100.0 * (cpi / cpi_lag12 - 1.0)
    infl = infl.where(cpi_lag12 > 0, np.nan)
    real_rate = fedfunds - infl
    stance = 0.6 * real_rate + 0.4 * fedfunds_chg12m
    smooth = stance.rolling(3, min_periods=1).mean()
    return smooth
real_yield_shock_timingotherDecomposes nominal 10y yield into breakeven inflation and real yield

Decomposes nominal 10y yield into breakeven inflation and real yield. Rising real yields (12m shock) and elevated real yield relative to its own 60m history signal tightening financial conditions and predict weak equity returns; falling real yields support risk-taking.

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    dgs10 = panel['macro_dgs10'].groupby(level='date').first()
    breakeven10 = panel['macro_breakeven10'].groupby(level='date').first()
    dgs10_chg12m = panel['macro_dgs10_chg12m'].groupby(level='date').first()
    breakeven10_chg12m = panel['macro_breakeven10_chg12m'].groupby(level='date').first()
    dates = dgs10.index.sort_values()
    dgs10 = dgs10.reindex(dates)
    breakeven10 = breakeven10.reindex(dates)
    dgs10_chg12m = dgs10_chg12m.reindex(dates)
    breakeven10_chg12m = breakeven10_chg12m.reindex(dates)
    real10 = dgs10 - breakeven10
    shock12_direct = real10 - real10.shift(12)
    shock12_fallback = dgs10_chg12m - breakeven10_chg12m
    shock12 = shock12_direct.where(shock12_direct.notna(), shock12_fallback)
    roll_mean = real10.rolling(60, min_periods=24).mean().shift(1)
    gap = real10 - roll_mean
    shock12_valid = shock12.notna()
    gap_valid = gap.notna()
    shock12_term = 0.6 * shock12.fillna(0.0)
    gap_term = 0.4 * gap.fillna(0.0)
    signal = shock12_term + gap_term
    signal = signal.where(shock12_valid | gap_valid, np.nan)
    signal.index.name = 'date'
    return signal
sahm_labor_deterioration_timingotherSahm-rule-style labor deterioration timing signal: computes the gap between current unemployment and its…

Sahm-rule-style labor deterioration timing signal: computes the gap between current unemployment and its trailing 12-month minimum, applies a hinge at 0.2pp (nonlinear, near-zero in expansions, positive once labor markets crack), and scales confidence by whether consumer sentiment is deteriorating. Higher exposure_raw indicates rising recession risk and is expected to precede negative equity returns (expected_sign = -1).

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    unrate = panel['macro_unrate'].groupby(level='date').mean().sort_index()
    umcsent_chg = panel['macro_umcsent_chg12m'].groupby(level='date').mean().sort_index()
    unrate = unrate.ffill()
    umcsent_chg = umcsent_chg.ffill()
    u_min12 = unrate.rolling(window=12, min_periods=1).min()
    gap = unrate - u_min12
    hinge = (gap - 0.2).clip(lower=0.0)
    conf = pd.Series(0.5, index=umcsent_chg.index)
    conf[umcsent_chg < 0] = 1.0
    exposure_raw = hinge * conf
    missing_mask = unrate.isna() | umcsent_chg.isna()
    exposure_raw = exposure_raw.where(~missing_mask, 0.0)
    exposure_raw.name = self.name
    return exposure_raw
seasonal_risk_appetite_credit_timingotherHalloween/Sell-in-May seasonal timing rule conditioned on the level of the Baa credit spread: long risk in…

Halloween/Sell-in-May seasonal timing rule conditioned on the level of the Baa credit spread: long risk in the winter half (Nov-Apr) and short/defensive in the summer half (May-Oct), with the winter exposure amplified when the expanding past-only z-score of the Baa credit spread is elevated (wide risk premia).

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    baa = panel['macro_baa_spread'].groupby(level='date').mean()
    baa = baa.sort_index()
    baa = baa.ffill()
    baa = baa.dropna()
    dates = baa.index
    # seasonal sign: +1 for Nov-Apr, -1 for May-Oct
    months = pd.Series(dates.month, index=dates)
    hall = months.apply(lambda m: 1.0 if m in (11, 12, 1, 2, 3, 4) else -1.0)
    # expanding past-only z-score, min 36 obs, shift(1)
    expanding_mean = baa.expanding(min_periods=36).mean().shift(1)
    expanding_std = baa.expanding(min_periods=36).std().shift(1)
    z = (baa - expanding_mean) / expanding_std
    z = z.where(expanding_std > 0, np.nan)
    z = z.fillna(0.0)
    z_clipped = z.clip(lower=0, upper=2)
    exposure = hall * (1.0 + z_clipped)
    exposure.index.name = 'date'
    return exposure
sentiment_capitulation_recovery_timingotherCountercyclical market-timing signal that goes long equity exposure when consumer sentiment is deeply…

Countercyclical market-timing signal that goes long equity exposure when consumer sentiment is deeply depressed relative to its own trailing history (contrarian level) AND is showing signs of stabilization/improvement over the last 3 months (confirming trend), with a secondary boost from elevated unemployment relative to its trailing history. Combines a contrarian level with a confirming direction to capture the capitulation-then-recovery phase of the sentiment cycle.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    umc = panel['macro_umcsent'].groupby(level='date').mean().sort_index()
    umc = umc.ffill()
    lvl_mean = umc.rolling(60, min_periods=24).mean()
    lvl_std = umc.rolling(60, min_periods=24).std()
    lvl_z = (umc - lvl_mean) / lvl_std
    lvl_z = lvl_z.where(lvl_std > 0)
    d3 = umc - umc.shift(3)
    d3_mean = d3.rolling(60, min_periods=24).mean()
    d3_std = d3.rolling(60, min_periods=24).std()
    mom_z = (d3 - d3_mean) / d3_std
    mom_z = mom_z.where(d3_std > 0)
    ur = panel['macro_unrate'].groupby(level='date').mean().sort_index()
    ur = ur.ffill()
    ur_mean = ur.rolling(60, min_periods=24).mean()
    ur_std = ur.rolling(60, min_periods=24).std()
    ur_z = (ur - ur_mean) / ur_std
    ur_z = ur_z.where(ur_std > 0)
    exposure = (-lvl_z + mom_z + 0.5 * ur_z) / 2.5
    exposure = exposure.where(~(lvl_z.isna() | mom_z.isna() | ur_z.isna()), 0.0)
    exposure.name = self.name
    return exposure
sentiment_fundamental_disagreement_timingotherOrthogonalizes consumer sentiment against a composite of hard macro fundamentals (unemployment level/change,…

Orthogonalizes consumer sentiment against a composite of hard macro fundamentals (unemployment level/change, industrial production growth, CPI inflation change). When sentiment is far more pessimistic than hard data warrant, equity risk premia are elevated and subsequent market returns tend to be high; excess euphoria predicts the reverse.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    m = panel.groupby(level='date').first()
    m = m.sort_index()
    umcsent = m['macro_umcsent'].ffill()
    unrate = m['macro_unrate'].ffill()
    unrate_chg12m = m['macro_unrate_chg12m'].ffill()
    indpro_chg12m = m['macro_indpro_chg12m'].ffill()
    cpi_chg12m = m['macro_cpi_chg12m'].ffill()
    z_sent = self._zscore(umcsent)
    z_unrate = self._zscore(unrate)
    z_unrate_chg = self._zscore(unrate_chg12m)
    z_indpro_chg = self._zscore(indpro_chg12m)
    z_cpi_chg = self._zscore(cpi_chg12m)
    z_fund = (-z_unrate - z_unrate_chg + z_indpro_chg - z_cpi_chg) / 4.0
    signal = z_fund - z_sent
    valid = (
        z_sent.notna()
        & z_unrate.notna()
        & z_unrate_chg.notna()
        & z_indpro_chg.notna()
        & z_cpi_chg.notna()
    )
    signal = signal.where(valid)
    return signal
sentiment_fundamental_divergence_timingotherMarket-timing signal based on divergence between consumer sentiment (level + 12m change) and hard economic…

Market-timing signal based on divergence between consumer sentiment (level + 12m change) and hard economic data (industrial production and unemployment 12m changes). When sentiment is far more depressed than fundamentals justify, expected equity risk premium is high and exposure is increased; when sentiment overshoots fundamentals, exposure is reduced.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    umcsent = panel['macro_umcsent'].groupby(level='date').first().sort_index()
    umcsent_chg12m = panel['macro_umcsent_chg12m'].groupby(level='date').first().sort_index()
    indpro_chg12m = panel['macro_indpro_chg12m'].groupby(level='date').first().sort_index()
    unrate_chg12m = panel['macro_unrate_chg12m'].groupby(level='date').first().sort_index()
    umcsent = umcsent.ffill()
    umcsent_chg12m = umcsent_chg12m.ffill()
    indpro_chg12m = indpro_chg12m.ffill()
    unrate_chg12m = unrate_chg12m.ffill()
    z_umcsent = self._zexp(umcsent)
    z_umcsent_chg12m = self._zexp(umcsent_chg12m)
    z_indpro_chg12m = self._zexp(indpro_chg12m)
    z_unrate_chg12m = self._zexp(unrate_chg12m)
    sent = 0.5 * z_umcsent + 0.5 * z_umcsent_chg12m
    fund = z_indpro_chg12m - z_unrate_chg12m
    exposure = (fund - sent).clip(-2, 2)
    exposure = exposure.where(sent.notna() & fund.notna(), 0.0)
    exposure.name = self.name
    return exposure
sentiment_fundamental_gap_timingotherGap between consumer sentiment (level + 12m change) and hard macro fundamentals (unemployment rate/change,…

Gap between consumer sentiment (level + 12m change) and hard macro fundamentals (unemployment rate/change, industrial production growth), z-scored with expanding past-only moments. High sentiment relative to fundamentals signals compressed risk premia and low forward returns (behavioral/affect component isolated from the business cycle itself).

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    cols = ['macro_umcsent', 'macro_umcsent_chg12m', 'macro_unrate',
            'macro_unrate_chg12m', 'macro_indpro_chg12m']
    date_level = {}
    for c in cols:
        s = panel[c].groupby(level='date').mean()
        s = s.sort_index()
        s = s.ffill()
        date_level[c] = s
    z_umcsent = self._zx(date_level['macro_umcsent']).fillna(0.0)
    z_umcsent_chg = self._zx(date_level['macro_umcsent_chg12m']).fillna(0.0)
    z_unrate = self._zx(date_level['macro_unrate']).fillna(0.0)
    z_unrate_chg = self._zx(date_level['macro_unrate_chg12m']).fillna(0.0)
    z_indpro_chg = self._zx(date_level['macro_indpro_chg12m']).fillna(0.0)
    sentiment = 0.5 * z_umcsent + 0.5 * z_umcsent_chg
    fundamentals = 0.5 * (-z_unrate) + 0.25 * (-z_unrate_chg) + 0.25 * z_indpro_chg
    signal_val = sentiment - fundamentals
    # dates with all-NaN raw inputs should yield NaN
    raw_all_nan = pd.concat(
        [date_level[c] for c in cols], axis=1
    ).isna().all(axis=1)
    signal_val = signal_val.where(~raw_all_nan, np.nan)
    exposure = -signal_val
    exposure.name = self.name
    return exposure
sentiment_fundamental_pessimism_gap_timingotherResidual of consumer sentiment vs

Residual of consumer sentiment vs. macro-fundamentals-implied sentiment (unemployment, CPI change, oil change, industrial production change). When sentiment is unjustifiably worse than fundamentals imply, subsequent market returns tend to be higher; positioned as a contrarian macro-timing signal.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    def get_date_series(col):
        s = panel[col].groupby(level='date').mean()
        s = s.sort_index()
        s = s.ffill()
        return s
    umcsent = get_date_series('macro_umcsent')
    unrate = get_date_series('macro_unrate')
    cpi_chg12m = get_date_series('macro_cpi_chg12m')
    oil_chg12m = get_date_series('macro_oil_chg12m')
    indpro_chg12m = get_date_series('macro_indpro_chg12m')
    unrate_chg12m = get_date_series('macro_unrate_chg12m')
    def zx(s):
        mean = s.expanding(36).mean()
        std = s.expanding(36).std()
        z = (s - mean) / std
        z = z.where(std > 0, np.nan)
        return z
    z_umcsent = zx(umcsent)
    z_unrate = zx(unrate)
    z_cpi_chg12m = zx(cpi_chg12m)
    z_oil_chg12m = zx(oil_chg12m)
    z_indpro_chg12m = zx(indpro_chg12m)
    z_unrate_chg12m = zx(unrate_chg12m)
    components = pd.DataFrame({
        'unrate': z_unrate,
        'cpi_chg12m': z_cpi_chg12m,
        'oil_chg12m': z_oil_chg12m,
        'indpro_chg12m': -z_indpro_chg12m,
        'unrate_chg12m': z_unrate_chg12m,
    })
    valid_mask = components.notna().any(axis=1)
    components_filled = components.fillna(0.0)
    misery = (
        components_filled['unrate']
        + components_filled['cpi_chg12m']
        + components_filled['oil_chg12m']
        + components_filled['indpro_chg12m']
        - components_filled['unrate_chg12m']
    )
    implied_sent = -misery / 4.0
    exposure = implied_sent - z_umcsent.fillna(0.0)
    # if z_umcsent itself is NaN and no other component valid either, mark NaN
    all_nan_mask = components.isna().all(axis=1) & z_umcsent.isna()
    exposure = exposure.where(~all_nan_mask, np.nan)
    exposure = exposure.where(valid_mask | z_umcsent.notna(), np.nan)
    exposure.name = self.name
    return exposure
sentiment_gap_contrarian_timingotherContrarian macro timing signal using consumer sentiment deviation from its trailing 3-year mean…

Contrarian macro timing signal using consumer sentiment deviation from its trailing 3-year mean (countercyclical) combined with unemployment slack and rising unemployment momentum. Elevated risk aversion (low sentiment, high slack, rising unemployment) predicts higher subsequent equity risk premia.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    s = panel['macro_umcsent'].groupby(level='date').first()
    u = panel['macro_unrate'].groupby(level='date').first()
    du = panel['macro_unrate_chg12m'].groupby(level='date').first()
    s_mean = s.rolling(36, min_periods=18).mean()
    s_std = s.rolling(36, min_periods=18).std()
    sent_gap = (s - s_mean) / s_std.where(s_std > 0)
    u_mean = u.rolling(36, min_periods=18).mean()
    u_std = u.rolling(36, min_periods=18).std()
    slack = (u - u_mean) / u_std.where(u_std > 0)
    exposure = (-sent_gap + 0.5 * slack + 0.5 * du.clip(lower=0)).clip(-3, 3)
    exposure.name = self.name
    return exposure
sentiment_hard_data_divergence_timingotherContrarian macro timing signal using the divergence between household sentiment (UMich) and hard data…

Contrarian macro timing signal using the divergence between household sentiment (UMich) and hard data (industrial production, unemployment). When sentiment is high relative to hard data, expected market returns are low (euphoria priced in); when sentiment is depressed relative to solid hard data, expected returns are high (elevated risk premium). Exposure is hard_z - sent_z, expanding-window z-scored, past-only, min 36 obs.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    umcsent = panel['macro_umcsent'].groupby(level='date').mean().sort_index()
    indpro_chg = panel['macro_indpro_chg12m'].groupby(level='date').mean().sort_index()
    unrate_chg = panel['macro_unrate_chg12m'].groupby(level='date').mean().sort_index()
    umcsent = umcsent.ffill()
    indpro_chg = indpro_chg.ffill()
    unrate_chg = unrate_chg.ffill()
    sent_z = self._expanding_z(umcsent, min_obs=36)
    indpro_z = self._expanding_z(indpro_chg, min_obs=36)
    unrate_z = self._expanding_z(unrate_chg, min_obs=36)
    hard_z = 0.5 * indpro_z - 0.5 * unrate_z
    exposure = hard_z - sent_z
    exposure = exposure.clip(-2.0, 2.0)
    exposure.name = self.name
    return exposure
sentiment_misery_residual_timingotherTimes the market using the residual of consumer sentiment relative to the misery index (unemployment +…

Times the market using the residual of consumer sentiment relative to the misery index (unemployment + realized inflation). Excess pessimism relative to observable hardship (a large negative sentiment+misery residual) is a contrarian bullish signal for future market returns; unjustified optimism amid high misery is bearish.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    cpi = panel['macro_cpi'].groupby(level='date').first()
    cpi_chg12m = panel['macro_cpi_chg12m'].groupby(level='date').first()
    unrate = panel['macro_unrate'].groupby(level='date').first()
    umcsent = panel['macro_umcsent'].groupby(level='date').first()
    dates = cpi.index.sort_values()
    cpi = cpi.reindex(dates).ffill()
    cpi_chg12m = cpi_chg12m.reindex(dates).ffill()
    unrate = unrate.reindex(dates).ffill()
    umcsent = umcsent.reindex(dates).ffill()
    infl = 100.0 * cpi_chg12m / cpi.where(cpi > 0)
    misery = unrate + infl
    required_valid = umcsent.notna() & misery.notna()
    s_mean = umcsent.expanding(min_periods=36).mean()
    s_std = umcsent.expanding(min_periods=36).std()
    m_mean = misery.expanding(min_periods=36).mean()
    m_std = misery.expanding(min_periods=36).std()
    zs = (umcsent - s_mean) / s_std.where(s_std > 0)
    zm = (misery - m_mean) / m_std.where(m_std > 0)
    residual = zs + zm
    exposure = -residual
    exposure = exposure.clip(-2, 2)
    count = pd.Series(np.arange(1, len(dates) + 1), index=dates)
    has_history = count >= 36
    valid = required_valid & has_history & exposure.notna()
    exposure = exposure.where(valid, 0.0)
    exposure.name = self.name
    return exposure
stagflation_supply_shock_timingotherDecomposes oil price moves by concurrent industrial production impulse to distinguish demand-driven oil…

Decomposes oil price moves by concurrent industrial production impulse to distinguish demand-driven oil rallies (benign for equities) from adverse supply shocks (oil and breakeven inflation rising while industrial production stalls). Exposure is reduced when supply-shock stagflationary conditions are detected.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    oil_chg = panel['macro_oil_chg12m'].groupby(level='date').first().sort_index()
    breakeven_chg = panel['macro_breakeven10_chg12m'].groupby(level='date').first().sort_index()
    cpi_chg = panel['macro_cpi_chg12m'].groupby(level='date').first().sort_index()
    indpro_chg = panel['macro_indpro_chg12m'].groupby(level='date').first().sort_index()
    oil_chg = oil_chg.ffill()
    breakeven_chg = breakeven_chg.ffill()
    cpi_chg = cpi_chg.ffill()
    indpro_chg = indpro_chg.ffill()
    z_oil = self._zexp(oil_chg)
    z_breakeven = self._zexp(breakeven_chg)
    z_cpi = self._zexp(cpi_chg)
    z_indpro = self._zexp(indpro_chg)
    components = pd.concat(
        [
            0.5 * z_oil,
            0.25 * z_breakeven,
            0.25 * z_cpi,
            -0.6 * z_indpro,
        ],
        axis=1,
    )
    all_nan_mask = components.isna().all(axis=1)
    components_filled = components.fillna(0.0)
    supply_shock = components_filled.sum(axis=1)
    supply_shock[all_nan_mask] = np.nan
    exposure = -supply_shock.clip(lower=-2.5, upper=2.5)
    exposure = exposure.where(~all_nan_mask, 0.0)
    exposure.name = self.name
    return exposure
taylor_rule_policy_accommodation_timingotherRule-relative monetary policy stance: compares the effective fed funds rate to a Taylor-rule-implied rate…

Rule-relative monetary policy stance: compares the effective fed funds rate to a Taylor-rule-implied rate based on realized inflation and deviation of unemployment from its trailing 10-year average. Positive accommodation (rule rate above actual fed funds) signals abundant funding liquidity and lower discount rates, predicting higher subsequent equity market returns.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    cpi = panel['macro_cpi'].groupby(level='date').first().sort_index()
    cpi_chg = panel['macro_cpi_chg12m'].groupby(level='date').first().sort_index()
    unrate = panel['macro_unrate'].groupby(level='date').first().sort_index()
    fedfunds = panel['macro_fedfunds'].groupby(level='date').first().sort_index()
    # forward-fill isolated publication gaps (up to a few months)
    cpi = cpi.ffill(limit=3)
    cpi_chg = cpi_chg.ffill(limit=3)
    unrate = unrate.ffill(limit=3)
    fedfunds = fedfunds.ffill(limit=3)
    # inflation rate (annualized, %) from cpi level and its 12m change
    cpi_base = cpi - cpi_chg
    infl = 100.0 * cpi_chg / cpi_base.where(cpi_base > 0)
    # past-only rolling 120-month mean of unemployment, shifted 1 to avoid look-ahead
    ustar = unrate.shift(1).rolling(window=120, min_periods=36).mean()
    taylor = 2.0 + infl + 0.5 * (infl - 2.0) - 2.0 * (unrate - ustar)
    accommodation = taylor - fedfunds
    exposure = accommodation.dropna()
    exposure.index.name = 'date'
    return exposure
unemployment_news_regime_asymmetry_timingotherSign-flipping labour-market timing signal: rising unemployment is treated as good news for the market during…

Sign-flipping labour-market timing signal: rising unemployment is treated as good news for the market during expansions (implying policy easing) but as bad news during contractions. Recession state is proxied in real time by a Sahm-rule-style rolling-minimum trigger on the unemployment rate (or the NBER indicator), avoiding look-ahead from publication-lagged recession dating.

Category otherDirection High score → risk-on (raise exposure)Data macro
def compute(self, panel) -> pd.Series:
    u = panel['macro_unrate'].groupby(level='date').first()
    u_chg12m = panel['macro_unrate_chg12m'].groupby(level='date').first()
    usrec = panel['macro_usrec'].groupby(level='date').first()
    u = u.sort_index()
    u_chg12m = u_chg12m.reindex(u.index)
    usrec = usrec.reindex(u.index)
    # rolling 12-month min of past unemployment (excludes current month)
    rolling_min = u.rolling(12, closed='left').min()
    sahm_gap = u - rolling_min
    recession_state = ((sahm_gap > 0.4) | (usrec == 1)).astype(float)
    exposure = u_chg12m * (1 - 2 * recession_state)
    valid = u.notna() & u_chg12m.notna() & usrec.notna() & sahm_gap.notna()
    exposure = exposure.where(valid, np.nan)
    exposure.name = self.name
    return exposure
unemployment_trough_breakout_timingotherSahm-rule-like gap between unemployment rate and its trailing 12-month minimum, amplified when industrial…

Sahm-rule-like gap between unemployment rate and its trailing 12-month minimum, amplified when industrial production momentum is negative. Higher gap signals labor-market deterioration and impending equity drawdown; harness de-risks exposure when the raw score is large (expected_sign=-1).

Category otherDirection High score → risk-off (cut exposure)Data macro
def compute(self, panel) -> pd.Series:
    unrate = panel["macro_unrate"].groupby(level="date").first().sort_index()
    indpro_chg = panel["macro_indpro_chg12m"].groupby(level="date").first().sort_index()
    unrate = unrate.ffill()
    indpro_chg = indpro_chg.ffill()
    rolling_min = unrate.rolling(window=12, min_periods=6).min()
    gap = unrate - rolling_min
    slow = (indpro_chg < 0).astype(float)
    raw = gap * (1.0 + slow)
    missing_mask = unrate.isna() | indpro_chg.isna()
    raw = raw.where(~missing_mask, np.nan)
    return raw
valuation_dispersion_market_timingotherCross-sectional dispersion of log(P/B) across the equity universe, measured as the 90th minus 10th percentile…

Cross-sectional dispersion of log(P/B) across the equity universe, measured as the 90th minus 10th percentile each month. Wide dispersion signals speculative excess/disagreement and historically precedes weak aggregate returns; the signal is dispersion minus its trailing 36-month (past-only) mean, so high (wide) dispersion implies reduced market exposure.

Category otherDirection High score → risk-off (cut exposure)Data price, fundamental
def compute(self, panel) -> pd.Series:
    df = panel[['pb', 'marketcap']].copy()
    df = df[(df['pb'] > 0) & (df['marketcap'] > 0)]
    df['x'] = np.log(df['pb'])
    def disp_fn(group):
        if len(group) < 200:
            return np.nan
        p90 = group.quantile(0.90)
        p10 = group.quantile(0.10)
        return p90 - p10
    disp = df.groupby(level='date')['x'].apply(disp_fn)
    disp = disp.sort_index()
    rolling_mean = disp.rolling(window=36, min_periods=24).mean()
    # ensure past-only: shift by 1 so current month's disp is not in its own rolling mean
    rolling_mean = rolling_mean.shift(1)
    signal = disp - rolling_mean
    signal = signal.reindex(disp.index)
    return signal
variance_risk_premium_timingotherVariance risk premium timing signal: gap between implied volatility (VIX) and realized equity volatility…

Variance risk premium timing signal: gap between implied volatility (VIX) and realized equity volatility (cross-sectional median of trailing 1y stock-level realized vol), smoothed with a 3-month rolling mean. A wide gap indicates elevated compensation for bearing variance risk, forecasting higher subsequent market excess returns.

Category otherDirection High score → risk-on (raise exposure)Data macro, price
def compute(self, panel) -> pd.Series:
    vix = panel['macro_vix'].groupby(level='date').first()
    rv = panel['vol_252'].groupby(level='date').median() * 100.0
    vrp = vix - rv
    raw = vrp.rolling(3, min_periods=1).mean()
    return raw
aggregate_accruals_market_timingqualityAggregate accruals (netinc - ncfo)/assets, median across large liquid names, as a macro earnings-quality…

Aggregate accruals (netinc - ncfo)/assets, median across large liquid names, as a macro earnings-quality timing signal; high aggregate accruals predict low subsequent market returns, so exposure = -zscore of smoothed aggregate accruals.

Category qualityDirection High score → risk-on (raise exposure)Data fundamental, price
def compute(self, panel) -> pd.Series:
    df = panel[['netinc', 'ncfo', 'assets', 'marketcap']].copy()
    valid = (
        df['assets'].notna() & (df['assets'] > 0) &
        df['netinc'].notna() & df['ncfo'].notna() &
        df['marketcap'].notna()
    )
    df = df[valid]
    acc = (df['netinc'] - df['ncfo']) / df['assets']
    acc = acc.clip(-0.5, 0.5)
    df['acc'] = acc
    # market-cap 40th percentile filter, per date
    pct40 = df.groupby(level='date')['marketcap'].transform(
        lambda x: x.quantile(0.40)
    )
    mask = df['marketcap'] >= pct40
    df = df[mask]
    # count valid names per date
    counts = df.groupby(level='date')['acc'].count()
    agg_t = df.groupby(level='date')['acc'].median()
    agg_t = agg_t.where(counts >= self.MIN_NAMES, np.nan)
    agg_t = agg_t.sort_index()
    smooth = agg_t.rolling(3, min_periods=3).mean()
    exp_mean = smooth.expanding(36).mean()
    exp_std = smooth.expanding(36).std()
    z = (smooth - exp_mean) / exp_std
    z = z.where(exp_std > 0, np.nan)
    exposure = -z
    exposure = exposure.clip(-2, 2)
    exposure.name = self.name
    return exposure
aggregate_earnings_growth_breadth_timingqualityTiming signal based on the cross-sectional breadth of YoY earnings growth among top liquid-cap firms

Timing signal based on the cross-sectional breadth of YoY earnings growth among top liquid-cap firms. Breadth is the fraction of firms with positive YoY net income growth; the signal is the deviation of current breadth from its trailing 12m average, deviation-detrended breadth. Wide breadth (broad earnings improvement) historically precedes weak subsequent market returns (Kothari, Lewellen, Warner), so expected_sign=-1.

Category qualityDirection High score → risk-off (cut exposure)Data fundamental, price
def compute(self, panel) -> pd.Series:
    netinc = panel['netinc']
    marketcap = panel['marketcap']
    ni_lag = netinc.groupby(level='ticker').shift(12)
    valid = (
        netinc.notna() & ni_lag.notna()
        & (ni_lag != 0)
        & marketcap.notna() & (marketcap > 0)
    )
    # top liquid universe: top 50% by marketcap within each date among valid entries
    mc_valid = marketcap.where(valid)
    mc_rank = mc_valid.groupby(level='date').rank(pct=True, ascending=True)
    top_liquid = valid & (mc_rank >= 0.5)
    growth = (netinc - ni_lag) / ni_lag.abs()
    imp = (growth > 0).astype(float)
    imp = imp.where(top_liquid)
    breadth_t = imp.groupby(level='date').mean()
    rolling_mean = breadth_t.rolling(12, min_periods=6).mean()
    raw_t = breadth_t - rolling_mean
    return raw_t
money_illusion_real_yield_gap_timingvalueModigliani-Cohn money illusion timing signal: compares aggregate market earnings yield (median of 1/pe among…

Modigliani-Cohn money illusion timing signal: compares aggregate market earnings yield (median of 1/pe among top-70% marketcap stocks) to the REAL 10y Treasury yield (nominal 10y minus trailing CPI inflation). A wide gap signals equities are irrationally cheap relative to real cash-flow discounting, forecasting high subsequent market returns as the illusion unwinds.

Category valueDirection High score → risk-on (raise exposure)Data price, fundamental, macro
def compute(self, panel) -> pd.Series:
    df = panel[['pe', 'marketcap', 'macro_dgs10', 'macro_cpi', 'macro_cpi_chg12m']].copy()
    valid = (df['pe'] > 0) & df['marketcap'].notna() & (df['marketcap'] > 0)
    df = df[valid]
    # top 70% marketcap per date
    def top70_mask(x):
        thresh = x.quantile(0.30)
        return x >= thresh
    mc = df['marketcap']
    thresh_by_date = mc.groupby(level='date').transform(lambda x: x.quantile(0.30))
    top_df = df[mc >= thresh_by_date]
    ey = 1.0 / top_df['pe']
    agg_ey = ey.groupby(level='date').median()
    macro = panel[['macro_dgs10', 'macro_cpi', 'macro_cpi_chg12m']].groupby(level='date').mean()
    denom = macro['macro_cpi'] - macro['macro_cpi_chg12m']
    valid_macro = macro['macro_dgs10'].notna() & macro['macro_cpi'].notna() & \
        macro['macro_cpi_chg12m'].notna() & (denom > 0)
    infl = 100.0 * macro['macro_cpi_chg12m'] / denom
    real_y = macro['macro_dgs10'] - infl
    signal = 100.0 * agg_ey - real_y
    signal = signal.where(valid_macro)
    signal = signal.dropna()
    signal.index.name = 'date'
    return signal

Research, not investment advice. Data ends around February 2024. Transaction costs use a simple per-unit-traded model with no market-impact term, so capacity is not addressed. Delisting returns are approximated rather than sourced from the corporate-actions table. Every number here is a backtest.

Source Code
---
title: "LLM Alpha Farming"
description: "Can you farm tradable equity signals out of a language model? Three agents propose, implement and document cross-sectional alpha; one deterministic harness judges them all — and discounts every result for the size of the search."
date: 2026-10-02
date-format: long
categories: [finance, quant, ai-agent, llm]
execute:
  echo: false
  freeze: false
format:
  html:
    page-layout: full
    toc: true
    toc-depth: 2
---


<style>
.la-hero{background:linear-gradient(135deg,#1a1a2e 0%,#0f3460 100%);color:#fff;
  border-radius:14px;padding:2.5rem 2rem;margin:0 0 2rem}
.la-hero h2{color:#fff;margin:0 0 .4rem;font-size:1.9rem;border:0}
.la-hero .la-sub{opacity:.85;font-weight:300;font-size:1.05rem;margin-bottom:1.25rem}
.la-hero .la-meta{display:flex;gap:.6rem;flex-wrap:wrap}
.la-hero .la-chip{background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.28);
  padding:.32rem .85rem;border-radius:2rem;font-size:.8rem;font-weight:500}
.la-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
  gap:1rem;margin:1.5rem 0 2rem}
.la-metric{background:#fff;border:1px solid #e9ecef;border-radius:10px;padding:1.15rem;text-align:center}
.la-metric .v{font-size:1.7rem;font-weight:700;color:#1a1a2e;line-height:1.15}
.la-metric .l{font-size:.72rem;color:#6c757d;text-transform:uppercase;letter-spacing:.05em;margin-top:.3rem}
.la-metric.good .v{color:#198754}
.la-flow{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.9rem;margin:1.5rem 0}
.la-step{background:#fff;border:1px solid #e9ecef;border-left:4px solid #0f3460;
  border-radius:10px;padding:1.1rem}
.la-step .n{font-family:'JetBrains Mono',monospace;font-size:.7rem;color:#e94560;font-weight:600}
.la-step h4{margin:.35rem 0 .45rem;font-size:.98rem;color:#1a1a2e}
.la-step p{margin:0;font-size:.86rem;color:#6c757d;line-height:1.45}
.la-step .m{display:inline-block;margin-top:.6rem;font-family:'JetBrains Mono',monospace;
  font-size:.7rem;background:#f8f9fa;color:#0f3460;padding:.15rem .5rem;border-radius:1rem}
.la-table{width:100%;border-collapse:collapse;font-size:.87rem;margin:1rem 0}
.la-table th{text-align:left;font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;
  color:#6c757d;border-bottom:2px solid #e9ecef;padding:.55rem .5rem;font-weight:600}
.la-table td{padding:.55rem .5rem;border-bottom:1px solid #f1f3f5;vertical-align:middle}
.la-table tr:hover td{background:#fbfcfd}
.la-table .sig code{font-family:'JetBrains Mono',monospace;font-size:.82rem;
  background:#f8f9fa;color:#1a1a2e;padding:.15rem .45rem;border-radius:4px;font-weight:600}
.la-table .mono{font-family:'JetBrains Mono',monospace;font-size:.83rem}
.la-pill{display:inline-block;padding:.16rem .6rem;border-radius:1rem;font-size:.7rem;
  font-weight:600;color:#fff;white-space:nowrap}
.la-bar{display:inline-flex;align-items:center;gap:.45rem}
.la-bar-fill{display:inline-block;width:var(--w);height:8px;border-radius:4px;background:var(--c);min-width:2px}
.la-bar-num{font-family:'JetBrains Mono',monospace;font-size:.78rem;color:#495057}
.la-dash{color:#adb5bd}
.la-table td.la-err{color:#6c757d;font-style:italic;font-size:.82rem}
.la-note{background:#f8f9fa;border-left:4px solid #e94560;border-radius:8px;
  padding:1rem 1.15rem;margin:1.5rem 0;font-size:.9rem}
.la-note strong{color:#1a1a2e}
.la-tablenote{font-size:.82rem;color:#6c757d;margin:-.4rem 0 1.5rem}
.la-tablenote a{color:#0f3460}
.la-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.la-details{border:1px solid #e9ecef;border-radius:12px;margin:1.5rem 0;background:#fff;overflow:hidden}
.la-details>summary{cursor:pointer;padding:1.05rem 1.35rem;font-weight:600;color:#1a1a2e;
  list-style:none;display:flex;align-items:center;gap:.65rem;background:#fbfcfd;
  border-bottom:1px solid transparent}
.la-details>summary::-webkit-details-marker{display:none}
.la-details>summary::before{content:"\25B8";color:#e94560;font-size:1.05rem;
  display:inline-block;transition:transform .18s ease}
.la-details[open]>summary::before{transform:rotate(90deg)}
.la-details[open]>summary{border-bottom-color:#e9ecef}
.la-details>summary:hover{background:#f4f6f8}
.la-details .hint{font-weight:400;color:#6c757d;font-size:.82rem;margin-left:auto}
.la-body{padding:.25rem 1.35rem 1.35rem}
.la-cmp{width:100%;border-collapse:collapse;font-size:.88rem;margin:1rem 0}
.la-cmp th{text-align:right;font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;
  color:#6c757d;border-bottom:2px solid #e9ecef;padding:.5rem .6rem;font-weight:600}
.la-cmp th:first-child{text-align:left}
.la-cmp td{padding:.5rem .6rem;border-bottom:1px solid #f1f3f5;text-align:right;
  font-family:'JetBrains Mono',monospace;font-size:.84rem}
.la-cmp td:first-child{text-align:left;font-family:inherit;color:#495057}
.la-cmp .win{color:#198754;font-weight:600}
.la-cmp .lose{color:#b02a37}
.la-gloss dt{font-weight:600;color:#1a1a2e;margin-top:1.15rem;font-size:.94rem}
.la-gloss dd{margin:.35rem 0 0;color:#495057;font-size:.9rem;line-height:1.62}
.la-formula{font-family:'JetBrains Mono',monospace;background:#f8f9fa;border-left:3px solid #0f3460;
  padding:.5rem .8rem;border-radius:4px;font-size:.8rem;color:#0f3460;margin:.55rem 0;
  display:block;overflow-x:auto;white-space:pre}
.la-guard{font-size:.82rem;color:#6c757d;margin-top:.35rem}
.la-guard b{color:#b02a37;font-weight:600}
.la-next{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin:1.5rem 0}
.la-card{background:#fff;border:1px solid #e9ecef;border-radius:10px;padding:1.25rem}
.la-card h4{margin:0 0 .5rem;font-size:.95rem;color:#1a1a2e}
.la-card p{margin:0 0 .6rem;font-size:.86rem;color:#6c757d;line-height:1.5}
.la-card .eff{font-size:.72rem;font-family:'JetBrains Mono',monospace;color:#0f3460;
  background:#f8f9fa;padding:.15rem .5rem;border-radius:1rem}
.la-lib-intro{font-size:.9rem;color:#495057;line-height:1.6}
.la-group{margin:1.6rem 0 .2rem;font-size:.95rem;color:#1a1a2e}
.la-group:first-of-type{margin-top:.6rem}
.la-group-note{margin:.15rem 0 .9rem;font-size:.83rem;color:#6c757d;line-height:1.5}
.la-sig{border:1px solid #eef0f2;border-radius:9px;margin:.5rem 0;background:#fff;overflow:hidden}
.la-sig>summary{cursor:pointer;padding:.65rem .9rem;list-style:none;display:flex;
  align-items:baseline;gap:.55rem;flex-wrap:wrap;font-size:.87rem}
.la-sig>summary::-webkit-details-marker{display:none}
.la-sig>summary::before{content:"B8";color:#e94560;font-size:.9rem;
  display:inline-block;transition:transform .18s ease;align-self:center}
.la-sig[open]>summary::before{transform:rotate(90deg)}
.la-sig>summary:hover{background:#fbfcfd}
.la-sig>summary code{font-family:'JetBrains Mono',monospace;font-size:.82rem;
  background:#f8f9fa;color:#1a1a2e;padding:.15rem .45rem;border-radius:4px;font-weight:600}
.la-sig .gist{color:#6c757d;font-size:.82rem;flex:1 1 14rem;min-width:0}
.la-tag{font-size:.66rem;text-transform:uppercase;letter-spacing:.04em;font-weight:600;
  color:#0f3460;background:#eef2f7;border-radius:1rem;padding:.12rem .5rem;white-space:nowrap}
.la-sig-body{padding:.1rem .95rem 1rem;border-top:1px solid #f1f3f5}
.la-sig-body p{margin:.8rem 0 .6rem;font-size:.87rem;color:#495057;line-height:1.6}
.la-meta{display:flex;gap:.4rem;flex-wrap:wrap;margin:.55rem 0}
.la-meta span{font-size:.7rem;color:#495057;background:#f8f9fa;border:1px solid #eef0f2;
  border-radius:1rem;padding:.14rem .55rem}
.la-meta b{color:#1a1a2e;font-weight:600}
.la-code{font-family:'JetBrains Mono',monospace;font-size:.76rem;line-height:1.5;
  background:#f8f9fa;border:1px solid #eef0f2;border-left:3px solid #0f3460;border-radius:6px;
  padding:.7rem .85rem;margin:.55rem 0 0;color:#1a1a2e;overflow:auto;max-height:26rem;
  white-space:pre;-webkit-overflow-scrolling:touch}
@media (max-width:768px){.la-hero{padding:1.75rem 1.25rem}.la-table{font-size:.8rem}}
</style>


<div class="la-hero">
  <h2>Farming alpha out of a language model</h2>
  <div class="la-sub">Three cooperating agents invent, implement and document equity
    signals. One deterministic harness judges every one of them under identical,
    point-in-time-correct rules — and then discounts the result for the size of the search.</div>
  <div class="la-meta"><span class="la-chip">Run 2026-10-02</span><span class="la-chip">171 signals evaluated</span><span class="la-chip">280 months of history</span><span class="la-chip">API spend $0.36</span></div>
</div>

<div class="la-metrics"><div class="la-metric "><div class="v">171</div><div class="l">Signals evaluated</div></div><div class="la-metric "><div class="v">0</div><div class="l">Survive correction</div></div><div class="la-metric "><div class="v">1.01</div><div class="l">Search hurdle (Sharpe)</div></div><div class="la-metric "><div class="v">210</div><div class="l">Trials priced in</div></div><div class="la-metric "><div class="v">0.92</div><div class="l">Best raw Sharpe</div></div><div class="la-metric "><div class="v">4.26</div><div class="l">Best alpha t-stat</div></div></div>

## How the pipeline works

The premise is a meta-experiment: generate hypotheses with the most capable model
available, implement them with a cheaper one, and measure how many survive a
ruthless, uniform backtest. The interesting number was never the best Sharpe — it
is the **survival rate**, and that only means something if the judging is identical
for every candidate and honest about the size of the search.

<details class="la-details">
<summary>The six stages, end to end <span class="hint">click to expand</span></summary>
<div class="la-body">
<div class="la-flow"><div class="la-step"><div class="n">01</div><h4>Idea agent</h4><p>Mines the asset-pricing literature for an economically motivated hypothesis that can actually be built from the panel — with a stated mechanism, a formula, an expected sign, and references.</p><span class="m">Opus 5</span></div><div class="la-step"><div class="n">02</div><h4>Data agent</h4><p>Maps each requested concept onto a real panel column and gates on coverage. An idea that needs data we do not have dies here, cheaply, before any code is written.</p><span class="m">Sonnet 5</span></div><div class="la-step"><div class="n">03</div><h4>Coder agent</h4><p>Writes a self-contained module implementing the signal interface, then self-corrects against the validator's complaints. Generated code is untrusted: a static check blocks file, network and process access before the module is ever imported.</p><span class="m">Sonnet 5</span></div><div class="la-step"><div class="n">04</div><h4>Shared harness</h4><p>Monthly cross-sectional decile long/short on a survivorship-safe, liquidity-filtered universe. Identical for every signal — invented or canonical — so nothing gets a bespoke advantage.</p><span class="m">deterministic</span></div><div class="la-step"><div class="n">05</div><h4>Robustness battery</h4><p>Subperiod stability, transaction-cost sweep, sector-neutral variant, parameter and universe sensitivity, and holding-horizon decay.</p><span class="m">deterministic</span></div><div class="la-step"><div class="n">06</div><h4>Selection-bias layer</h4><p>Factor attribution against market, size, value and momentum; Benjamini-Hochberg across the library; and a deflated Sharpe that prices in how many ideas were tried to find this one.</p><span class="m">deterministic</span></div></div>

<div class="la-note">
<strong>Everything is point-in-time.</strong> Fundamentals join as of the SEC filing
date, never the fiscal period. The universe keeps delisted names. Signals must pass a
no-look-ahead validator: recomputing on a time-truncated panel may not change any
earlier value, or even its missing/not-missing status. A name that leaves the panel
books a delisting return rather than silently disappearing from the cross-section —
otherwise the worst outcomes quietly vanish from whichever decile was holding them.
</div>
</div>
</details>


## Results

*Last refreshed 2026-10-02 — regenerated automatically on every pipeline run.*


<table class="la-table">
<thead><tr>
  <th>Signal</th><th>Verdict</th><th>Sharpe</th>
  <th>Alpha t</th><th>Factor R²</th><th>Deflated Sharpe</th>
</tr></thead>
<tbody><tr><td class="sig"><code>gross_profitability</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.92</td><td class="mono">3.22</td><td class="mono">21%</td><td><span class="la-bar" style="--w:34.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.35</span></span></td></tr><tr><td class="sig"><code>payout_funding_gap</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.82</td><td class="mono">3.55</td><td class="mono">42%</td><td><span class="la-bar" style="--w:18.4px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.19</span></span></td></tr><tr><td class="sig"><code>sbc_adjusted_cashflow_yield</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.78</td><td class="mono">3.30</td><td class="mono">55%</td><td><span class="la-bar" style="--w:12.3px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.13</span></span></td></tr><tr><td class="sig"><code>net_share_issuance</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.73</td><td class="mono">4.26</td><td class="mono">59%</td><td><span class="la-bar" style="--w:8.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.08</span></span></td></tr><tr><td class="sig"><code>macro_systematic_variance_share</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.57</td><td class="mono">1.78</td><td class="mono">53%</td><td><span class="la-bar" style="--w:3.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.03</span></span></td></tr><tr><td class="sig"><code>cross_sectional_beta_dispersion_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.51</td><td class="mono">1.10</td><td class="mono">39%</td><td><span class="la-bar" style="--w:0.9px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td></tr><tr><td class="sig"><code>late_quarter_buyback_share_shrinkage</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.56</td><td class="mono">3.07</td><td class="mono">70%</td><td><span class="la-bar" style="--w:0.8px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td></tr><tr><td class="sig"><code>trade_credit_stretch_quality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.51</td><td class="mono">2.60</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.7px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td></tr><tr><td class="sig"><code>stagflation_supply_shock_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.52</td><td class="mono">2.59</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.6px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td></tr><tr><td class="sig"><code>predicted_dividend_month_premium</code></td><td><span class="la-pill" style="background:#e94560">Loses to factors</span></td><td class="mono">0.41</td><td class="mono">-4.06</td><td class="mono">95%</td><td><span class="la-bar" style="--w:0.3px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td></tr></tbody>
</table>
<p class="la-tablenote">The 10 best of 171 signals, ranked by deflated Sharpe. <a href="#all-results">All 171, with confidence intervals, are further down.</a></p>

<div class="la-note">
<strong>Why the board is not sorted by Sharpe.</strong> Farm enough strategies and one
will post a fine Sharpe by luck alone. With 210 trials priced in,
the expected best-of-search Sharpe — the <em>hurdle</em> — is
<strong>1.01</strong>. Anything below that is
indistinguishable from noise no matter how good the raw number looks.
<br><br>
<strong>Alpha t</strong> is the return left after regressing on market, size, value and
momentum, with Newey-West standard errors. A high Sharpe next to a high <strong>Factor
R²</strong> and a flat alpha t means the strategy is a repackaged factor tilt, not a
discovery. The reverse also happens: a modest Sharpe with a strong alpha t is worth
more than it first looks.
<br><br>
<strong>Deflated Sharpe</strong> is the probability the signal is real once the size of
the search is accounted for; <strong>q-value</strong> is the Benjamini-Hochberg
false-discovery rate across the whole library.
</div>


![Net long/short equity curves for the signals that survived multiple-testing correction. Log scale — a straight line is a constant compounding rate.](figures/equity.png){fig-alt="Equity curves of surviving alpha signals"}

## What this run found

- No signal in this run survives correction. With a hurdle Sharpe of 1.01, nothing on the board is currently distinguishable from the best of a random search of the same size — which is itself the honest headline, not a failure of the machinery.

- 10 are factor tilts in disguise. The clearest case is <code>gradual_information_momentum</code>, whose returns are 93% explained by the standard factors with an alpha t-stat of -0.83 — it is not adding anything the four-factor model does not already capture.

- <code>excess_speculative_volatility</code> is the case for looking past raw Sharpe: only 0.29 on its own, but an alpha t-stat of 2.95 once the known factors are stripped out — its raw performance understates it.

- <code>predicted_dividend_month_premium</code> is the inverse warning: it tracks the standard factors closely (95% of variance) yet delivers significantly *negative* alpha against them (t=-4.06). On this universe and sample you would have been better off holding the plain factor than this construction of it.


<details class="la-details" id="all-results">
<summary>All 171 signals, with 95% confidence intervals
<span class="hint">click to expand</span></summary>
<div class="la-body">
<div class="la-scroll">
<table class="la-table">
<thead><tr>
  <th>Signal</th><th>Verdict</th><th>Sharpe</th><th>95% CI</th>
  <th>Alpha t</th><th>Factor R²</th><th>Deflated Sharpe</th><th>q-value</th>
</tr></thead>
<tbody><tr><td class="sig"><code>gross_profitability</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.92</td><td class="mono">[0.51, 1.36]</td><td class="mono">3.22</td><td class="mono">21%</td><td><span class="la-bar" style="--w:34.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.35</span></span></td><td class="mono">0.002</td></tr><tr><td class="sig"><code>payout_funding_gap</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.82</td><td class="mono">[0.40, 1.28]</td><td class="mono">3.55</td><td class="mono">42%</td><td><span class="la-bar" style="--w:18.4px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.19</span></span></td><td class="mono">0.011</td></tr><tr><td class="sig"><code>sbc_adjusted_cashflow_yield</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.78</td><td class="mono">[0.38, 1.19]</td><td class="mono">3.30</td><td class="mono">55%</td><td><span class="la-bar" style="--w:12.3px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.13</span></span></td><td class="mono">0.015</td></tr><tr><td class="sig"><code>net_share_issuance</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.73</td><td class="mono">[0.36, 1.09]</td><td class="mono">4.26</td><td class="mono">59%</td><td><span class="la-bar" style="--w:8.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.08</span></span></td><td class="mono">0.015</td></tr><tr><td class="sig"><code>macro_systematic_variance_share</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.57</td><td class="mono">[0.11, 0.98]</td><td class="mono">1.78</td><td class="mono">53%</td><td><span class="la-bar" style="--w:3.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.03</span></span></td><td class="mono">0.081</td></tr><tr><td class="sig"><code>cross_sectional_beta_dispersion_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.51</td><td class="mono">[0.07, 1.00]</td><td class="mono">1.10</td><td class="mono">39%</td><td><span class="la-bar" style="--w:0.9px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td><td class="mono">0.132</td></tr><tr><td class="sig"><code>late_quarter_buyback_share_shrinkage</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.56</td><td class="mono">[0.20, 0.88]</td><td class="mono">3.07</td><td class="mono">70%</td><td><span class="la-bar" style="--w:0.8px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td><td class="mono">0.061</td></tr><tr><td class="sig"><code>trade_credit_stretch_quality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.51</td><td class="mono">[0.10, 0.89]</td><td class="mono">2.60</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.7px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td><td class="mono">0.081</td></tr><tr><td class="sig"><code>stagflation_supply_shock_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.52</td><td class="mono">[0.12, 0.91]</td><td class="mono">2.59</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.6px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.01</span></span></td><td class="mono">0.090</td></tr><tr><td class="sig"><code>predicted_dividend_month_premium</code></td><td><span class="la-pill" style="background:#e94560">Loses to factors</span></td><td class="mono">0.41</td><td class="mono">[-0.00, 0.89]</td><td class="mono">-4.06</td><td class="mono">95%</td><td><span class="la-bar" style="--w:0.3px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.204</td></tr><tr><td class="sig"><code>aggregate_accruals_market_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.37</td><td class="mono">[-0.07, 0.78]</td><td class="mono">1.70</td><td class="mono">14%</td><td><span class="la-bar" style="--w:0.2px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.373</td></tr><tr><td class="sig"><code>asset_growth</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.43</td><td class="mono">[0.02, 0.81]</td><td class="mono">2.27</td><td class="mono">45%</td><td><span class="la-bar" style="--w:0.2px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.204</td></tr><tr><td class="sig"><code>tax_loss_selling_turn_of_year_pressure</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.58</td><td class="mono">[-1.48, 0.18]</td><td class="mono">-0.73</td><td class="mono">8%</td><td><span class="la-bar" style="--w:0.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.374</td></tr><tr><td class="sig"><code>average_systematic_comovement_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.36</td><td class="mono">[-0.12, 0.88]</td><td class="mono">0.25</td><td class="mono">39%</td><td><span class="la-bar" style="--w:0.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.425</td></tr><tr><td class="sig"><code>operating_leverage_industrial_cycle</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.39</td><td class="mono">[-0.07, 0.81]</td><td class="mono">1.75</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.335</td></tr><tr><td class="sig"><code>retained_vs_contributed_capital_value</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.38</td><td class="mono">[0.01, 0.75]</td><td class="mono">1.76</td><td class="mono">65%</td><td><span class="la-bar" style="--w:0.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.230</td></tr><tr><td class="sig"><code>frog_in_the_pan_gradual_momentum</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.33</td><td class="mono">[-0.10, 0.76]</td><td class="mono">1.07</td><td class="mono">90%</td><td><span class="la-bar" style="--w:0.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.373</td></tr><tr><td class="sig"><code>lottery_demand_sentiment_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.31</td><td class="mono">[-0.15, 0.79]</td><td class="mono">1.08</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.1px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.491</td></tr><tr><td class="sig"><code>earnings_yield</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.34</td><td class="mono">[-0.10, 0.80]</td><td class="mono">-0.00</td><td class="mono">62%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.416</td></tr><tr><td class="sig"><code>cash_backed_earnings_surprise</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.28</td><td class="mono">[-0.13, 0.69]</td><td class="mono">0.81</td><td class="mono">57%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.441</td></tr><tr><td class="sig"><code>sentiment_fundamental_pessimism_gap_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.25</td><td class="mono">[-0.26, 0.72]</td><td class="mono">0.45</td><td class="mono">12%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.656</td></tr><tr><td class="sig"><code>momentum_12_1</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.27</td><td class="mono">[-0.16, 0.73]</td><td class="mono">-0.10</td><td class="mono">92%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.491</td></tr><tr><td class="sig"><code>excess_speculative_volatility</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.29</td><td class="mono">[-0.09, 0.67]</td><td class="mono">2.95</td><td class="mono">50%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.391</td></tr><tr><td class="sig"><code>equity_rate_correlation_regime_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.25</td><td class="mono">[-0.17, 0.73]</td><td class="mono">0.18</td><td class="mono">26%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.592</td></tr><tr><td class="sig"><code>inventory_growth_gap</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.26</td><td class="mono">[-0.14, 0.70]</td><td class="mono">0.22</td><td class="mono">25%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.484</td></tr><tr><td class="sig"><code>gradual_information_momentum</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.25</td><td class="mono">[-0.17, 0.69]</td><td class="mono">-0.83</td><td class="mono">93%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.543</td></tr><tr><td class="sig"><code>equity_yield_vs_real_rate_gap_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.18</td><td class="mono">[-0.31, 0.70]</td><td class="mono">-0.75</td><td class="mono">38%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.786</td></tr><tr><td class="sig"><code>inflation_passthrough_margin_expansion</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.23</td><td class="mono">[-0.20, 0.69]</td><td class="mono">0.75</td><td class="mono">27%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.592</td></tr><tr><td class="sig"><code>profitless_market_cap_share_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.18</td><td class="mono">[-0.42, 0.77]</td><td class="mono">0.19</td><td class="mono">18%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>sticky_cost_downturn_drag</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.24</td><td class="mono">[-0.14, 0.62]</td><td class="mono">-0.15</td><td class="mono">42%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.506</td></tr><tr><td class="sig"><code>quiet_strength_low_attention_high</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.22</td><td class="mono">[-0.20, 0.65]</td><td class="mono">1.59</td><td class="mono">85%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.592</td></tr><tr><td class="sig"><code>low_volatility</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.25</td><td class="mono">[-0.15, 0.65]</td><td class="mono">2.39</td><td class="mono">83%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.507</td></tr><tr><td class="sig"><code>real_yield_shock_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.13</td><td class="mono">[-0.27, 0.57]</td><td class="mono">0.48</td><td class="mono">2%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>aggregate_rollover_risk_funding_shock_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.17</td><td class="mono">[-0.35, 0.68]</td><td class="mono">0.45</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>aggregate_equity_issuance_market_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.20</td><td class="mono">[-0.34, 0.70]</td><td class="mono">1.11</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.786</td></tr><tr><td class="sig"><code>financial_slack_under_tightening</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.21</td><td class="mono">[-0.17, 0.60]</td><td class="mono">0.38</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.592</td></tr><tr><td class="sig"><code>tax_loss_selling_seasonal_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.57</td><td class="mono">[-1.39, 0.23]</td><td class="mono">-1.06</td><td class="mono">19%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.433</td></tr><tr><td class="sig"><code>credit_vol_complacency_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.12</td><td class="mono">[-0.21, 0.51]</td><td class="mono">-0.47</td><td class="mono">17%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>depreciation_conservatism_reserve</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.20</td><td class="mono">[-0.27, 0.65]</td><td class="mono">0.77</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.745</td></tr><tr><td class="sig"><code>sentiment_misery_residual_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.15</td><td class="mono">[-0.28, 0.55]</td><td class="mono">0.89</td><td class="mono">9%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.786</td></tr><tr><td class="sig"><code>seasonal_risk_appetite_credit_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.20</td><td class="mono">[-0.19, 0.52]</td><td class="mono">1.41</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.649</td></tr><tr><td class="sig"><code>accruals</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.18</td><td class="mono">[-0.25, 0.64]</td><td class="mono">1.96</td><td class="mono">21%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.745</td></tr><tr><td class="sig"><code>cash_vs_accounting_value_divergence</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.19</td><td class="mono">[-0.25, 0.64]</td><td class="mono">1.90</td><td class="mono">26%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.746</td></tr><tr><td class="sig"><code>residual_vix_hedge_premium</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.18</td><td class="mono">[-0.25, 0.61]</td><td class="mono">1.01</td><td class="mono">15%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.720</td></tr><tr><td class="sig"><code>equity_fear_vs_credit_stress_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.09</td><td class="mono">[-0.25, 0.47]</td><td class="mono">-1.11</td><td class="mono">25%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>macro_beta_rotation_instability</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.15</td><td class="mono">[-0.24, 0.54]</td><td class="mono">1.20</td><td class="mono">65%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.786</td></tr><tr><td class="sig"><code>effective_tax_rate_earnings_quality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.15</td><td class="mono">[-0.20, 0.51]</td><td class="mono">0.15</td><td class="mono">6%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.781</td></tr><tr><td class="sig"><code>sentiment_fundamental_gap_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.11</td><td class="mono">[-0.33, 0.54]</td><td class="mono">0.73</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>market_breadth_high_ratio_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.12</td><td class="mono">[-0.24, 0.50]</td><td class="mono">2.64</td><td class="mono">24%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>macro_exposure_drift</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.13</td><td class="mono">[-0.26, 0.51]</td><td class="mono">1.04</td><td class="mono">66%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>equity_duration_rate_trend_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.13</td><td class="mono">[-0.37, 0.68]</td><td class="mono">1.28</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.858</td></tr><tr><td class="sig"><code>macro_trend_exposure_alignment</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.01</td><td class="mono">[-0.47, 0.48]</td><td class="mono">0.15</td><td class="mono">24%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.979</td></tr><tr><td class="sig"><code>unproductive_acquisition_growth</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.12</td><td class="mono">[-0.31, 0.58]</td><td class="mono">-0.27</td><td class="mono">13%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.838</td></tr><tr><td class="sig"><code>credit_equity_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.08</td><td class="mono">[-0.39, 0.48]</td><td class="mono">0.21</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>credit_spread_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.09</td><td class="mono">[-0.40, 0.52]</td><td class="mono">0.60</td><td class="mono">15%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>market_trend_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.06</td><td class="mono">[-0.43, 0.53]</td><td class="mono">1.17</td><td class="mono">36%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.919</td></tr><tr><td class="sig"><code>macro_beta_instability</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.10</td><td class="mono">[-0.27, 0.45]</td><td class="mono">1.38</td><td class="mono">72%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.864</td></tr><tr><td class="sig"><code>credit_beta_defensive</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.11</td><td class="mono">[-0.29, 0.44]</td><td class="mono">0.25</td><td class="mono">34%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.833</td></tr><tr><td class="sig"><code>oil_beta_trend_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.10</td><td class="mono">[-0.33, 0.50]</td><td class="mono">-0.49</td><td class="mono">16%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>hidden_dilution_sbc_yield</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.10</td><td class="mono">[-0.32, 0.50]</td><td class="mono">-0.82</td><td class="mono">75%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>book_to_market</code></td><td><span class="la-pill" style="background:#e94560">Loses to factors</span></td><td class="mono">0.11</td><td class="mono">[-0.36, 0.58]</td><td class="mono">-3.14</td><td class="mono">92%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>sbc_inflated_operating_cash_flow</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.10</td><td class="mono">[-0.37, 0.55]</td><td class="mono">-1.13</td><td class="mono">53%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>macro_momentum_diffusion_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.06</td><td class="mono">[-0.46, 0.59]</td><td class="mono">1.02</td><td class="mono">29%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.930</td></tr><tr><td class="sig"><code>money_illusion_real_yield_gap_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.05</td><td class="mono">[-0.34, 0.49]</td><td class="mono">-1.86</td><td class="mono">39%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.912</td></tr><tr><td class="sig"><code>vix_conditioned_reversal_flip</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.06</td><td class="mono">[-0.25, 0.39]</td><td class="mono">-0.70</td><td class="mono">11%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>usd_exposure_dollar_trend_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.10</td><td class="mono">[-0.58, 0.42]</td><td class="mono">-0.50</td><td class="mono">25%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>vix_beta_drift</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.07</td><td class="mono">[-0.41, 0.52]</td><td class="mono">0.02</td><td class="mono">15%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.886</td></tr><tr><td class="sig"><code>turn_of_year_tax_loss_rebound_intensity</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.07</td><td class="mono">[-0.31, 0.46]</td><td class="mono">0.82</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>macro_trend_beta_alignment</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.20</td><td class="mono">[-0.77, 0.39]</td><td class="mono">-0.94</td><td class="mono">40%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.809</td></tr><tr><td class="sig"><code>macro_beta_churn_instability</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">0.07</td><td class="mono">[-0.30, 0.44]</td><td class="mono">0.88</td><td class="mono">70%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>dollar_trend_fx_beta_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.08</td><td class="mono">[-0.55, 0.39]</td><td class="mono">-0.58</td><td class="mono">26%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>stock_comp_earnings_dilution</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.08</td><td class="mono">[-0.39, 0.53]</td><td class="mono">-1.27</td><td class="mono">53%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>macro_exposure_instability</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.05</td><td class="mono">[-0.33, 0.41]</td><td class="mono">0.65</td><td class="mono">66%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.912</td></tr><tr><td class="sig"><code>net_trade_credit_expansion</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.04</td><td class="mono">[-0.31, 0.41]</td><td class="mono">-0.47</td><td class="mono">9%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.940</td></tr><tr><td class="sig"><code>stock_comp_burden_yield</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.04</td><td class="mono">[-0.36, 0.49]</td><td class="mono">1.09</td><td class="mono">24%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.944</td></tr><tr><td class="sig"><code>aggregate_inventory_overhang_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.02</td><td class="mono">[-0.57, 0.48]</td><td class="mono">1.07</td><td class="mono">38%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.975</td></tr><tr><td class="sig"><code>dollar_liquidity_cycle_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.18</td><td class="mono">[-0.70, 0.47]</td><td class="mono">0.32</td><td class="mono">17%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.811</td></tr><tr><td class="sig"><code>unemployment_news_regime_asymmetry_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.02</td><td class="mono">[-0.58, 0.55]</td><td class="mono">0.55</td><td class="mono">13%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.979</td></tr><tr><td class="sig"><code>macro_beta_churn</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.03</td><td class="mono">[-0.34, 0.39]</td><td class="mono">0.73</td><td class="mono">70%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.950</td></tr><tr><td class="sig"><code>macro_beta_instability_ambiguity</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.03</td><td class="mono">[-0.34, 0.39]</td><td class="mono">0.73</td><td class="mono">70%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.950</td></tr><tr><td class="sig"><code>cross_sectional_return_dispersion_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.05</td><td class="mono">[-0.62, 0.45]</td><td class="mono">0.49</td><td class="mono">28%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.944</td></tr><tr><td class="sig"><code>macro_pessimism_contrarian_market_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.00</td><td class="mono">[-0.54, 0.51]</td><td class="mono">-0.16</td><td class="mono">5%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.986</td></tr><tr><td class="sig"><code>aggregate_inventory_sales_cycle_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.02</td><td class="mono">[-0.40, 0.37]</td><td class="mono">0.04</td><td class="mono">6%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.975</td></tr><tr><td class="sig"><code>rate_duration_mispricing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.03</td><td class="mono">[-0.36, 0.50]</td><td class="mono">1.05</td><td class="mono">29%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.963</td></tr><tr><td class="sig"><code>funding_conditioned_beta_tilt</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.03</td><td class="mono">[-0.35, 0.38]</td><td class="mono">-0.47</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.950</td></tr><tr><td class="sig"><code>book_tax_gap_earnings_quality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.03</td><td class="mono">[-0.38, 0.42]</td><td class="mono">-0.97</td><td class="mono">13%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.966</td></tr><tr><td class="sig"><code>variance_risk_premium_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.01</td><td class="mono">[-0.46, 0.45]</td><td class="mono">-1.67</td><td class="mono">31%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.986</td></tr><tr><td class="sig"><code>aggregate_earnings_growth_breadth_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.05</td><td class="mono">[-0.52, 0.40]</td><td class="mono">-0.51</td><td class="mono">21%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.944</td></tr><tr><td class="sig"><code>unemployment_trough_breakout_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.02</td><td class="mono">[-0.57, 0.56]</td><td class="mono">0.22</td><td class="mono">10%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.979</td></tr><tr><td class="sig"><code>sentiment_fundamental_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.10</td><td class="mono">[-0.67, 0.43]</td><td class="mono">-0.12</td><td class="mono">17%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>macro_risk_on_breadth_contrarian_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.02</td><td class="mono">[-0.50, 0.43]</td><td class="mono">-1.08</td><td class="mono">30%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.979</td></tr><tr><td class="sig"><code>rollover_risk_credit_cycle_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">0.01</td><td class="mono">[-0.39, 0.45]</td><td class="mono">1.40</td><td class="mono">12%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.979</td></tr><tr><td class="sig"><code>aggregate_illiquidity_shock_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.04</td><td class="mono">[-0.50, 0.36]</td><td class="mono">-1.47</td><td class="mono">29%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.950</td></tr><tr><td class="sig"><code>turn_of_year_tax_loss_pressure</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.96</td><td class="mono">[-1.85, -0.15]</td><td class="mono">-1.16</td><td class="mono">22%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.128</td></tr><tr><td class="sig"><code>sahm_labor_deterioration_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.02</td><td class="mono">[-0.61, 0.53]</td><td class="mono">0.20</td><td class="mono">11%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.979</td></tr><tr><td class="sig"><code>hidden_rate_duration_mismatch</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.01</td><td class="mono">[-0.47, 0.51]</td><td class="mono">1.13</td><td class="mono">12%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.986</td></tr><tr><td class="sig"><code>long_term_reversal_sentiment_conditioned</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.10</td><td class="mono">[-0.65, 0.40]</td><td class="mono">-0.87</td><td class="mono">40%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>sentiment_gap_contrarian_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.12</td><td class="mono">[-0.74, 0.50]</td><td class="mono">-0.69</td><td class="mono">9%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>aggregate_turnover_sentiment_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.10</td><td class="mono">[-0.52, 0.37]</td><td class="mono">-1.43</td><td class="mono">20%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>household_sentiment_fundamental_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.17</td><td class="mono">[-0.63, 0.30]</td><td class="mono">-1.11</td><td class="mono">11%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.802</td></tr><tr><td class="sig"><code>sentiment_fundamental_disagreement_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.23</td><td class="mono">[-0.71, 0.20]</td><td class="mono">-0.84</td><td class="mono">9%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.592</td></tr><tr><td class="sig"><code>sentiment_capitulation_recovery_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.09</td><td class="mono">[-0.58, 0.44]</td><td class="mono">-0.58</td><td class="mono">14%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>labor_cycle_turning_point_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.21</td><td class="mono">[-0.86, 0.36]</td><td class="mono">-0.02</td><td class="mono">29%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.803</td></tr><tr><td class="sig"><code>hidden_equity_dilution_cost</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.08</td><td class="mono">[-0.52, 0.28]</td><td class="mono">-1.31</td><td class="mono">59%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>attention_shock_amplified_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.09</td><td class="mono">[-0.46, 0.25]</td><td class="mono">-2.14</td><td class="mono">22%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>halloween_volatility_scaled_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.11</td><td class="mono">[-0.47, 0.27]</td><td class="mono">0.30</td><td class="mono">5%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.838</td></tr><tr><td class="sig"><code>hidden_refinancing_rate_exposure</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.07</td><td class="mono">[-0.41, 0.27]</td><td class="mono">-1.71</td><td class="mono">16%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>cross_asset_news_diffusion_momentum</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.08</td><td class="mono">[-0.48, 0.32]</td><td class="mono">0.38</td><td class="mono">8%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.872</td></tr><tr><td class="sig"><code>sentiment_hard_data_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.21</td><td class="mono">[-0.72, 0.27]</td><td class="mono">-0.55</td><td class="mono">21%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.745</td></tr><tr><td class="sig"><code>fundamental_momentum_price_gap</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">-0.10</td><td class="mono">[-0.43, 0.26]</td><td class="mono">0.55</td><td class="mono">72%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.838</td></tr><tr><td class="sig"><code>net_cash_yield_rate_change_interaction</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.09</td><td class="mono">[-0.48, 0.32]</td><td class="mono">-1.04</td><td class="mono">12%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>credit_sensitivity_amplified_balance_sheet_safety</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.10</td><td class="mono">[-0.55, 0.34]</td><td class="mono">2.54</td><td class="mono">60%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.867</td></tr><tr><td class="sig"><code>macro_trend_beta_spillover</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.17</td><td class="mono">[-0.60, 0.28]</td><td class="mono">-0.99</td><td class="mono">35%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.786</td></tr><tr><td class="sig"><code>macro_trend_attributed_return_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.13</td><td class="mono">[-0.47, 0.25]</td><td class="mono">0.13</td><td class="mono">39%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.803</td></tr><tr><td class="sig"><code>transitory_nonoperating_income_intensity</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.14</td><td class="mono">[-0.59, 0.30]</td><td class="mono">-0.08</td><td class="mono">39%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.822</td></tr><tr><td class="sig"><code>consumer_sentiment_credit_divergence_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.24</td><td class="mono">[-0.67, 0.19]</td><td class="mono">-1.41</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.636</td></tr><tr><td class="sig"><code>real_policy_stance_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.16</td><td class="mono">[-0.55, 0.33]</td><td class="mono">-2.07</td><td class="mono">21%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.802</td></tr><tr><td class="sig"><code>seasonal_same_month_return_persistence</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.28</td><td class="mono">[-0.76, 0.11]</td><td class="mono">-1.44</td><td class="mono">53%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.441</td></tr><tr><td class="sig"><code>macro_beta_dispersion_crowding_rotation</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.17</td><td class="mono">[-0.62, 0.23]</td><td class="mono">-1.97</td><td class="mono">24%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.774</td></tr><tr><td class="sig"><code>abnormal_reporting_lag</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.16</td><td class="mono">[-0.60, 0.29]</td><td class="mono">0.17</td><td class="mono">21%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.802</td></tr><tr><td class="sig"><code>rate_beta_duration_mismatch</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.20</td><td class="mono">[-0.74, 0.25]</td><td class="mono">-2.15</td><td class="mono">13%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.664</td></tr><tr><td class="sig"><code>valuation_dispersion_market_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.31</td><td class="mono">[-0.83, 0.17]</td><td class="mono">-0.22</td><td class="mono">32%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.491</td></tr><tr><td class="sig"><code>conditional_size_credit_cycle</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.25</td><td class="mono">[-0.71, 0.23]</td><td class="mono">-1.02</td><td class="mono">12%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.592</td></tr><tr><td class="sig"><code>operating_leverage_business_cycle</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.19</td><td class="mono">[-0.69, 0.28]</td><td class="mono">-0.56</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.786</td></tr><tr><td class="sig"><code>pure_calendar_month_seasonality_spread</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.29</td><td class="mono">[-0.70, 0.12]</td><td class="mono">-1.61</td><td class="mono">5%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.441</td></tr><tr><td class="sig"><code>attention_shock_at_the_highs</code></td><td><span class="la-pill" style="background:#e94560">Loses to factors</span></td><td class="mono">-0.29</td><td class="mono">[-0.69, 0.09]</td><td class="mono">-2.96</td><td class="mono">84%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.373</td></tr><tr><td class="sig"><code>taylor_rule_policy_accommodation_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.35</td><td class="mono">[-0.72, 0.07]</td><td class="mono">-2.65</td><td class="mono">16%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.372</td></tr><tr><td class="sig"><code>working_capital_release_under_tightening</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.29</td><td class="mono">[-0.68, 0.10]</td><td class="mono">-0.56</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.402</td></tr><tr><td class="sig"><code>seasonal_same_month_residual_return</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.38</td><td class="mono">[-0.81, 0.01]</td><td class="mono">-1.92</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.254</td></tr><tr><td class="sig"><code>attention_shock_return_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.42</td><td class="mono">[-0.78, -0.10]</td><td class="mono">-3.92</td><td class="mono">31%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.053</td></tr><tr><td class="sig"><code>turnover_shock_price_run_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.42</td><td class="mono">[-0.78, -0.10]</td><td class="mono">-3.92</td><td class="mono">31%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.053</td></tr><tr><td class="sig"><code>earnings_smoothing_detection</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.35</td><td class="mono">[-0.81, 0.15]</td><td class="mono">-2.36</td><td class="mono">34%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.425</td></tr><tr><td class="sig"><code>same_quarter_earnings_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.29</td><td class="mono">[-0.72, 0.16]</td><td class="mono">-0.68</td><td class="mono">23%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.441</td></tr><tr><td class="sig"><code>sbc_inflated_cash_earnings</code></td><td><span class="la-pill" style="background:#e94560">Loses to factors</span></td><td class="mono">-0.31</td><td class="mono">[-0.78, 0.12]</td><td class="mono">-3.55</td><td class="mono">61%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.391</td></tr><tr><td class="sig"><code>credit_beta_leverage_mismatch</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">-0.28</td><td class="mono">[-0.63, 0.10]</td><td class="mono">0.22</td><td class="mono">73%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.439</td></tr><tr><td class="sig"><code>same_month_return_seasonality_spread</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.45</td><td class="mono">[-0.95, 0.02]</td><td class="mono">-1.72</td><td class="mono">13%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.219</td></tr><tr><td class="sig"><code>oil_shock_demand_supply_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.30</td><td class="mono">[-0.68, 0.12]</td><td class="mono">-1.32</td><td class="mono">13%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.433</td></tr><tr><td class="sig"><code>unpriced_leverage_credit_insensitivity</code></td><td><span class="la-pill" style="background:#6c757d">Factor tilt</span></td><td class="mono">-0.32</td><td class="mono">[-0.66, 0.05]</td><td class="mono">-0.14</td><td class="mono">73%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.369</td></tr><tr><td class="sig"><code>oil_demand_vs_supply_shock_timing</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.40</td><td class="mono">[-0.83, 0.01]</td><td class="mono">-1.42</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.280</td></tr><tr><td class="sig"><code>earnings_smoothing_opacity</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.42</td><td class="mono">[-0.85, 0.01]</td><td class="mono">-2.74</td><td class="mono">41%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.261</td></tr><tr><td class="sig"><code>earnings_smoothness_vs_cashflow</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.42</td><td class="mono">[-0.86, 0.00]</td><td class="mono">-2.78</td><td class="mono">41%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.258</td></tr><tr><td class="sig"><code>earnings_smoothing_vs_cashflow_volatility</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.41</td><td class="mono">[-0.84, 0.01]</td><td class="mono">-2.58</td><td class="mono">42%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.249</td></tr><tr><td class="sig"><code>same_month_return_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.65</td><td class="mono">[-1.19, -0.18]</td><td class="mono">-2.59</td><td class="mono">10%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.046</td></tr><tr><td class="sig"><code>annual_return_seasonality_spread</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.66</td><td class="mono">[-1.20, -0.19]</td><td class="mono">-2.60</td><td class="mono">10%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.046</td></tr><tr><td class="sig"><code>external_finance_dependence_credit_shock</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.46</td><td class="mono">[-0.90, -0.01]</td><td class="mono">-1.96</td><td class="mono">1%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.165</td></tr><tr><td class="sig"><code>annual_return_seasonality_purged</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.62</td><td class="mono">[-1.11, -0.18]</td><td class="mono">-2.03</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.061</td></tr><tr><td class="sig"><code>same_month_seasonality_ex_adjacent</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.62</td><td class="mono">[-1.11, -0.18]</td><td class="mono">-2.03</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.061</td></tr><tr><td class="sig"><code>size_premium_credit_conditions_switch</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.44</td><td class="mono">[-0.85, -0.03]</td><td class="mono">-2.13</td><td class="mono">12%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.199</td></tr><tr><td class="sig"><code>same_month_seasonality_excess</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.65</td><td class="mono">[-1.13, -0.24]</td><td class="mono">-2.45</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.039</td></tr><tr><td class="sig"><code>relative_month_of_year_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.65</td><td class="mono">[-1.15, -0.23]</td><td class="mono">-2.27</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.040</td></tr><tr><td class="sig"><code>same_month_seasonality_residual</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.65</td><td class="mono">[-1.15, -0.23]</td><td class="mono">-2.27</td><td class="mono">4%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.040</td></tr><tr><td class="sig"><code>annual_return_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.70</td><td class="mono">[-1.26, -0.24]</td><td class="mono">-2.53</td><td class="mono">7%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.039</td></tr><tr><td class="sig"><code>zero_earnings_threshold_management</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.56</td><td class="mono">[-1.00, -0.16]</td><td class="mono">-1.06</td><td class="mono">46%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.048</td></tr><tr><td class="sig"><code>same_month_seasonal_persistence</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.69</td><td class="mono">[-1.12, -0.30]</td><td class="mono">-2.68</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.015</td></tr><tr><td class="sig"><code>pre_earnings_filing_clock_premium</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.47</td><td class="mono">[-0.85, -0.10]</td><td class="mono">-2.51</td><td class="mono">2%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.073</td></tr><tr><td class="sig"><code>hidden_credit_risk_mismatch</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.48</td><td class="mono">[-0.81, -0.15]</td><td class="mono">-1.29</td><td class="mono">46%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.079</td></tr><tr><td class="sig"><code>same_month_minus_offseason_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.73</td><td class="mono">[-1.20, -0.30]</td><td class="mono">-3.02</td><td class="mono">5%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.021</td></tr><tr><td class="sig"><code>same_calendar_month_return_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.71</td><td class="mono">[-1.27, -0.22]</td><td class="mono">-2.96</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.040</td></tr><tr><td class="sig"><code>same_calendar_month_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.71</td><td class="mono">[-1.27, -0.22]</td><td class="mono">-2.96</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.040</td></tr><tr><td class="sig"><code>leverage_credit_beta_complacency</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.51</td><td class="mono">[-0.86, -0.19]</td><td class="mono">-1.40</td><td class="mono">49%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.061</td></tr><tr><td class="sig"><code>stock_comp_dilution_earnings_overstatement</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.58</td><td class="mono">[-1.08, -0.06]</td><td class="mono">-3.40</td><td class="mono">43%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.090</td></tr><tr><td class="sig"><code>residual_same_month_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.80</td><td class="mono">[-1.32, -0.32]</td><td class="mono">-3.36</td><td class="mono">5%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.015</td></tr><tr><td class="sig"><code>same_month_annual_seasonality</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.75</td><td class="mono">[-1.33, -0.25]</td><td class="mono">-3.15</td><td class="mono">2%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.035</td></tr><tr><td class="sig"><code>same_month_seasonal_return_persistence</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.75</td><td class="mono">[-1.33, -0.25]</td><td class="mono">-3.15</td><td class="mono">2%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.035</td></tr><tr><td class="sig"><code>expected_earnings_announcement_month</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.57</td><td class="mono">[-0.95, -0.20]</td><td class="mono">-2.46</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.046</td></tr><tr><td class="sig"><code>tax_loss_overhang_seasonal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.50</td><td class="mono">[-0.90, -0.06]</td><td class="mono">-1.82</td><td class="mono">6%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.131</td></tr><tr><td class="sig"><code>tax_loss_selling_turn_of_year_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.47</td><td class="mono">[-0.78, -0.11]</td><td class="mono">-2.26</td><td class="mono">3%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.081</td></tr><tr><td class="sig"><code>attention_spike_return_reversal</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.75</td><td class="mono">[-1.28, -0.33]</td><td class="mono">-5.32</td><td class="mono">10%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.004</td></tr><tr><td class="sig"><code>tax_loss_overhang_turn_of_year</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.45</td><td class="mono">[-0.73, -0.12]</td><td class="mono">-2.28</td><td class="mono">2%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.104</td></tr><tr><td class="sig"><code>macro_loading_instability</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-0.70</td><td class="mono">[-1.08, -0.31]</td><td class="mono">-3.42</td><td class="mono">10%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.015</td></tr><tr><td class="sig"><code>filing_staleness_information_neglect</code></td><td><span class="la-pill" style="background:#6c757d">Noise</span></td><td class="mono">-1.05</td><td class="mono">[-1.47, -0.67]</td><td class="mono">-4.39</td><td class="mono">1%</td><td><span class="la-bar" style="--w:0.0px;--c:#0f3460"><span class="la-bar-fill"></span><span class="la-bar-num">0.00</span></span></td><td class="mono">0.000</td></tr></tbody>
</table>
</div>
</div>
</details>


## Best signal vs the market

The top of the board is <code>gross_profitability</code>. Here it is against a **large-cap
benchmark** — the 500 biggest eligible names each month, cap-weighted, rebalanced
monthly, run through the identical point-in-time and delisting machinery.

![Cumulative growth and drawdown of the best signal against the cap-weighted large-cap benchmark. Log scale on the upper panel.](figures/benchmark.png){fig-alt="Best signal versus large-cap benchmark equity curves and drawdowns"}

<table class="la-cmp">
<thead><tr><th>Metric</th><th>gross_profitability</th><th>Benchmark</th></tr></thead>
<tbody><tr><td>Annualized return</td><td class="win">11.1%</td><td class="">8.3%</td></tr><tr><td>Annualized volatility</td><td class="win">12.3%</td><td class="">15.5%</td></tr><tr><td>Sharpe ratio</td><td class="win">0.92</td><td class="">0.60</td></tr><tr><td>Max drawdown</td><td class="win">-21.3%</td><td class="">-45.2%</td></tr><tr><td>Hit rate (months up)</td><td class="">62.9%</td><td class="win">65.4%</td></tr></tbody>
</table>

<div class="la-note">
<strong>These are not the same kind of return.</strong> The signal is a
<em>market-neutral</em> long/short book: it holds the top decile and shorts the
bottom, so it carries almost no market exposure and its return does not depend on
the market going up. The benchmark is long-only and fully exposed. Comparing their
raw returns rewards whichever one happened to suit the sample — the relationship
between them is the informative part:
<br><br>
Correlation to the benchmark is <strong>-0.11</strong> and beta is
<strong>-0.09</strong>. A beta near zero is the point of a long/short
construction, not a shortfall: it means the return is not repackaged market
exposure. Up-capture is <strong>19%</strong> and
down-capture <strong>-39%</strong> — how much of the
market's average up-month and down-month the strategy tracked. Low or negative
down-capture is the property a diversifying book is actually bought for.
<br><br>
Measured over 280 overlapping months. A market-neutral book with a
lower headline return than a bull-market index can still be the more valuable asset
in a portfolio, because it supplies a return stream the index cannot.
</div>


## The factors and the statistics, in depth

<details class="la-details">
<summary>The four factors — what they are and how they are built here
<span class="hint">click to expand</span></summary>
<div class="la-body">
<p style="font-size:.9rem;color:#495057;line-height:1.6">
A new signal is only interesting if it earns something the known premia do not
already pay for. These four are the standard yardstick. All are built from the same
panel the signals are scored on — equal-weighted top-third minus bottom-third
spreads on the same eligible universe — so attribution is like-for-like rather than
against an external dataset with different conventions.</p>
<dl class="la-gloss"><dt>MKT — Market</dt><dd>The excess return of holding equities at all. Every long-only strategy earns most of its return here, so it is the first thing to strip out.<span class="la-formula">Built here as: Equal-weighted return of the whole eligible universe each month.</span></dd><dt>SMB — Size (small minus big)</dt><dd>Small-cap stocks have historically earned more than large caps — compensation for illiquidity and distress risk, and partly a reflection of how hard small names are to trade at size.<span class="la-formula">Built here as: Long the smallest third by market cap, short the largest third.</span></dd><dt>HML — Value (high minus low)</dt><dd>Cheap stocks relative to book value have outearned expensive ones. The debate over whether this is risk compensation or a behavioural mispricing is unsettled; either way it is a known, harvestable premium, so a new signal should not be paid twice for it.<span class="la-formula">Built here as: Long the cheapest third by price-to-book, short the most expensive third.</span></dd><dt>UMD — Momentum (up minus down)</dt><dd>Past winners keep winning over horizons of roughly three to twelve months, usually attributed to under-reaction to news and to slow diffusion of information.<span class="la-formula">Built here as: Long the top third by 12-1 month return, short the bottom third.</span></dd></dl>
</div>
</details>

<details class="la-details">
<summary>The statistics — what each one answers, and the failure it guards against
<span class="hint">click to expand</span></summary>
<div class="la-body">
<p style="font-size:.9rem;color:#495057;line-height:1.6">
Each statistic exists because a simpler one can be fooled in a specific way. Read
top to bottom and the argument builds: a Sharpe is not enough, so bound it; a bound
is not enough on skewed returns, so reshape it; and none of it means anything until
you price in how many strategies were tried to find this one.</p>
<dl class="la-gloss"><dt>Sharpe ratio</dt><dd>Return per unit of volatility, annualized. The default headline — and the single most misleading number on any farmed leaderboard, because it says nothing about how many strategies were tried before this one was picked.<span class="la-formula">Sharpe = mean(r) / sd(r) × √12</span><div class="la-guard"><b>Guards against:</b> Says nothing about selection. Rewards fat left tails.</div></dd><dt>95% confidence interval</dt><dd>A circular block bootstrap resamples contiguous blocks of months — preserving short-run autocorrelation an i.i.d. bootstrap would destroy — and re-computes the Sharpe a few thousand times. The interval is the middle 95% of that distribution. If it straddles zero, the point estimate is not doing the work you think it is.<span class="la-formula">block length ≈ T^(1/3), 2,000 resamples</span><div class="la-guard"><b>Guards against:</b> A single Sharpe with no interval invites false confidence.</div></dd><dt>Newey-West t-statistic</dt><dd>The t-stat of the mean return, with standard errors corrected for autocorrelation and heteroskedasticity. Monthly strategy returns are rarely independent, and ignoring that inflates significance.<span class="la-formula">t = mean(r) / SE_NW(r),  lags = ⌊4(T/100)^(2/9)⌋</span><div class="la-guard"><b>Guards against:</b> Plain OLS errors overstate significance on persistent series.</div></dd><dt>Factor R²</dt><dd>The share of the strategy's month-to-month variance explained by regressing it on market, size, value and momentum. High R² means you have largely rebuilt a portfolio of things that already have names.<span class="la-formula">R² from  r_t = α + β₁MKT + β₂SMB + β₃HML + β₄UMD + ε_t</span><div class="la-guard"><b>Guards against:</b> A high Sharpe can be entirely factor exposure.</div></dd><dt>Alpha t-statistic</dt><dd>The intercept of that same regression — the return the four factors cannot explain — divided by its Newey-West standard error. This is the closest thing here to a test of whether a signal is genuinely new. Note the sign: a large negative alpha t means the construction <em>loses</em> to the factor it tracks.<span class="la-formula">α from the regression above; t = α / SE_NW(α)</span><div class="la-guard"><b>Guards against:</b> Sharpe alone cannot tell a discovery from a repackaged tilt.</div></dd><dt>Probabilistic Sharpe (PSR)</dt><dd>The probability the true Sharpe exceeds zero, adjusted for skew and kurtosis. Strategies that make small gains most months and lose badly occasionally — the classic short-leg blow-up shape — get marked down relative to their raw Sharpe.<span class="la-formula">PSR = Φ( (SR − SR*)·√(T−1) / √(1 − γ₃SR + (γ₄−1)SR²/4) )</span><div class="la-guard"><b>Guards against:</b> Raw Sharpe assumes normality that returns rarely have.</div></dd><dt>Search hurdle</dt><dd>The Sharpe you would expect from the <em>best</em> of N random strategies, given how dispersed the Sharpes in this library actually are. It is the bar luck alone clears. Anything under it is not evidence of anything.<span class="la-formula">E[max SR] ≈ √V · [ (1−γ)Φ⁻¹(1 − 1/N) + γΦ⁻¹(1 − 1/(Ne)) ]</span><div class="la-guard"><b>Guards against:</b> Testing many strategies guarantees a good-looking winner.</div></dd><dt>Deflated Sharpe (DSR)</dt><dd>The PSR measured against that hurdle instead of against zero: the probability the signal is real <em>given</em> the size of the search that found it. This is what the board is sorted by. N counts every farming attempt in the ledger — including the ideas that died at the data or compile stage — because counting only survivors would understate the search and flatter every number.<span class="la-formula">DSR = PSR evaluated at SR* = E[max SR]</span><div class="la-guard"><b>Guards against:</b> The headline result of a bulk search is biased upward by construction.</div></dd><dt>Benjamini-Hochberg q-value</dt><dd>Ranks every p-value in the library and controls the expected proportion of false discoveries among those called significant. Far less brutal than Bonferroni when signals are correlated — which factor strategies always are — while still bounding the damage.<span class="la-formula">q_(i) = min over j ≥ i of  p_(j) · m / j</span><div class="la-guard"><b>Guards against:</b> Per-signal p-values ignore that the library is a search.</div></dd></dl>
<div class="la-note" style="margin-bottom:0">
<strong>How to read a row in one pass.</strong> Start at the deflated Sharpe: if it
is low, nothing else on the row rescues the signal. If it is high, check the alpha
t-stat to see whether the return is genuinely new or a factor tilt wearing a
different name. Then check the confidence interval: if it straddles zero, the
sample is too short to lean on regardless of what the point estimates say.
</div>
</div>
</details>


## Run history

<table class="la-table">
<thead><tr><th>Run</th><th>Signals</th><th>Survivors</th>
<th>Hurdle</th><th>Best Sharpe</th></tr></thead>
<tbody><tr><td class="mono">2026-10-02</td><td class="mono">171</td><td class="mono">0</td><td class="mono">1.01</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-09-15</td><td class="mono">170</td><td class="mono">0</td><td class="mono">1.00</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-09-02</td><td class="mono">121</td><td class="mono">0</td><td class="mono">0.95</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-09-01</td><td class="mono">113</td><td class="mono">0</td><td class="mono">0.95</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-31</td><td class="mono">102</td><td class="mono">0</td><td class="mono">0.95</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-30</td><td class="mono">96</td><td class="mono">0</td><td class="mono">0.94</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-29</td><td class="mono">88</td><td class="mono">0</td><td class="mono">0.92</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-27</td><td class="mono">77</td><td class="mono">0</td><td class="mono">0.89</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-26</td><td class="mono">69</td><td class="mono">0</td><td class="mono">0.89</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-25</td><td class="mono">58</td><td class="mono">0</td><td class="mono">0.83</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-24</td><td class="mono">48</td><td class="mono">0</td><td class="mono">0.75</td><td class="mono">0.92</td></tr><tr><td class="mono">2026-08-23</td><td class="mono">39</td><td class="mono">0</td><td class="mono">0.66</td><td class="mono">0.92</td></tr></tbody>
</table>


## Where this goes next

The pipeline works; the constraint now is the *hypothesis space*, not the
machinery. Ranked roughly by expected value per unit of effort:

<div class="la-next"><div class="la-card"><h4>Widen the data surface</h4><p>Every signal is bounded by what the panel contains. The highest-leverage additions are short interest and borrow cost (crowding and limits to arbitrage), institutional 13F holdings (ownership churn), analyst revisions and dispersion (expectation dynamics), and options-implied volatility surfaces (skew as a crash-risk premium). Each new provider under <code>data/features/</code> multiplies the hypothesis space rather than adding to it.</p><span class="eff">high impact · medium effort</span></div><div class="la-card"><h4>Walk-forward, not full-sample</h4><p>The harness currently scores each signal over the whole history. A purged, embargoed walk-forward protocol — fit and select on an expanding window, evaluate only on the untouched next block — would turn every number on this page into an out-of-sample one, and would catch signals that only work in the regime they were invented from.</p><span class="eff">high impact · medium effort</span></div><div class="la-card"><h4>Let the agents read their own failures</h4><p>The idea agent sees which themes are thin, but not <em>why</em> past ideas died. Feeding back the specific failure — the validator's complaint, the coverage gate, the factor attribution that revealed a tilt — should raise the hit rate per unit of spend, and it costs nothing but prompt tokens.</p><span class="eff">high impact · low effort</span></div><div class="la-card"><h4>Search combinations, not just singles</h4><p>Most published anomalies are weak alone and strong combined. A composite layer — rank-average within a theme, or a shrunk regression on standardized signals — would test whether the farmed library is greater than its parts. The novelty machinery already stores the return streams needed for it.</p><span class="eff">medium impact · low effort</span></div><div class="la-card"><h4>Model capacity honestly</h4><p>Costs are a flat per-unit-traded charge. Real capacity binds through market impact that scales with size and inversely with liquidity. A square-root impact model plus an explicit AUM assumption would tell us which of these signals survive at size rather than only on paper.</p><span class="eff">medium impact · medium effort</span></div><div class="la-card"><h4>Turn the delisting assumption into a measurement</h4><p>Delisted names currently book a configurable return, defaulted to a liquidation at the last observed price. Sharadar's corporate-actions table carries the actual delisting events; using them would replace an assumption with data, and would matter most for exactly the distressed, cheap names that value signals load on.</p><span class="eff">medium impact · low effort</span></div><div class="la-card"><h4>Regime-condition the evaluation</h4><p>A signal that works only in high-dispersion or high-rate regimes is still useful — if you know which regime you are in. Conditioning the report on macro states already in the panel would separate genuinely broken signals from ones that are simply out of season.</p><span class="eff">medium impact · medium effort</span></div><div class="la-card"><h4>Raise the bar as the library grows</h4><p>The deflation hurdle scales with the number of trials, so the more this pipeline runs, the harder it becomes to claim a discovery — correctly. The counter-move is not a lower bar but a better prior: pre-register the economic mechanism before the backtest, and treat signals whose mechanism was stated in advance as a separate, smaller family of tests.</p><span class="eff">structural · ongoing</span></div></div>

The honest summary is that a language model is a good generator of *plausible*
hypotheses and a poor judge of which ones are real. That division of labour is the
whole design: let the model propose freely, and make the harness — not the model —
decide what counts.


## The signal library

<details class="la-details">
<summary>All 171 signals — what each one measures, and the code that computes it
<span class="hint">click to expand</span></summary>
<div class="la-body">
<p class="la-lib-intro">
One entry per signal currently in the library: the reference anomalies from the
literature and every agent-written signal that compiled and passed validation.
The calculation shown is the <code>compute</code> method itself, read straight out of
the module at publish time — the page cannot drift away from the code it documents.
Each returns a <em>raw</em> score; the harness multiplies it by the expected sign, so
after orientation a high score always means "go long".
</p>
<h4 class="la-group">Cross-sectional signals (122)</h4><p class="la-group-note">Scored for every stock every month. The strategy is long the top third of the oriented score and short the bottom third, rebalanced monthly.</p><details class="la-sig"><summary><code>credit_sensitivity_amplified_balance_sheet_safety</code><span class="la-tag">combo</span><span class="gist">Interaction of credit-beta sensitivity with fundamental balance-sheet safety: among stocks whose returns load…</span></summary><div class="la-sig-body"><p>Interaction of credit-beta sensitivity with fundamental balance-sheet safety: among stocks whose returns load heavily on credit-market moves, those with low leverage, high EBIT coverage, high liquidity and cash reserves are over-discounted and expected to outperform.</p><div class="la-meta"><span><b>Category</b> combo</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debt = panel['debt']
    assets = panel['assets']
    ebit = panel['ebit']
    currentratio = panel['currentratio']
    cashneq = panel['cashneq']
    liabilities = panel['liabilities']
    beta_credit = panel['beta_credit_252']
    # base validity mask: require all raw inputs present
    base_valid = (
        debt.notna() &amp; assets.notna() &amp; ebit.notna() &amp;
        currentratio.notna() &amp; cashneq.notna() &amp;
        liabilities.notna() &amp; beta_credit.notna()
    )
    assets_pos = assets &gt; 0
    debt_pos = debt &gt; 0
    lev = pd.Series(np.nan, index=panel.index)
    lev_mask = base_valid &amp; assets_pos
    lev.loc[lev_mask] = debt.loc[lev_mask] / assets.loc[lev_mask]
    denom = pd.Series(np.nan, index=panel.index)
    cov_mask = base_valid &amp; debt_pos
    denom.loc[cov_mask] = debt.loc[cov_mask]
    # fallback to eps-like max(debt, eps) per formula, but eps not mapped -&gt; use debt only
    cov = pd.Series(np.nan, index=panel.index)
    cov.loc[cov_mask] = ebit.loc[cov_mask] / denom.loc[cov_mask]
    cov = cov.clip(-5, 5)
    liq = currentratio.clip(upper=5)
    liq = liq.where(base_valid)
    cashr = pd.Series(np.nan, index=panel.index)
    cashr.loc[lev_mask] = cashneq.loc[lev_mask] / assets.loc[lev_mask]
    df = pd.DataFrame({
        'lev': lev,
        'cov': cov,
        'liq': liq,
        'cashr': cashr,
        'beta_credit': beta_credit.where(base_valid),
    }, index=panel.index)
    def per_date(g: pd.DataFrame) -&gt; pd.Series:
        valid_rows = g[['lev', 'cov', 'liq', 'cashr', 'beta_credit']].notna().all(axis=1)
        g_valid = g.loc[valid_rows]
        out = pd.Series(np.nan, index=g.index)
        if g_valid.empty:
            return out
        z_lev = self._zscore(g_valid['lev'])
        z_cov = self._zscore(g_valid['cov'])
        z_liq = self._zscore(g_valid['liq'])
        z_cashr = self._zscore(g_valid['cashr'])
        safety = -z_lev + 0.5 * z_cov + 0.5 * z_liq + 0.5 * z_cashr
        abs_beta = g_valid['beta_credit'].abs()
        w = abs_beta.rank(pct=True)
        sig = safety * (0.25 + w)
        out.loc[g_valid.index] = sig
        return out
    raw_signal = df.groupby(level='date', group_keys=False).apply(per_date)
    # macro_baa_spread past-only z-score scaling
    baa = panel['macro_baa_spread']
    baa_by_date = baa.groupby(level='date').first()
    baa_by_date = baa_by_date.sort_index()
    mean_rolling = baa_by_date.expanding(min_periods=2).mean().shift(1)
    std_rolling = baa_by_date.expanding(min_periods=2).std().shift(1)
    z_baa = (baa_by_date - mean_rolling) / std_rolling
    z_baa = z_baa.replace([np.inf, -np.inf], np.nan)
    z_baa_clipped = z_baa.clip(-1, 1)
    multiplier = 1 + 0.5 * z_baa_clipped
    multiplier = multiplier.fillna(1.0)
    multiplier_full = panel.index.get_level_values('date').map(multiplier)
    multiplier_full = pd.Series(multiplier_full, index=panel.index)
    result = raw_signal * multiplier_full
    result.name = self.name
    return result</pre></div></details><details class="la-sig"><summary><code>funding_conditioned_beta_tilt</code><span class="la-tag">combo</span><span class="gist">Flips the sign of the beta tilt with the funding-liquidity cycle, measured by the joint 12-month change in…</span></summary><div class="la-sig-body"><p>Flips the sign of the beta tilt with the funding-liquidity cycle, measured by the joint 12-month change in fed funds rate and Baa credit spread. During funding easing episodes, high-beta names are favored; during tightening, low-beta names are favored.</p><div class="la-meta"><span><b>Category</b> combo</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta = panel['beta_252']
    fedfunds_chg = panel['macro_fedfunds_chg12m']
    baa_chg = panel['macro_baa_spread_chg12m']
    nobs = panel['nobs']
    tighten = fedfunds_chg + baa_chg
    ease = -tighten
    score = beta * np.tanh(ease)
    invalid = beta.isna() | fedfunds_chg.isna() | baa_chg.isna() | (nobs &lt; 200)
    score = score.where(~invalid, np.nan)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>net_cash_yield_rate_change_interaction</code><span class="la-tag">combo</span><span class="gist">Interaction between net-cash-to-marketcap ratio and the signed 12-month change in the 2-year Treasury yield</span></summary><div class="la-sig-body"><p>Interaction between net-cash-to-marketcap ratio and the signed 12-month change in the 2-year Treasury yield. Cash-rich firms benefit from rising short rates via interest income while net-debt firms benefit from falling rates via cheaper refinancing; this macro-conditioned quality/leverage signal flips sign with the direction of the short-rate cycle.</p><div class="la-meta"><span><b>Category</b> combo</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cashneq = panel['cashneq']
    debt = panel['debt']
    marketcap = panel['marketcap']
    cond = panel['macro_dgs2_chg12m']
    valid = cashneq.notna() &amp; debt.notna() &amp; marketcap.notna() &amp; (marketcap &gt; 0) &amp; cond.notna()
    net_cash_yield = (cashneq - debt) / marketcap
    net_cash_yield = net_cash_yield.where(valid)
    def winsorize(s):
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    net_cash_yield_w = net_cash_yield.groupby(level='date').transform(winsorize)
    signal = net_cash_yield_w * cond
    signal = signal.where(valid)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>operating_leverage_business_cycle</code><span class="la-tag">combo</span><span class="gist">Operating leverage (opex/assets) conditioned on the trailing 12m change in industrial production and…</span></summary><div class="la-sig-body"><p>Operating leverage (opex/assets) conditioned on the trailing 12m change in industrial production and recession state: high fixed-cost intensity firms are expected to outperform when industrial activity is improving (and underperform when it is deteriorating), capturing a macro-state-dependent risk premium orthogonal to static value/quality factors.</p><div class="la-meta"><span><b>Category</b> combo</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    opex = panel['opex']
    assets = panel['assets']
    revenue = panel['revenue']
    valid = (assets &gt; 0) &amp; (revenue &gt; 0)
    ol = (opex / assets).where(valid)
    ol_clipped = ol.clip(lower=0, upper=2)
    def zscore(s):
        mu = s.mean()
        sd = s.std()
        if sd is None or sd == 0 or pd.isna(sd):
            return pd.Series(np.nan, index=s.index)
        return (s - mu) / sd
    ol_z = ol_clipped.groupby(level='date').transform(zscore)
    indpro_chg = panel['macro_indpro_chg12m']
    usrec = panel['macro_usrec']
    cyc = np.tanh(indpro_chg / 2.0) * (1 - 0.5 * usrec)
    cyc = cyc.where(indpro_chg.notna() &amp; usrec.notna())
    raw = ol_z * cyc
    return raw.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>rollover_risk_credit_cycle_interaction</code><span class="la-tag">combo</span><span class="gist">Interaction between near-term rollover risk (net current debt / assets) and the 12-month change in the…</span></summary><div class="la-sig-body"><p>Interaction between near-term rollover risk (net current debt / assets) and the 12-month change in the Baa-Treasury credit spread. Firms with high net short-term debt suffer when credit spreads widen and benefit when spreads tighten, so the score sign-flips with the credit cycle.</p><div class="la-meta"><span><b>Category</b> combo</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debtc = panel['debtc']
    cashneq = panel['cashneq']
    assets = panel['assets']
    tighten = panel['macro_baa_spread_chg12m']
    valid = debtc.notna() &amp; cashneq.notna() &amp; assets.notna() &amp; tighten.notna() &amp; (assets &gt; 0)
    rollover = (debtc - cashneq) / assets.where(assets &gt; 0)
    rollover = rollover.clip(-1, 1)
    score = -rollover * tighten
    score = score.where(valid, np.nan)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>asset_growth</code><span class="la-tag">investment</span><span class="gist">Cooper-Gulen-Schill asset growth (YoY); low-growth firms outperform</span></summary><div class="la-sig-body"><p>Cooper-Gulen-Schill asset growth (YoY); low-growth firms outperform.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the low scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    assets = panel["assets"].where(panel["assets"] &gt; 0)
    return _yoy(assets)</pre></div></details><details class="la-sig"><summary><code>external_finance_dependence_credit_shock</code><span class="la-tag">investment</span><span class="gist">Rajan-Zingales style external finance dependence (capex exceeding internal operating cash flow) combined with…</span></summary><div class="la-sig-body"><p>Rajan-Zingales style external finance dependence (capex exceeding internal operating cash flow) combined with short-term debt load, interacted with the 12-month change in the Baa credit spread. Firms with high financing need are hurt when credit spreads widen and helped when spreads tighten; the harness orients the score so that expected sign is positive.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    assets = panel['assets']
    capex = panel['capex']
    ncfo = panel['ncfo']
    debtc = panel['debtc']
    macro_chg = panel['macro_baa_spread_chg12m']
    valid_assets = assets &gt; 0
    dep = pd.Series(np.nan, index=panel.index)
    stdebt = pd.Series(np.nan, index=panel.index)
    dep_valid = valid_assets &amp; capex.notna() &amp; ncfo.notna() &amp; assets.notna()
    dep.loc[dep_valid] = (capex.loc[dep_valid].abs() - ncfo.loc[dep_valid]) / assets.loc[dep_valid]
    stdebt_valid = valid_assets &amp; debtc.notna() &amp; assets.notna()
    stdebt.loc[stdebt_valid] = debtc.loc[stdebt_valid] / assets.loc[stdebt_valid]
    base_missing = (
        (~valid_assets)
        | capex.isna()
        | ncfo.isna()
        | assets.isna()
        | debtc.isna()
        | macro_chg.isna()
    )
    dep = dep.where(~base_missing)
    stdebt = stdebt.where(~base_missing)
    def cross_section_z(group: pd.Series) -&gt; pd.Series:
        n_valid = group.notna().sum()
        if n_valid &lt; 20:
            return pd.Series(np.nan, index=group.index)
        return _zscore(group)
    dep_z = dep.groupby(level='date').apply(cross_section_z)
    dep_z.index = dep_z.index.droplevel(0) if isinstance(dep_z.index, pd.MultiIndex) and dep_z.index.nlevels &gt; panel.index.nlevels else dep_z.index
    stdebt_z = stdebt.groupby(level='date').apply(cross_section_z)
    stdebt_z.index = stdebt_z.index.droplevel(0) if isinstance(stdebt_z.index, pd.MultiIndex) and stdebt_z.index.nlevels &gt; panel.index.nlevels else stdebt_z.index
    need = dep_z + stdebt_z
    tight = np.tanh(macro_chg / 0.5)
    raw_signal = -need * tight
    raw_signal = raw_signal.where(~base_missing)
    return raw_signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>inventory_growth_gap</code><span class="la-tag">investment</span><span class="gist">Gap between inventory growth and sales growth over trailing 12 months</span></summary><div class="la-sig-body"><p>Gap between inventory growth and sales growth over trailing 12 months. High inventory growth relative to sales growth signals over-optimistic demand forecasts, future write-downs, and margin pressure, predicting negative future abnormal returns.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    inventory = panel['inventory']
    revenue = panel['revenue']
    assets = panel['assets']
    inv_l12 = inventory.groupby(level='ticker').shift(12)
    rev_l12 = revenue.groupby(level='ticker').shift(12)
    inv_g = pd.Series(np.nan, index=panel.index)
    valid_inv = inv_l12 &gt; 0
    inv_g[valid_inv] = inventory[valid_inv] / inv_l12[valid_inv] - 1.0
    sales_g = pd.Series(np.nan, index=panel.index)
    valid_rev = rev_l12 &gt; 0
    sales_g[valid_rev] = revenue[valid_rev] / rev_l12[valid_rev] - 1.0
    inv_g_clipped = inv_g.clip(-1, 3)
    sales_g_clipped = sales_g.clip(-1, 3)
    raw = inv_g_clipped - sales_g_clipped
    valid_mask = (
        (assets &gt; 0)
        &amp; (inventory &gt; 0)
        &amp; (inv_l12 &gt; 0)
        &amp; (revenue &gt; 0)
        &amp; (rev_l12 &gt; 0)
    )
    raw = raw.where(valid_mask, np.nan)
    return raw</pre></div></details><details class="la-sig"><summary><code>late_quarter_buyback_share_shrinkage</code><span class="la-tag">investment</span><span class="gist">Signal detects late-quarter share buybacks by comparing weighted-average shares to point-in-time basic shares…</span></summary><div class="la-sig-body"><p>Signal detects late-quarter share buybacks by comparing weighted-average shares to point-in-time basic shares outstanding: sharesbas materially below shareswa implies repurchases concentrated late in the quarter, not yet reflected in annual issuance measures. The signal is confirmed with actual cash outflow for equity (negative ncfcommon relative to market cap) to filter mechanical share-count artifacts.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    shareswa = panel['shareswa']
    sharesbas = panel['sharesbas']
    ncfcommon = panel['ncfcommon']
    marketcap = panel['marketcap']
    valid_shares = (shareswa &gt; 0) &amp; (sharesbas &gt; 0)
    shrink = (shareswa - sharesbas) / shareswa.where(shareswa &gt; 0)
    shrink = shrink.where(valid_shares)
    cash_conf = (-ncfcommon / marketcap.where(marketcap &gt; 0))
    cash_conf = cash_conf.where((marketcap &gt; 0) &amp; ncfcommon.notna(), 0.0)
    cash_conf = cash_conf.clip(-0.25, 0.25)
    signal = shrink + 0.5 * cash_conf
    invalid = shrink.isna() | (shrink.abs() &gt; 0.5)
    signal = signal.where(~invalid, np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>net_share_issuance</code><span class="la-tag">investment</span><span class="gist">YoY growth in shares outstanding; net issuers underperform, repurchasers outperform</span></summary><div class="la-sig-body"><p>YoY growth in shares outstanding; net issuers underperform, repurchasers outperform.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the low scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    shares = panel["sharesbas"].where(panel["sharesbas"] &gt; 0)
    return _yoy(shares)</pre></div></details><details class="la-sig"><summary><code>payout_funding_gap</code><span class="la-tag">investment</span><span class="gist">Payout-versus-free-cash-flow funding gap: payout = max(-ncfdiv,0) + max(-ncfcommon,0); gap = (payout - fcf) /…</span></summary><div class="la-sig-body"><p>Payout-versus-free-cash-flow funding gap: payout = max(-ncfdiv,0) + max(-ncfcommon,0); gap = (payout - fcf) / assets, winsorized at 1/99% per date. Firms paying out far more cash than their free cash flow supports are unlikely to sustain the distribution and tend to underperform; self-funded payers (negative gap) tend to outperform.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ncfdiv = panel['ncfdiv']
    ncfcommon = panel['ncfcommon']
    fcf = panel['fcf']
    assets = panel['assets']
    valid = ncfdiv.notna() &amp; ncfcommon.notna() &amp; fcf.notna() &amp; assets.notna() &amp; (assets &gt; 0)
    payout = (-ncfdiv).clip(lower=0) + (-ncfcommon).clip(lower=0)
    gap = (payout - fcf) / assets
    gap = gap.where(valid, np.nan)
    def _winsorize(s: pd.Series) -&gt; pd.Series:
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    gap = gap.groupby(level='date').transform(_winsorize)
    return gap.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>unproductive_acquisition_growth</code><span class="la-tag">investment</span><span class="gist">Growth in goodwill/intangibles (acquisition footprint) net of revenue growth</span></summary><div class="la-sig-body"><p>Growth in goodwill/intangibles (acquisition footprint) net of revenue growth. High acquisition-driven balance-sheet expansion unaccompanied by revenue growth signals overpayment/empire-building and predicts lower future returns.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    intangibles = panel['intangibles']
    assets = panel['assets']
    revenue = panel['revenue']
    grp = panel.index.get_level_values('ticker')
    lag_intangibles = intangibles.groupby(grp).shift(12)
    lag_assets = assets.groupby(grp).shift(12)
    lag_revenue = revenue.groupby(grp).shift(12)
    acq = (intangibles - lag_intangibles) / lag_assets
    acq = acq.where(lag_assets &gt; 0, np.nan)
    acq = acq.where(intangibles.notna() &amp; lag_intangibles.notna(), np.nan)
    acq = acq.clip(-1, 1)
    rev_g = (revenue - lag_revenue) / lag_revenue.abs()
    rev_g = rev_g.where(lag_revenue &gt; 0, np.nan)
    rev_g = rev_g.where(revenue.notna() &amp; lag_revenue.notna(), np.nan)
    rev_g = rev_g.clip(-2, 2)
    df = pd.DataFrame({'acq': acq, 'rev_g': rev_g})
    dates = panel.index.get_level_values('date')
    def cs_zscore(s: pd.Series) -&gt; pd.Series:
        mean = s.groupby(dates).transform('mean')
        std = s.groupby(dates).transform('std')
        z = (s - mean) / std
        return z.where(std &gt; 0, np.nan)
    z_acq = cs_zscore(df['acq'])
    z_rev = cs_zscore(df['rev_g'])
    signal = z_acq - z_rev
    signal = signal.where(df['acq'].notna() &amp; df['rev_g'].notna(), np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>credit_beta_defensive</code><span class="la-tag">low_risk</span><span class="gist">Cross-asset correlation factor: prefer defensive stocks least hurt by credit-spread widening (higher corr to…</span></summary><div class="la-sig-body"><p>Cross-asset correlation factor: prefer defensive stocks least hurt by credit-spread widening (higher corr to spread changes).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    return panel["corr_credit_252"]</pre></div></details><details class="la-sig"><summary><code>credit_beta_leverage_mismatch</code><span class="la-tag">low_risk</span><span class="gist">Cross-asset residual comparing equity's market-implied distress sensitivity (negative of credit beta) to…</span></summary><div class="la-sig-body"><p>Cross-asset residual comparing equity's market-implied distress sensitivity (negative of credit beta) to balance-sheet net leverage. Stocks whose equity underprices credit risk relative to true leverage (implied distress low, leverage high) are expected to underperform; the reverse mismatch is expected to earn a premium.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta_credit = panel['beta_credit_252']
    corr_credit = panel['corr_credit_252']
    debt = panel['debt']
    cashneq = panel['cashneq']
    assets = panel['assets']
    valid_beta_corr = beta_credit.notna() &amp; corr_credit.notna()
    implied_distress = -beta_credit
    implied_distress = implied_distress.where(
        valid_beta_corr &amp; (corr_credit.abs() &gt;= 0.02)
    )
    assets_valid = assets.notna() &amp; (assets &gt; 0)
    netlev = (debt - cashneq) / assets
    netlev = netlev.where(assets_valid)
    netlev = netlev.clip(lower=-1.0, upper=2.0)
    valid = valid_beta_corr &amp; assets_valid
    implied_distress = implied_distress.where(valid)
    netlev = netlev.where(valid)
    def zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.replace(0, np.nan)
    z1 = zscore(implied_distress)
    z2 = zscore(netlev)
    signal = z1 - z2
    signal = signal.where(z1.notna() &amp; z2.notna())
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>leverage_credit_beta_complacency</code><span class="la-tag">low_risk</span><span class="gist">Interaction of balance-sheet net leverage with complacency toward credit risk (low absolute correlation to…</span></summary><div class="la-sig-body"><p>Interaction of balance-sheet net leverage with complacency toward credit risk (low absolute correlation to credit-spread changes). Highly levered firms whose equity returns are largely uncorrelated with credit conditions are hypothesized to harbor unpriced distress risk, predicting subsequent underperformance.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debt = panel['debt']
    cashneq = panel['cashneq']
    assets = panel['assets']
    corr_credit = panel['corr_credit_252']
    valid_assets = assets &gt; 0
    netlev = (debt - cashneq) / assets.where(valid_assets)
    netlev = netlev.where(valid_assets)
    complacency = 1.0 - corr_credit.abs()
    complacency = complacency.clip(lower=0.0, upper=1.0)
    # drop rows missing required inputs
    base_valid = debt.notna() &amp; cashneq.notna() &amp; assets.notna() &amp; valid_assets &amp; corr_credit.notna()
    netlev = netlev.where(base_valid)
    complacency = complacency.where(base_valid)
    lev_r = netlev.groupby(level='date').rank(pct=True)
    comp_r = complacency.groupby(level='date').rank(pct=True)
    signal = lev_r * comp_r
    signal = signal.where(lev_r.notna() &amp; comp_r.notna())
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>lottery_demand_sentiment_interaction</code><span class="la-tag">low_risk</span><span class="gist">Interaction of a lottery-stock proxy (high vol, low nominal price, small size) with a publication-lagged…</span></summary><div class="la-sig-body"><p>Interaction of a lottery-stock proxy (high vol, low nominal price, small size) with a publication-lagged consumer-sentiment state variable. Lottery stocks are expected to be overpriced (and thus underperform) when sentiment/optimism is high, with the effect attenuating or reversing after prolonged pessimism.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    vol_252 = panel['vol_252']
    closeunadj = panel['closeunadj']
    marketcap = panel['marketcap']
    log_price = np.log(closeunadj.where(closeunadj &gt; 0))
    log_mcap = np.log(marketcap.where(marketcap &gt; 0))
    neg_log_price = -log_price
    neg_log_mcap = -log_mcap
    df = pd.DataFrame({
        'vol_252': vol_252,
        'neg_log_price': neg_log_price,
        'neg_log_mcap': neg_log_mcap,
    }, index=panel.index)
    r_vol = df.groupby(level='date')['vol_252'].transform(_rank_scaled)
    r_price = df.groupby(level='date')['neg_log_price'].transform(_rank_scaled)
    r_mcap = df.groupby(level='date')['neg_log_mcap'].transform(_rank_scaled)
    lottery = r_vol + r_price + r_mcap
    lottery = lottery.where(
        vol_252.notna() &amp; (closeunadj &gt; 0) &amp; (marketcap &gt; 0)
    )
    # date-level sentiment series
    umcsent = panel['macro_umcsent'].groupby(level='date').first()
    umcsent_chg12m = panel['macro_umcsent_chg12m'].groupby(level='date').first()
    umcsent = umcsent.sort_index()
    umcsent_chg12m = umcsent_chg12m.sort_index()
    # backward-looking: use shift(1) before rolling to avoid look-ahead
    umcsent_lag = umcsent.shift(1)
    roll_mean = umcsent_lag.rolling(120, min_periods=36).mean()
    roll_std = umcsent_lag.rolling(120, min_periods=36).std()
    sz = (umcsent - roll_mean) / roll_std
    sz = sz.clip(-2, 2)
    chg_lag = umcsent_chg12m.shift(1)
    chg_roll_mean = chg_lag.rolling(120, min_periods=36).mean()
    chg_roll_std = chg_lag.rolling(120, min_periods=36).std()
    chg_z = (umcsent_chg12m - chg_roll_mean) / chg_roll_std
    chg_z = chg_z.clip(-2, 2)
    sent_state = 0.5 * sz + 0.5 * chg_z
    # broadcast sent_state to panel index
    sent_state_full = panel.index.get_level_values('date').map(sent_state)
    sent_state_full = pd.Series(sent_state_full, index=panel.index)
    score = -lottery * sent_state_full
    valid = (
        lottery.notna()
        &amp; sent_state_full.notna()
        &amp; panel['macro_umcsent'].notna()
        &amp; panel['macro_umcsent_chg12m'].notna()
    )
    score = score.where(valid)
    return score</pre></div></details><details class="la-sig"><summary><code>low_volatility</code><span class="la-tag">low_risk</span><span class="gist">Low-volatility anomaly: trailing 1y realized vol of daily returns (low = long)</span></summary><div class="la-sig-body"><p>Low-volatility anomaly: trailing 1y realized vol of daily returns (low = long).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    return panel["vol_252"]</pre></div></details><details class="la-sig"><summary><code>macro_beta_churn</code><span class="la-tag">low_risk</span><span class="gist">Measures churn/instability in a stock's trailing macro factor betas (credit, rates, oil, usd, vix) over 12…</span></summary><div class="la-sig-body"><p>Measures churn/instability in a stock's trailing macro factor betas (credit, rates, oil, usd, vix) over 12 months, cross-sectionally z-scored per factor per date and averaged. High churn indicates an unstable risk profile that predicts lower future risk-adjusted returns.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    factors = [
        'beta_credit_252',
        'beta_rates_252',
        'beta_oil_252',
        'beta_usd_252',
        'beta_vix_252',
    ]
    z_scores = []
    for f in factors:
        cur = panel[f]
        lag = cur.groupby(level='ticker').shift(12)
        d = (cur - lag).abs()
        d = d.where(cur.notna() &amp; lag.notna())
        std_by_date = d.groupby(level='date').transform('std')
        z = d / std_by_date.where(std_by_date &gt; 0)
        z_scores.append(z)
    z_df = pd.concat(z_scores, axis=1)
    z_df.columns = factors
    count = z_df.notna().sum(axis=1)
    mean_z = z_df.mean(axis=1, skipna=True)
    signal = mean_z.where(count &gt;= 3)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>macro_beta_churn_instability</code><span class="la-tag">low_risk</span><span class="gist">Measures instability of a stock's macro factor exposures (rates, credit, oil, usd, vix betas) over the…</span></summary><div class="la-sig-body"><p>Measures instability of a stock's macro factor exposures (rates, credit, oil, usd, vix betas) over the trailing 12 months. High churn in factor loadings implies ambiguous risk profile and speculative-flow-driven comovement, predicting underperformance; stable-exposure firms earn higher realized returns.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    factors = ['beta_rates_252', 'beta_credit_252', 'beta_oil_252',
               'beta_usd_252', 'beta_vix_252']
    s_list = []
    for f in factors:
        if f not in panel.columns:
            continue
        cur = panel[f]
        lag = panel[f].groupby(level='ticker').shift(12)
        d_f = (cur - lag).abs()
        d_f = d_f.where(cur.notna() &amp; lag.notna())
        med = d_f.groupby(level='date').transform('median')
        s_f = d_f / med.where(med &gt; 0)
        s_list.append(s_f)
    if not s_list:
        return pd.Series(np.nan, index=panel.index)
    s_df = pd.concat(s_list, axis=1)
    count = s_df.notna().sum(axis=1)
    churn = s_df.mean(axis=1, skipna=True)
    churn = churn.where(count &gt;= 3)
    return churn.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>macro_beta_instability</code><span class="la-tag">low_risk</span><span class="gist">Measures instability of a firm's macro-exposure vector (rates, oil, usd, credit, vix, market beta) over the…</span></summary><div class="la-sig-body"><p>Measures instability of a firm's macro-exposure vector (rates, oil, usd, credit, vix, market beta) over the trailing 12 months, using the mean cross-sectional percentile rank of the absolute 12-month change across available beta fields (requires &gt;=4 available). High instability signals an unstable, hard-to-hedge risk profile associated with lower future returns.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    fields = [
        'beta_rates_252',
        'beta_oil_252',
        'beta_usd_252',
        'beta_credit_252',
        'beta_vix_252',
        'beta_252',
    ]
    rank_frames = []
    for f in fields:
        if f not in panel.columns:
            continue
        current = panel[f]
        lagged = current.groupby(level='ticker').shift(12)
        diff = (current - lagged).abs()
        valid = current.notna() &amp; lagged.notna()
        diff = diff.where(valid)
        rank = diff.groupby(level='date').rank(pct=True)
        rank_frames.append(rank)
    if not rank_frames:
        return pd.Series(np.nan, index=panel.index)
    rank_df = pd.concat(rank_frames, axis=1)
    count_valid = rank_df.notna().sum(axis=1)
    mean_rank = rank_df.mean(axis=1)
    score = mean_rank.where(count_valid &gt;= 4)
    score.name = self.name
    return score</pre></div></details><details class="la-sig"><summary><code>macro_beta_instability_ambiguity</code><span class="la-tag">low_risk</span><span class="gist">Instability of a stock's cross-asset macro betas (oil, usd, rates, credit, vix) over the trailing 12 months,…</span></summary><div class="la-sig-body"><p>Instability of a stock's cross-asset macro betas (oil, usd, rates, credit, vix) over the trailing 12 months, standardized by cross-sectional dispersion each date. Stocks with unstable macro exposures have opaque/changing risk profiles and tend to underperform, so a higher instability score predicts LOWER returns (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    eps = 1e-8
    factors = [
        'beta_oil_252',
        'beta_usd_252',
        'beta_rates_252',
        'beta_credit_252',
        'beta_vix_252',
    ]
    deltas = []
    for f in factors:
        if f not in panel.columns:
            continue
        b = panel[f]
        b_lag = b.groupby(level='ticker').shift(12)
        valid = b.notna() &amp; b_lag.notna()
        abs_chg = (b - b_lag).abs()
        cs_std = abs_chg.where(valid).groupby(level='date').transform('std')
        d = abs_chg / (cs_std + eps)
        d = d.where(valid)
        deltas.append(d)
    if not deltas:
        return pd.Series(np.nan, index=panel.index)
    deltas_df = pd.concat(deltas, axis=1)
    valid_count = deltas_df.notna().sum(axis=1)
    instability = deltas_df.mean(axis=1, skipna=True)
    instability = instability.where(valid_count &gt;= 3)
    return instability.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>macro_beta_rotation_instability</code><span class="la-tag">low_risk</span><span class="gist">Measures instability (rotation) of a firm's vector of macro betas (credit, oil, rates, usd, vix) over a…</span></summary><div class="la-sig-body"><p>Measures instability (rotation) of a firm's vector of macro betas (credit, oil, rates, usd, vix) over a trailing 12-month window. Firms whose macro exposure vector rotates sharply are opaque and subject to parameter/model uncertainty, generating mispricing that on average resolves adversely, leading to lower subsequent returns.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta_cols = [
        'beta_credit_252',
        'beta_oil_252',
        'beta_rates_252',
        'beta_usd_252',
        'beta_vix_252',
    ]
    def winsorized_zscore(s: pd.Series) -&gt; pd.Series:
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        z = (s - mean) / std.replace(0, np.nan)
        z = z.clip(-4, 4)
        return z
    z_diffs = []
    missing_flags = []
    for col in beta_cols:
        if col not in panel.columns:
            # treat as fully missing component
            nan_series = pd.Series(np.nan, index=panel.index)
            z_diffs.append(nan_series)
            missing_flags.append(pd.Series(True, index=panel.index))
            continue
        raw = panel[col]
        valid_mask = raw.notna()
        z = pd.Series(np.nan, index=panel.index)
        z.loc[valid_mask] = winsorized_zscore(raw.loc[valid_mask])
        z_lag = z.groupby(level='ticker').shift(12)
        missing = z.isna() | z_lag.isna()
        diff_sq = (z - z_lag) ** 2
        z_diffs.append(diff_sq)
        missing_flags.append(missing)
    missing_count = pd.concat(missing_flags, axis=1).sum(axis=1)
    diff_matrix = pd.concat(z_diffs, axis=1)
    # sum of squared diffs, treating missing contributions as 0 (only allowed if &lt;=1 missing)
    diff_matrix_filled = diff_matrix.fillna(0.0)
    sum_sq = diff_matrix_filled.sum(axis=1)
    churn = np.sqrt(sum_sq)
    churn[missing_count &gt; 1] = np.nan
    churn.name = self.name
    return churn</pre></div></details><details class="la-sig"><summary><code>macro_exposure_drift</code><span class="la-tag">low_risk</span><span class="gist">Measures the drift in a stock's macro exposure profile (rates, oil, USD, credit, VIX betas) over the trailing…</span></summary><div class="la-sig-body"><p>Measures the drift in a stock's macro exposure profile (rates, oil, USD, credit, VIX betas) over the trailing 12 months via cross-sectional z-score changes. Stocks with unstable macro exposures are harder to model and attract speculative demand, leading to subsequent underperformance.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    betas = [
        'beta_rates_252',
        'beta_oil_252',
        'beta_usd_252',
        'beta_credit_252',
        'beta_vix_252',
    ]
    drift_terms = []
    for b in betas:
        col = panel[b]
        # cross-sectional zscore within each date
        grp = col.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        z = (col - mean) / std
        z = z.where(std &gt; 0, np.nan)
        z = z.clip(lower=-4, upper=4)
        z_lag = z.groupby(level='ticker').shift(12)
        d = (z - z_lag).abs()
        drift_terms.append(d)
    drift_df = pd.concat(drift_terms, axis=1)
    drift_df.columns = betas
    nonnull_count = drift_df.notna().sum(axis=1)
    avg_drift = drift_df.mean(axis=1, skipna=True)
    signal = avg_drift.where(nonnull_count &gt;= 3, np.nan)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>macro_exposure_instability</code><span class="la-tag">low_risk</span><span class="gist">Measures instability of a stock's macro factor betas (oil, rates, usd, credit, vix) over a 12-month horizon</span></summary><div class="la-sig-body"><p>Measures instability of a stock's macro factor betas (oil, rates, usd, credit, vix) over a 12-month horizon. Stocks with unstable macro exposures are hard to hedge/value and earn lower subsequent returns; stable macro-exposure stocks earn higher returns (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    factors = [
        'beta_oil_252',
        'beta_rates_252',
        'beta_usd_252',
        'beta_credit_252',
        'beta_vix_252',
    ]
    s_list = []
    for f in factors:
        if f not in panel.columns:
            continue
        cur = panel[f]
        lag = panel[f].groupby(level='ticker').shift(12)
        d_f = (cur - lag).abs()
        d_f = d_f.where(cur.notna() &amp; lag.notna())
        # cross-sectional median of |d_f| per date
        med = d_f.groupby(level='date').transform('median')
        mad = (d_f - med).abs()
        mad_med = mad.groupby(level='date').transform('median')
        s_f = d_f / mad_med.where(mad_med &gt; 0)
        s_f = s_f.where(mad_med &gt; 0)
        s_list.append(s_f)
    if len(s_list) == 0:
        return pd.Series(np.nan, index=panel.index)
    s_df = pd.concat(s_list, axis=1)
    count_avail = s_df.notna().sum(axis=1)
    instab = s_df.mean(axis=1, skipna=True)
    instab = instab.where(count_avail &gt;= 3)
    # orthogonalization term: date-level cross-sectional rank of vol_252
    if 'vol_252' in panel.columns:
        vol = panel['vol_252']
        vol_rank = vol.groupby(level='date').rank(pct=True)
        vol_rank = vol_rank.fillna(0.0)
    else:
        vol_rank = pd.Series(0.0, index=panel.index)
    score = instab - 0.5 * vol_rank
    score = score.where(instab.notna())
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>macro_loading_instability</code><span class="la-tag">low_risk</span><span class="gist">Measures the time variation of a stock's cross-asset macro loading vector (corr to credit, oil, rates, usd,…</span></summary><div class="la-sig-body"><p>Measures the time variation of a stock's cross-asset macro loading vector (corr to credit, oil, rates, usd, vix) over the trailing year. Firms with unstable macro exposures have opaque risk profiles and tend to underperform relative to firms with stable macro loadings.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    corr_cols = [
        'corr_credit_252',
        'corr_oil_252',
        'corr_rates_252',
        'corr_usd_252',
        'corr_vix_252',
    ]
    grp = panel.groupby(level='ticker')
    d_abs_list = []
    for c in corr_cols:
        if c not in panel.columns:
            continue
        lagged = grp[c].shift(12)
        d_abs = (panel[c] - lagged).abs()
        d_abs_list.append(d_abs)
    d_df = pd.concat(d_abs_list, axis=1)
    d_df.columns = range(len(d_abs_list))
    valid_count = d_df.notna().sum(axis=1)
    instability = d_df.mean(axis=1, skipna=True)
    score = instability.where(
        (valid_count &gt;= 3) &amp; panel['vol_252'].notna()
    )
    if 'sector' in panel.columns:
        date_level = panel.index.get_level_values('date')
        sector_vals = panel['sector']
        group_keys = [date_level, sector_vals]
        sector_median = score.groupby(group_keys).transform('median')
        score = score - sector_median
    score = score.reindex(panel.index)
    score.name = self.name
    return score</pre></div></details><details class="la-sig"><summary><code>macro_systematic_variance_share</code><span class="la-tag">low_risk</span><span class="gist">Systematic variance share: sum of squared trailing 252d correlations of a stock's returns to five macro…</span></summary><div class="la-sig-body"><p>Systematic variance share: sum of squared trailing 252d correlations of a stock's returns to five macro factors (credit, rates, oil, usd, vix), clipped to [0,1]. Low share indicates idiosyncratic, opaque, lottery-like names that historically underperform (Ang et al., Durnev/Morck).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = [
        'corr_credit_252',
        'corr_rates_252',
        'corr_oil_252',
        'corr_usd_252',
        'corr_vix_252',
    ]
    valid = pd.Series(True, index=panel.index)
    for c in cols:
        valid &amp;= panel[c].notna()
    nobs_ok = panel['nobs'] &gt;= 200
    share = pd.Series(0.0, index=panel.index)
    for c in cols:
        share = share + panel[c].fillna(0) ** 2
    share = share.clip(lower=0, upper=1)
    score = share.where(valid &amp; nobs_ok)
    return score</pre></div></details><details class="la-sig"><summary><code>rate_duration_mispricing</code><span class="la-tag">low_risk</span><span class="gist">Shorts hidden long-duration equities (high PB, low yield, high realized rate beta) during rising-yield…</span></summary><div class="la-sig-body"><p>Shorts hidden long-duration equities (high PB, low yield, high realized rate beta) during rising-yield regimes and reverses during falling-yield regimes, by comparing realized rate beta to fundamental duration implied by valuation/dividend structure.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    pb = panel['pb']
    divyield = panel['divyield']
    beta_rates = panel['beta_rates_252']
    macro_chg = panel['macro_dgs10_chg12m']
    pb_valid = pb.where(pb &gt; 0)
    log_pb = np.log(pb_valid)
    def cs_zscore(s: pd.Series) -&gt; pd.Series:
        grouped = s.groupby(level='date')
        mean = grouped.transform('mean')
        std = grouped.transform('std')
        z = (s - mean) / std
        return z.where(std &gt; 0)
    log_pb_z = cs_zscore(log_pb)
    # only include divyield in z-score for rows with valid pb
    divyield_masked = divyield.where(pb_valid.notna())
    divyield_z = cs_zscore(divyield_masked)
    dur_z = log_pb_z - divyield_z
    implied_beta = -dur_z
    beta_rates_masked = beta_rates.where(pb_valid.notna())
    beta_rates_z = cs_zscore(beta_rates_masked)
    mismatch = beta_rates_z - implied_beta
    rate_trend = np.tanh(macro_chg)
    signal = -mismatch * rate_trend
    # apply missing-data policy
    valid_mask = pb_valid.notna() &amp; divyield.notna() &amp; beta_rates.notna() &amp; macro_chg.notna()
    signal = signal.where(valid_mask)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>residual_vix_hedge_premium</code><span class="la-tag">low_risk</span><span class="gist">Residual VIX beta after removing market-beta component via per-date cross-sectional OLS of beta_vix_252 on…</span></summary><div class="la-sig-body"><p>Residual VIX beta after removing market-beta component via per-date cross-sectional OLS of beta_vix_252 on [1, beta_252]. High residual VIX beta stocks act as crash hedges and are bid up, earning lower subsequent returns.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = ['beta_vix_252', 'beta_252']
    df = panel[cols].copy()
    result = df.groupby(level='date', group_keys=False).apply(self._fit_resid)
    result = result.reindex(panel.index)
    result.name = self.name
    return result</pre></div></details><details class="la-sig"><summary><code>unpriced_leverage_credit_insensitivity</code><span class="la-tag">low_risk</span><span class="gist">Mismatch between balance-sheet leverage and realized equity credit-beta: highly levered firms whose equity…</span></summary><div class="la-sig-body"><p>Mismatch between balance-sheet leverage and realized equity credit-beta: highly levered firms whose equity shows low sensitivity to credit spread moves are 'credit-complacent' and prone to sharp repricing when credit conditions tighten. Score = z(net leverage) - z(-beta_credit_252).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debt = panel['debt']
    cashneq = panel['cashneq']
    assets = panel['assets']
    beta_credit = panel['beta_credit_252']
    corr_credit = panel['corr_credit_252']
    valid = (
        assets.notna() &amp; (assets &gt; 0)
        &amp; debt.notna() &amp; cashneq.notna()
        &amp; beta_credit.notna() &amp; corr_credit.notna()
    )
    netlev = (debt - cashneq) / assets
    netlev = netlev.clip(-1, 2)
    credit_exposure = -beta_credit
    netlev_v = netlev.where(valid)
    credit_exposure_v = credit_exposure.where(valid)
    def zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.replace(0, np.nan)
    zl = zscore(netlev_v)
    zc = zscore(credit_exposure_v)
    score = zl - zc
    score = score.where(valid)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>vix_beta_drift</code><span class="la-tag">low_risk</span><span class="gist">Signal based on the 12-month drift in trailing VIX beta, scaled by realized volatility</span></summary><div class="la-sig-body"><p>Signal based on the 12-month drift in trailing VIX beta, scaled by realized volatility. A deteriorating (more negative) VIX beta signals quietly accumulating crash exposure not yet priced in, predicting underperformance; an improving VIX beta signals a cheap hedge, predicting outperformance. Restricted to stocks with meaningful trailing correlation to VIX (|corr_vix_252| &gt;= 0.02).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta_vix = panel['beta_vix_252']
    corr_vix = panel['corr_vix_252']
    vol_252 = panel['vol_252']
    beta_vix_lag12 = beta_vix.groupby(level='ticker').shift(12)
    dbeta = beta_vix - beta_vix_lag12
    scale = vol_252.clip(lower=0.05, upper=2.0)
    signal = dbeta / scale
    valid = (
        beta_vix.notna()
        &amp; beta_vix_lag12.notna()
        &amp; corr_vix.notna()
        &amp; vol_252.notna()
        &amp; (vol_252 &gt; 0)
        &amp; (corr_vix.abs() &gt;= 0.02)
    )
    signal = signal.where(valid, np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>annual_return_seasonality_purged</code><span class="la-tag">momentum</span><span class="gist">Heston-Sadka style calendar-month seasonality: average of same-calendar-month returns at 1-5 year annual…</span></summary><div class="la-sig-body"><p>Heston-Sadka style calendar-month seasonality: average of same-calendar-month returns at 1-5 year annual lags, purged by subtracting the average of adjacent (non-annual) lags to remove generic momentum/reversal, isolating pure seasonal return predictability.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    annual_lags = [12, 24, 36, 48, 60]
    adjacent_lags = [11, 13, 23, 25, 35, 37]
    annual_shifts = [grp.shift(k) for k in annual_lags]
    adjacent_shifts = [grp.shift(k) for k in adjacent_lags]
    annual_df = pd.concat(annual_shifts, axis=1)
    adjacent_df = pd.concat(adjacent_shifts, axis=1)
    annual_count = annual_df.notna().sum(axis=1)
    seas_mean = annual_df.mean(axis=1, skipna=True)
    nonseas_mean = adjacent_df.mean(axis=1, skipna=True)
    signal = seas_mean - nonseas_mean
    signal = signal.where(annual_count &gt;= 3)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>cross_asset_news_diffusion_momentum</code><span class="la-tag">momentum</span><span class="gist">Combines trailing macro-factor loadings (beta_usd_252, beta_oil_252, beta_rates_252 gated by correlation…</span></summary><div class="la-sig-body"><p>Combines trailing macro-factor loadings (beta_usd_252, beta_oil_252, beta_rates_252 gated by correlation strength) with recent standardized macro shocks (USD, oil, 10y rate) to capture slow diffusion of cross-asset news into stock prices.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel
    def date_level_shock(chg_series):
        # date-level series (same value across tickers per date)
        date_level = chg_series.groupby(level='date').first()
        roll_std = date_level.rolling(36, min_periods=18).std()
        sh_date = date_level / roll_std.replace(0, np.nan)
        # map back to (date,ticker)
        dates = chg_series.index.get_level_values('date')
        return sh_date.reindex(dates).to_numpy()
    # USD shock: pct change over 3 months
    usd_shift3 = df.groupby(level='ticker')['macro_usd'].shift(3)
    chg_usd = df['macro_usd'] / usd_shift3.replace(0, np.nan) - 1
    sh_usd = pd.Series(date_level_shock(chg_usd), index=df.index)
    # Oil shock: pct change over 3 months
    oil_shift3 = df.groupby(level='ticker')['macro_oil'].shift(3)
    chg_oil = df['macro_oil'] / oil_shift3.replace(0, np.nan) - 1
    sh_oil = pd.Series(date_level_shock(chg_oil), index=df.index)
    # Rates shock: level difference over 3 months
    rates_shift3 = df.groupby(level='ticker')['macro_dgs10'].shift(3)
    chg_rates = df['macro_dgs10'] - rates_shift3
    sh_rates = pd.Series(date_level_shock(chg_rates), index=df.index)
    def loading(beta_col, corr_col):
        beta = df[beta_col]
        corr = df[corr_col]
        valid = corr.abs() &gt; 0.15
        load = beta.where(valid, 0.0)
        load = load.where(beta.notna() &amp; corr.notna(), np.nan)
        return load
    load_usd = loading('beta_usd_252', 'corr_usd_252')
    load_oil = loading('beta_oil_252', 'corr_oil_252')
    load_rates = loading('beta_rates_252', 'corr_rates_252')
    term_usd = load_usd * sh_usd
    term_oil = load_oil * sh_oil
    term_rates = load_rates * sh_rates
    terms = pd.concat([term_usd, term_oil, term_rates], axis=1)
    terms.columns = ['usd', 'oil', 'rates']
    # a term contributes 0 if its inputs are missing/invalid, but overall
    # signal is NaN if ALL three loadings are missing
    loadings_all_nan = load_usd.isna() &amp; load_oil.isna() &amp; load_rates.isna()
    signal = terms.fillna(0.0).sum(axis=1)
    signal = signal.where(~loadings_all_nan, np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>frog_in_the_pan_gradual_momentum</code><span class="la-tag">momentum</span><span class="gist">Interacts 12-1 momentum with 'information discreteness' of the path: momentum accumulated via many small…</span></summary><div class="la-sig-body"><p>Interacts 12-1 momentum with 'information discreteness' of the path: momentum accumulated via many small same-sign monthly moves (gradual) is up-weighted, while momentum driven by a few large jumps is down-weighted, per Da, Gurun and Warachka's frog-in-the-pan hypothesis.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret_1m = panel["ret_1m"]
    ret_12_1 = panel["ret_12_1"]
    vol_252 = panel["vol_252"]
    grp = ret_1m.groupby(level="ticker")
    pos_sum = pd.Series(0.0, index=panel.index)
    count = pd.Series(0.0, index=panel.index)
    for k in range(1, 13):
        r_k = grp.shift(k)
        valid = r_k.notna()
        pos_sum = pos_sum + (r_k &gt; 0).astype(float).where(valid, 0.0)
        count = count + valid.astype(float)
    pos = pos_sum / count
    neg = 1.0 - pos
    enough = count &gt;= 9
    sign_ret = np.sign(ret_12_1)
    ID = sign_ret * (neg - pos)
    cont = -ID
    denom_ok = vol_252 &gt; 0
    base = ret_12_1 / vol_252.where(denom_ok, np.nan)
    # cross-sectional z-score of base, per date
    def zscore(s):
        mu = s.mean()
        sd = s.std()
        if sd is None or sd == 0 or pd.isna(sd):
            return pd.Series(np.nan, index=s.index)
        return (s - mu) / sd
    z = base.groupby(level="date").transform(zscore)
    score = z * (0.5 + cont)
    valid_mask = enough &amp; ret_12_1.notna() &amp; denom_ok &amp; base.notna() &amp; z.notna()
    score = score.where(valid_mask, np.nan)
    return score</pre></div></details><details class="la-sig"><summary><code>fundamental_momentum_price_gap</code><span class="la-tag">momentum</span><span class="gist">Gap between fundamental momentum (YoY change in gross profit and operating cash flow scaled by assets) and…</span></summary><div class="la-sig-body"><p>Gap between fundamental momentum (YoY change in gross profit and operating cash flow scaled by assets) and price momentum (6m and 12-1m returns). Positive gap indicates improved fundamentals without commensurate price appreciation, predicting underreaction drift; near-neutral to raw price momentum.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    gp = panel['gp']
    ncfo = panel['ncfo']
    assets = panel['assets']
    ret_6m = panel['ret_6m']
    ret_12_1 = panel['ret_12_1']
    gp_lag12 = gp.groupby(level='ticker').shift(12)
    ncfo_lag12 = ncfo.groupby(level='ticker').shift(12)
    valid_assets = assets &gt; 0
    dgp = (gp - gp_lag12) / assets.where(valid_assets)
    dcfo = (ncfo - ncfo_lag12) / assets.where(valid_assets)
    dgp = dgp.where(valid_assets &amp; gp_lag12.notna())
    dcfo = dcfo.where(valid_assets &amp; ncfo_lag12.notna())
    def cs_rank(s):
        return s.groupby(level='date').rank(pct=True)
    rank_dgp = cs_rank(dgp)
    rank_dcfo = cs_rank(dcfo)
    rank_ret6 = cs_rank(ret_6m)
    rank_ret12_1 = cs_rank(ret_12_1)
    fund_mom = pd.concat([rank_dgp, rank_dcfo], axis=1).mean(axis=1, skipna=True)
    price_mom = pd.concat([rank_ret6, rank_ret12_1], axis=1).mean(axis=1, skipna=True)
    signal = fund_mom - price_mom
    invalid = dgp.isna() &amp; dcfo.isna()
    signal = signal.where(~invalid)
    missing_price = ret_6m.isna() &amp; ret_12_1.isna()
    signal = signal.where(~missing_price)
    return signal</pre></div></details><details class="la-sig"><summary><code>gradual_information_momentum</code><span class="la-tag">momentum</span><span class="gist">Frog-in-the-pan style momentum: conditions 12-1 momentum on the gradualness of the underlying monthly return…</span></summary><div class="la-sig-body"><p>Frog-in-the-pan style momentum: conditions 12-1 momentum on the gradualness of the underlying monthly return path (Da-Gurun-Warachka information discreteness proxy). Momentum is weighted up for stocks with many small same-signed monthly moves (gradual info flow) and down for stocks whose 12-month return was driven by a few discrete jumps.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret_1m = panel["ret_1m"]
    ret_12_1 = panel["ret_12_1"]
    grouped = ret_1m.groupby(level="ticker")
    pos_count = None
    nonmiss_count = None
    for k in range(1, 13):
        lagged = grouped.shift(k)
        is_pos = (lagged &gt; 0).astype(float)
        is_nonmiss = lagged.notna().astype(float)
        is_pos = is_pos.where(lagged.notna(), 0.0)
        if pos_count is None:
            pos_count = is_pos
            nonmiss_count = is_nonmiss
        else:
            pos_count = pos_count + is_pos
            nonmiss_count = nonmiss_count + is_nonmiss
    pos_frac = pos_count / nonmiss_count.replace(0, np.nan)
    pos_frac = pos_frac.where(nonmiss_count &gt;= 9)
    sign_mom = np.sign(ret_12_1)
    ID = sign_mom * (1 - 2 * pos_frac)
    def cross_sectional_pct_rank(s: pd.Series) -&gt; pd.Series:
        return s.groupby(level="date").rank(pct=True, method="average")
    def cross_sectional_zscore(s: pd.Series) -&gt; pd.Series:
        mean = s.groupby(level="date").transform("mean")
        std = s.groupby(level="date").transform("std")
        z = (s - mean) / std.replace(0, np.nan)
        return z
    id_pct_rank = cross_sectional_pct_rank(ID)
    g = 1 - id_pct_rank
    mom_z = cross_sectional_zscore(ret_12_1)
    signal = mom_z * g
    signal = signal.where(ret_12_1.notna())
    signal = signal.where(pos_frac.notna())
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>momentum_12_1</code><span class="la-tag">momentum</span><span class="gist">Jegadeesh-Titman 12-1 month price momentum (skip most recent month)</span></summary><div class="la-sig-body"><p>Jegadeesh-Titman 12-1 month price momentum (skip most recent month).</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    return panel["ret_12_1"]</pre></div></details><details class="la-sig"><summary><code>pure_calendar_month_seasonality_spread</code><span class="la-tag">momentum</span><span class="gist">Seasonal spread: average of a stock's ret_1m in the same calendar month over prior years (lags 11,23,35,47,59…</span></summary><div class="la-sig-body"><p>Seasonal spread: average of a stock's ret_1m in the same calendar month over prior years (lags 11,23,35,47,59 months) minus the average of all trailing 60 monthly returns, isolating pure calendar-month seasonality net of the stock's unconditional mean return level.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [11, 23, 35, 47, 59]
    same_shifts = [grp.shift(k) for k in same_lags]
    same_df = pd.concat(same_shifts, axis=1)
    same_mean = same_df.mean(axis=1, skipna=True)
    same_count = same_df.notna().sum(axis=1)
    all_lags = list(range(0, 60))
    all_shifts = [grp.shift(k) for k in all_lags]
    all_df = pd.concat(all_shifts, axis=1)
    all_mean = all_df.mean(axis=1, skipna=True)
    all_count = all_df.notna().sum(axis=1)
    score = same_mean - all_mean
    valid = (same_count &gt;= 3) &amp; (all_count &gt;= 36)
    score = score.where(valid, np.nan)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>quiet_strength_low_attention_high</code><span class="la-tag">momentum</span><span class="gist">Interaction of proximity-to-52-week-high with low turnover (low dollar volume relative to market cap) and…</span></summary><div class="la-sig-body"><p>Interaction of proximity-to-52-week-high with low turnover (low dollar volume relative to market cap) and 12-1 momentum, capturing 'quiet strength': slow diffusion of good news in low-attention names near their highs, which should earn positive future drift versus high-turnover attention-saturated names near their highs.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    high_ratio = panel['high_ratio']
    dvol_21 = panel['dvol_21']
    marketcap = panel['marketcap']
    ret_12_1 = panel['ret_12_1']
    valid = (
        high_ratio.notna()
        &amp; dvol_21.notna()
        &amp; marketcap.notna()
        &amp; ret_12_1.notna()
        &amp; (marketcap &gt; 0)
        &amp; (dvol_21 &gt; 0)
    )
    turn = dvol_21 / marketcap.where(marketcap &gt; 0)
    log_turn = np.log(turn.where(turn &gt; 0))
    ret_clipped = ret_12_1.clip(-1, 3)
    def zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.replace(0, np.nan)
    zh = zscore(high_ratio.where(valid))
    zt = zscore(log_turn.where(valid))
    zm = zscore(ret_clipped.where(valid))
    score = 0.5 * zh + 0.5 * zm - zt
    score = score.where(valid)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>same_calendar_month_return_seasonality</code><span class="la-tag">momentum</span><span class="gist">Same-calendar-month return seasonality (Heston-Sadka): average of cross-sectionally winsorized same-month…</span></summary><div class="la-sig-body"><p>Same-calendar-month return seasonality (Heston-Sadka): average of cross-sectionally winsorized same-month lagged returns at 12,24,36,48,60 months back, requiring at least 3 non-null observations.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    lags = [12, 24, 36, 48, 60]
    ret = panel['ret_1m']
    grouped = ret.groupby(level='ticker')
    winsorized_lags = []
    for k in lags:
        lagged = grouped.shift(k)
        # cross-sectional winsorization at 1st/99th percentile, per date
        def _winsorize(s):
            lo = s.quantile(0.01)
            hi = s.quantile(0.99)
            return s.clip(lower=lo, upper=hi)
        wins = lagged.groupby(level='date').transform(_winsorize)
        winsorized_lags.append(wins)
    stacked = pd.concat(winsorized_lags, axis=1)
    stacked.columns = [f"lag_{k}" for k in lags]
    count_valid = stacked.notna().sum(axis=1)
    mean_val = stacked.mean(axis=1, skipna=True)
    signal = mean_val.where(count_valid &gt;= 3, np.nan)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>same_month_annual_seasonality</code><span class="la-tag">momentum</span><span class="gist">Firm-level same-calendar-month annual seasonality: average of a stock's ret_1m from the same calendar month…</span></summary><div class="la-sig-body"><p>Firm-level same-calendar-month annual seasonality: average of a stock's ret_1m from the same calendar month over the prior 1-5 years predicts current-month return (Heston &amp; Sadka seasonality effect).</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    shifts = []
    for k in range(1, 6):
        s_k = ret.groupby(level='ticker').shift(12 * k)
        shifts.append(s_k)
    stacked = pd.concat(shifts, axis=1)
    count_valid = stacked.notna().sum(axis=1)
    mean_val = stacked.mean(axis=1, skipna=True)
    signal = mean_val.where(count_valid &gt;= 3, np.nan)
    signal = signal.reindex(panel.index)
    return signal</pre></div></details><details class="la-sig"><summary><code>same_month_minus_offseason_seasonality</code><span class="la-tag">momentum</span><span class="gist">Firm-level calendar-month seasonality spread: mean of same-calendar-month historical 1m returns (at lags…</span></summary><div class="la-sig-body"><p>Firm-level calendar-month seasonality spread: mean of same-calendar-month historical 1m returns (at lags 12,24,36,48,60 months) minus mean of off-season average monthly return over the same lags. Isolates pure seasonal mispricing (Heston-Sadka / Keloharju-Linnainmaa-Nyberg).</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret_1m = panel['ret_1m']
    ret_12m = panel['ret_12m']
    lags = [12, 24, 36, 48, 60]
    same_list = []
    other_list = []
    for k in lags:
        same_k = ret_1m.groupby(level='ticker').shift(k)
        r12_k = ret_12m.groupby(level='ticker').shift(k)
        other_k = (r12_k - same_k) / 11.0
        valid = same_k.notna() &amp; r12_k.notna()
        same_k = same_k.where(valid)
        other_k = other_k.where(valid)
        same_list.append(same_k)
        other_list.append(other_k)
    same_df = pd.concat(same_list, axis=1)
    other_df = pd.concat(other_list, axis=1)
    count_available = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    other_mean = other_df.mean(axis=1)
    signal = same_mean - other_mean
    signal = signal.where(count_available &gt;= 2)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>same_month_seasonal_return_persistence</code><span class="la-tag">momentum</span><span class="gist">Averages a stock's ret_1m over the same calendar month in each of the past 5 years (lags of 12/24/36/48/60…</span></summary><div class="la-sig-body"><p>Averages a stock's ret_1m over the same calendar month in each of the past 5 years (lags of 12/24/36/48/60 months) to capture persistent calendar-month seasonality in cross-sectional returns, requiring at least 3 of 5 lagged observations to be non-NaN.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    lags = [g.shift(k) for k in (12, 24, 36, 48, 60)]
    lag_df = pd.concat(lags, axis=1)
    valid_count = lag_df.notna().sum(axis=1)
    seas_mean = lag_df.mean(axis=1, skipna=True)
    signal = seas_mean.where(valid_count &gt;= 3)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>same_month_seasonality_ex_adjacent</code><span class="la-tag">momentum</span><span class="gist">Stock-level calendar-month seasonality signal: average same-calendar-month return over the past 5 years (lags…</span></summary><div class="la-sig-body"><p>Stock-level calendar-month seasonality signal: average same-calendar-month return over the past 5 years (lags 12,24,36,48,60 months) minus the average return in adjacent non-seasonal months (lags 11,13,23,25,35,37), isolating pure seasonal return persistence from slow-moving level effects.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel["ret_1m"].groupby(level="ticker")
    seas_lags = [12, 24, 36, 48, 60]
    nonseas_lags = [11, 13, 23, 25, 35, 37]
    seas_shifts = [g.shift(k) for k in seas_lags]
    nonseas_shifts = [g.shift(k) for k in nonseas_lags]
    seas_df = pd.concat(seas_shifts, axis=1)
    nonseas_df = pd.concat(nonseas_shifts, axis=1)
    seas_count = seas_df.notna().sum(axis=1)
    nonseas_count = nonseas_df.notna().sum(axis=1)
    seas_mean = seas_df.mean(axis=1, skipna=True)
    nonseas_mean = nonseas_df.mean(axis=1, skipna=True)
    signal = seas_mean - nonseas_mean
    valid = (seas_count &gt;= 3) &amp; (nonseas_count &gt;= 1)
    signal = signal.where(valid, np.nan)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>same_month_seasonality_excess</code><span class="la-tag">momentum</span><span class="gist">Average same-calendar-month return over the past 5 years minus the average return of adjacent (off-annual)…</span></summary><div class="la-sig-body"><p>Average same-calendar-month return over the past 5 years minus the average return of adjacent (off-annual) month lags, isolating a repeating seasonal component in stock returns net of slow momentum and drift.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    annual_lags = [12, 24, 36, 48, 60]
    adjacent_lags = [11, 13, 23, 25, 35, 37, 47, 49, 59, 61]
    annual_shifts = [g.shift(k) for k in annual_lags]
    adjacent_shifts = [g.shift(k) for k in adjacent_lags]
    annual_df = pd.concat(annual_shifts, axis=1)
    annual_count = annual_df.notna().sum(axis=1)
    annual_mean = annual_df.mean(axis=1, skipna=True)
    annual_mean = annual_mean.where(annual_count &gt;= 3, np.nan)
    adjacent_df = pd.concat(adjacent_shifts, axis=1)
    adjacent_mean = adjacent_df.mean(axis=1, skipna=True)
    adjacent_mean = adjacent_mean.fillna(0.0)
    signal = annual_mean - adjacent_mean
    signal = signal.where(annual_mean.notna(), np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>seasonal_same_month_residual_return</code><span class="la-tag">momentum</span><span class="gist">Same-calendar-month seasonal return persistence: average of ret_1m at lags 11,23,35,47,59 months minus the…</span></summary><div class="la-sig-body"><p>Same-calendar-month seasonal return persistence: average of ret_1m at lags 11,23,35,47,59 months minus the trailing 60-month base mean of 1-month-lagged returns, isolating a purely seasonal component.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel["ret_1m"]
    grp = ret.groupby(level="ticker")
    same_month_lags = [11, 23, 35, 47, 59]
    shifted = [grp.shift(k) for k in same_month_lags]
    same_df = pd.concat(shifted, axis=1)
    same_month_count = same_df.notna().sum(axis=1)
    same_month = same_df.mean(axis=1)
    same_month = same_month.where(same_month_count &gt;= 3, np.nan)
    base_lag1 = grp.shift(1)
    base_grp = base_lag1.groupby(level="ticker")
    base_mean = base_grp.rolling(window=60, min_periods=36).mean()
    base_mean.index = base_mean.index.droplevel(0)
    base_mean = base_mean.reindex(ret.index)
    signal = same_month - base_mean
    signal = signal.where(same_month.notna() &amp; base_mean.notna(), np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>seasonal_same_month_return_persistence</code><span class="la-tag">momentum</span><span class="gist">Same-calendar-month seasonality in stock returns: uses average return in the same calendar month over the…</span></summary><div class="la-sig-body"><p>Same-calendar-month seasonality in stock returns: uses average return in the same calendar month over the past ~5 years (lags 11,23,35,47,59 months) relative to the average return over the other recent months (lags 1-10) to capture firm-specific seasonal return persistence.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    g = ret.groupby(level='ticker')
    seasonal_lags = [11, 23, 35, 47, 59]
    seasonal_shifts = [g.shift(k) for k in seasonal_lags]
    seasonal_df = pd.concat(seasonal_shifts, axis=1)
    seasonal_count = seasonal_df.notna().sum(axis=1)
    seas = seasonal_df.mean(axis=1)
    seas = seas.where(seasonal_count &gt;= 3)
    other_lags = list(range(1, 11))
    other_shifts = [g.shift(k) for k in other_lags]
    other_df = pd.concat(other_shifts, axis=1)
    other_count = other_df.notna().sum(axis=1)
    other = other_df.mean(axis=1)
    other = other.where(other_count &gt;= 1)
    score = seas - other
    score = score.where(seasonal_count &gt;= 3)
    score.index = panel.index
    return score</pre></div></details><details class="la-sig"><summary><code>tax_loss_overhang_turn_of_year</code><span class="la-tag">momentum</span><span class="gist">Tax-loss overhang proxy: distance from 52-week high scaled by volatility identifies stocks likely subject to…</span></summary><div class="la-sig-body"><p>Tax-loss overhang proxy: distance from 52-week high scaled by volatility identifies stocks likely subject to tax-motivated selling pressure in Nov/Dec, with mechanical reversal in January.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    req_cols = ['ret_12_1', 'marketcap', 'vol_252']
    df = panel[req_cols].copy()
    valid = df.notna().all(axis=1)
    log_mcap = np.log(df['marketcap'].where(df['marketcap'] &gt; 0))
    valid = valid &amp; log_mcap.notna()
    def zscore_by_date(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.where(std &gt; 0)
    neg_ret = (-df['ret_12_1']).where(valid)
    neg_logmcap = (-log_mcap).where(valid)
    vol = df['vol_252'].where(valid)
    z_ret = zscore_by_date(neg_ret)
    z_logmcap = zscore_by_date(neg_logmcap)
    z_vol = zscore_by_date(vol)
    loss = z_ret + 0.5 * z_logmcap + 0.5 * z_vol
    dates = panel.index.get_level_values('date')
    months = pd.Series(dates, index=panel.index).dt.month
    signal = pd.Series(np.nan, index=panel.index, dtype=float)
    signal = signal.where(~(months == 12), loss)
    signal = signal.where(~(months.isin([10, 11])), -loss)
    return signal</pre></div></details><details class="la-sig"><summary><code>annual_return_seasonality</code><span class="la-tag">other</span><span class="gist">Same-calendar-month return seasonality: average of a stock's ret_1m at annual lags (12,24,36,48,60 months)…</span></summary><div class="la-sig-body"><p>Same-calendar-month return seasonality: average of a stock's ret_1m at annual lags (12,24,36,48,60 months) minus the average of its ret_1m over non-seasonal lags (13..23 months). Captures the Heston-Sadka same-month seasonality effect, largely orthogonal to 12-1 momentum.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    other_lags = list(range(13, 24))
    same_shifts = [grp.shift(k) for k in same_lags]
    other_shifts = [grp.shift(k) for k in other_lags]
    same_df = pd.concat(same_shifts, axis=1)
    other_df = pd.concat(other_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    same_mean = same_mean.where(same_count &gt;= 3, np.nan)
    other_count = other_df.notna().sum(axis=1)
    other_mean = other_df.mean(axis=1)
    other_mean = other_mean.where(other_count &gt; 0, 0.0)
    signal = same_mean - other_mean
    signal = signal.where(same_mean.notna(), np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>annual_return_seasonality_spread</code><span class="la-tag">other</span><span class="gist">Seasonality spread: mean of same-calendar-month returns over prior years (12/24/36/48/60 month lags) minus…</span></summary><div class="la-sig-body"><p>Seasonality spread: mean of same-calendar-month returns over prior years (12/24/36/48/60 month lags) minus the stock's average trailing 60-month return, isolating the pure seasonal component of monthly returns as documented by Heston and Sadka.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    lags = [12, 24, 36, 48, 60]
    shifted = [g.shift(k) for k in lags]
    same_month_df = pd.concat(shifted, axis=1)
    same_month = same_month_df.mean(axis=1, skipna=True)
    same_month_count = same_month_df.notna().sum(axis=1)
    same_month = same_month.where(same_month_count &gt;= 3, np.nan)
    shifted1 = g.shift(1)
    all_month = (
        shifted1.groupby(level='ticker')
        .rolling(60, min_periods=24)
        .mean()
    )
    # rolling on grouped series returns a MultiIndex with an extra ticker level
    all_month = all_month.droplevel(0)
    all_month = all_month.reindex(panel.index)
    signal = same_month - all_month
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>attention_shock_amplified_reversal</code><span class="la-tag">other</span><span class="gist">Short-horizon reversal amplified by abnormal dollar-volume shocks: reversal (ret_1m scaled by vol_252) is…</span></summary><div class="la-sig-body"><p>Short-horizon reversal amplified by abnormal dollar-volume shocks: reversal (ret_1m scaled by vol_252) is combined multiplicatively with a rank-normalized measure of how far current log dollar volume deviates from its trailing 12-month average, capturing attention/liquidity-driven demand shocks that tend to reverse.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dvol_21 = panel["dvol_21"]
    ret_1m = panel["ret_1m"]
    vol_252 = panel["vol_252"]
    base = np.log(dvol_21.where(dvol_21 &gt; 0))
    grp = base.groupby(level="ticker")
    lags = [grp.shift(k) for k in range(1, 13)]
    lag_df = pd.concat(lags, axis=1)
    nonmissing_count = lag_df.notna().sum(axis=1)
    avg = lag_df.mean(axis=1)
    vshock = base - avg
    vshock = vshock.where(nonmissing_count &gt;= 9)
    # cross-sectional rank normalization into [-0.5, 0.5], per date
    def rank_norm(s: pd.Series) -&gt; pd.Series:
        valid = s.dropna()
        if len(valid) == 0:
            return pd.Series(np.nan, index=s.index)
        ranks = valid.rank(method="average")
        n = len(valid)
        if n &gt; 1:
            normed = (ranks - 1) / (n - 1) - 0.5
        else:
            normed = pd.Series(0.0, index=valid.index)
        out = pd.Series(np.nan, index=s.index)
        out.loc[valid.index] = normed
        return out
    vshock_c = vshock.groupby(level="date").apply(rank_norm)
    vshock_c.index = vshock.index
    # reversal component
    r = ret_1m.where(vol_252 &gt; 0) / vol_252.where(vol_252 &gt; 0)
    def winsorize(s: pd.Series) -&gt; pd.Series:
        valid = s.dropna()
        if len(valid) == 0:
            return s
        lo = valid.quantile(0.01)
        hi = valid.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    r_wins = r.groupby(level="date").apply(winsorize)
    r_wins.index = r.index
    signal = r_wins * (0.5 + vshock_c)
    # enforce missing-data policy: require dvol_21, ret_1m, vol_252 valid
    valid_mask = dvol_21.notna() &amp; ret_1m.notna() &amp; vol_252.notna() &amp; (vol_252 &gt; 0)
    signal = signal.where(valid_mask)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>attention_shock_return_reversal</code><span class="la-tag">other</span><span class="gist">Interaction of abnormal turnover (attention shock) with trailing 1m return</span></summary><div class="la-sig-body"><p>Interaction of abnormal turnover (attention shock) with trailing 1m return. High abnormal turnover accompanying a large recent gain forecasts reversal; high abnormal turnover accompanying a loss forecasts a bounce. Uses log turnover relative to its trailing 12m (min 6) mean, times ret_1m, with expected_sign=-1 so the harness implements the reversal orientation.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dvol = panel['dvol_21']
    mcap = panel['marketcap']
    ret_1m = panel['ret_1m']
    turn = (dvol / mcap).where((dvol &gt; 0) &amp; (mcap &gt; 0))
    lt = np.log(turn)
    lt_shifted = lt.groupby(level='ticker').shift(1)
    base = (
        lt_shifted.groupby(level='ticker')
        .rolling(12, min_periods=6)
        .mean()
        .reset_index(level=0, drop=True)
    )
    abn = lt - base
    score = abn * ret_1m
    score = score.where(abn.notna() &amp; ret_1m.notna())
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>attention_spike_return_reversal</code><span class="la-tag">other</span><span class="gist">Interaction of abnormal 21d dollar volume (attention spike) with recent 1m return scaled by volatility:…</span></summary><div class="la-sig-body"><p>Interaction of abnormal 21d dollar volume (attention spike) with recent 1m return scaled by volatility: reverses momentum on high-attention runs and rewards liquidity discount on high-attention sell-offs.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dvol = panel['dvol_21']
    ret_1m = panel['ret_1m']
    vol_252 = panel['vol_252']
    grp = dvol.groupby(level='ticker')
    lagged = [grp.shift(k) for k in range(1, 13)]
    stacked = pd.concat(lagged, axis=1)
    valid_count = stacked.notna().sum(axis=1)
    avg_dvol = stacked.mean(axis=1)
    valid_avg = (valid_count &gt;= 6) &amp; (avg_dvol &gt; 0)
    avg_dvol = avg_dvol.where(valid_avg)
    valid_dvol = dvol &gt; 0
    abn_vol = np.log(dvol / avg_dvol)
    abn_vol = abn_vol.where(valid_dvol &amp; valid_avg)
    abn_vol_clipped = abn_vol.clip(-2, 2)
    vol_floor = vol_252.clip(lower=0.05)
    valid_vol = vol_252 &gt; 0
    scaled_ret = ret_1m / vol_floor
    scaled_ret = scaled_ret.where(valid_vol)
    scaled_ret_clipped = scaled_ret.clip(-3, 3)
    signal = -1.0 * abn_vol_clipped * scaled_ret_clipped
    valid = (
        valid_avg
        &amp; valid_dvol
        &amp; valid_vol
        &amp; ret_1m.notna()
        &amp; dvol.notna()
    )
    signal = signal.where(valid)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>dollar_trend_fx_beta_interaction</code><span class="la-tag">other</span><span class="gist">Interacts a stock's trailing USD beta with the realized 12-month dollar trend: cross-sectionally z-scored,…</span></summary><div class="la-sig-body"><p>Interacts a stock's trailing USD beta with the realized 12-month dollar trend: cross-sectionally z-scored, winsorized beta_usd_252 (excluding names with weak |corr_usd_252|) is scaled by tanh of the fractional 12m dollar trend, producing a sign-switching macro-conditioned score.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta = panel['beta_usd_252']
    corr = panel['corr_usd_252']
    macro_usd = panel['macro_usd']
    macro_usd_chg12m = panel['macro_usd_chg12m']
    # base validity: all inputs present
    valid = beta.notna() &amp; corr.notna() &amp; macro_usd.notna() &amp; macro_usd_chg12m.notna()
    # winsorize beta at 1st/99th percentile per date
    def winsorize(s):
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    beta_masked = beta.where(valid)
    beta_wins = beta_masked.groupby(level='date').transform(winsorize)
    # cross-sectional zscore per date
    def zscore(s):
        mu = s.mean()
        sd = s.std()
        if sd is None or not np.isfinite(sd) or sd == 0:
            return s * np.nan
        return (s - mu) / sd
    zb = beta_wins.groupby(level='date').transform(zscore)
    # exclude weak corr names
    weak_corr = corr.abs() &lt;= 0.05
    zb = zb.where(~weak_corr)
    # dollar trend, same for all tickers on a date
    trend = macro_usd_chg12m / macro_usd.where(macro_usd &gt; 0)
    trend_signal = np.tanh(trend / 0.05)
    # if macro_usd or macro_usd_chg12m missing for date -&gt; NaN for all tickers that date
    macro_valid_by_date = (macro_usd.notna() &amp; macro_usd_chg12m.notna()).groupby(level='date').transform('all')
    signal = zb * trend_signal
    signal = signal.where(valid &amp; macro_valid_by_date)
    return signal</pre></div></details><details class="la-sig"><summary><code>expected_earnings_announcement_month</code><span class="la-tag">other</span><span class="gist">Scores stocks by how close their expected next earnings announcement is to the upcoming month, based on the…</span></summary><div class="la-sig-body"><p>Scores stocks by how close their expected next earnings announcement is to the upcoming month, based on the gap between the current point-in-time date and the last SEC filing date. Firms whose gap since last filing is close to ~72 days (implying an announcement roughly one quarter after the prior filing) score highest, capturing the earnings-announcement attention premium.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    asof_date = pd.to_datetime(panel['asof_date'])
    fund_datekey = pd.to_datetime(panel['fund_datekey'])
    gap_days = (asof_date - fund_datekey).dt.days
    score = np.exp(-((gap_days - 72.0) ** 2) / (2 * 15.0 ** 2))
    invalid = fund_datekey.isna() | gap_days.isna() | (gap_days &gt; 200)
    score = score.where(~invalid, np.nan)
    score = score.reindex(panel.index)
    score.name = self.name
    return score</pre></div></details><details class="la-sig"><summary><code>filing_staleness_information_neglect</code><span class="la-tag">other</span><span class="gist">Abnormal filing staleness: days elapsed since the most recent SEC filing (fund_datekey) relative to the…</span></summary><div class="la-sig-body"><p>Abnormal filing staleness: days elapsed since the most recent SEC filing (fund_datekey) relative to the industry-median lag on the same date. Unusually stale filings signal information friction / neglect and predict negative future drift; fresh filings predict positive drift.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    asof = pd.to_datetime(panel['asof_date'])
    datekey = pd.to_datetime(panel['fund_datekey'])
    lag_days = (asof - datekey).dt.days
    valid = datekey.notna() &amp; (lag_days &gt;= 0) &amp; (lag_days &lt;= self.MAX_LAG_DAYS)
    lag_valid = lag_days.where(valid)
    industry = panel['industry']
    df = pd.DataFrame({
        'lag': lag_valid,
        'industry': industry,
    }, index=panel.index)
    group_keys = [df.index.get_level_values('date'), df['industry']]
    group_median = df.groupby(group_keys)['lag'].transform('median')
    group_count = df.groupby(group_keys)['lag'].transform('count')
    abn = df['lag'] - group_median
    abn = abn.where(group_count &gt;= self.MIN_GROUP_SIZE)
    abn = abn.where(valid)
    abn.name = self.name
    return abn</pre></div></details><details class="la-sig"><summary><code>hidden_rate_duration_mismatch</code><span class="la-tag">other</span><span class="gist">Combines fundamental duration proxies (log P/B, log P/S) with trailing realized rate beta to identify…</span></summary><div class="la-sig-body"><p>Combines fundamental duration proxies (log P/B, log P/S) with trailing realized rate beta to identify long-duration names whose historical beta understates true rate sensitivity; conditions the mismatch score on the 12-month change in the 10y yield to time macro-state-dependent duration risk.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    pb = panel['pb']
    ps = panel['ps']
    beta_rates = panel['beta_rates_252']
    dgs10_chg12m = panel['macro_dgs10_chg12m']
    log_pb = np.log(pb.where(pb &gt; 0))
    log_ps = np.log(ps.where(ps &gt; 0))
    def cs_zscore(s):
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        return (s - mean) / std.where(std &gt; 0)
    zpb = cs_zscore(log_pb)
    zps = cs_zscore(log_ps)
    zrb = cs_zscore(beta_rates)
    hidden_duration = zpb + zps + zrb
    signal = -hidden_duration * np.tanh(dgs10_chg12m)
    mask = zpb.notna() &amp; zps.notna() &amp; zrb.notna() &amp; dgs10_chg12m.notna()
    signal = signal.where(mask)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>hidden_refinancing_rate_exposure</code><span class="la-tag">other</span><span class="gist">Interaction of net short-term debt burden (fundamental rollover risk) with a stock's realized beta to rates:…</span></summary><div class="la-sig-body"><p>Interaction of net short-term debt burden (fundamental rollover risk) with a stock's realized beta to rates: firms with high net short-term debt but positive rate beta are mispriced 'hidden' rollover-risk exposures, whose returns should suffer when short rates rise (and benefit when they fall). Signal = z(net_st) * z(beta_rates_252) * clipped 12m change in 2y yield.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debtc = panel['debtc']
    cashneq = panel['cashneq']
    assets = panel['assets']
    beta_rates = panel['beta_rates_252']
    macro_chg = panel['macro_dgs2_chg12m']
    valid = (
        debtc.notna()
        &amp; cashneq.notna()
        &amp; assets.notna()
        &amp; (assets &gt; 0)
        &amp; beta_rates.notna()
        &amp; macro_chg.notna()
    )
    net_st = pd.Series(np.nan, index=panel.index)
    net_st[valid] = (debtc[valid] - cashneq[valid]) / assets[valid]
    beta_valid = beta_rates.where(valid)
    z1 = net_st.groupby(level='date').transform(_winsorized_zscore)
    z2 = beta_valid.groupby(level='date').transform(_winsorized_zscore)
    mismatch = z1 * z2
    macro_clipped = macro_chg.clip(lower=-1.5, upper=1.5)
    score = mismatch * macro_clipped
    score = score.where(valid)
    return score</pre></div></details><details class="la-sig"><summary><code>long_term_reversal_sentiment_conditioned</code><span class="la-tag">other</span><span class="gist">Long-term (t-36 to t-12) reversal signal conditioned on past-only consumer sentiment state: reversal weighted…</span></summary><div class="la-sig-body"><p>Long-term (t-36 to t-12) reversal signal conditioned on past-only consumer sentiment state: reversal weighted more heavily when sentiment is above its own trailing average (elevated optimism), consistent with Baker-Wurgler/Stambaugh-Yu-Yuan sentiment-conditioned mispricing correction.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret_12m = panel['ret_12m']
    r1 = ret_12m.groupby(level='ticker').shift(12)
    r2 = ret_12m.groupby(level='ticker').shift(24)
    cum_36_12 = (1 + r1) * (1 + r2) - 1
    # date-level sentiment series
    sent_by_date = panel['macro_umcsent'].groupby(level='date').mean()
    sent_by_date = sent_by_date.sort_index()
    rolling_mean = sent_by_date.rolling(36, min_periods=24).mean().shift(1)
    sent_state = sent_by_date / rolling_mean.where(rolling_mean &gt; 0)
    w_by_date = pd.Series(
        np.where(sent_state &gt; 1.0, 1.0, 0.3),
        index=sent_state.index,
    )
    w_by_date = w_by_date.where(sent_state.notna())
    # broadcast weight to (date, ticker) index
    dates = panel.index.get_level_values('date')
    w = w_by_date.reindex(dates)
    w.index = panel.index
    score = cum_36_12 * w
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>macro_beta_dispersion_crowding_rotation</code><span class="la-tag">other</span><span class="gist">Entropy of squared macro correlations (oil, rates, usd, credit, vix) capturing the shape of a stock's macro…</span></summary><div class="la-sig-body"><p>Entropy of squared macro correlations (oil, rates, usd, credit, vix) capturing the shape of a stock's macro exposure profile. Diffuse macro exposure (high entropy) earns a premium for undiversifiable macro risk; concentrated exposure in a single fashionable factor is subject to crowded-flow reversals.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = ['corr_oil_252', 'corr_rates_252', 'corr_usd_252',
            'corr_credit_252', 'corr_vix_252']
    df = panel[cols].copy()
    n_valid = df.notna().sum(axis=1)
    sq = df.pow(2)
    sq_filled = sq.fillna(0.0)
    sum_sq = sq_filled.sum(axis=1)
    valid_mask = (n_valid &gt;= 4) &amp; (sum_sq &gt; 0)
    w = sq_filled.div(sum_sq.where(sum_sq &gt; 0, np.nan), axis=0)
    entropy = -(w * np.log(w + 1e-12)).sum(axis=1)
    score = entropy.where(valid_mask, np.nan)
    score.name = self.name
    return score</pre></div></details><details class="la-sig"><summary><code>macro_trend_attributed_return_reversal</code><span class="la-tag">other</span><span class="gist">Contrarian signal on the macro-attributed component of past returns: sums cross-sectionally z-scored macro…</span></summary><div class="la-sig-body"><p>Contrarian signal on the macro-attributed component of past returns: sums cross-sectionally z-scored macro betas (rates, oil, usd, credit, vix) signed by the direction of the corresponding 12-month macro trend. Stocks with high macro-attributed 'performance' are expected to mean-revert as macro trends are less persistent than the market extrapolates.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta_cols = {
        'rates': 'beta_rates_252',
        'oil': 'beta_oil_252',
        'usd': 'beta_usd_252',
        'credit': 'beta_credit_252',
        'vix': 'beta_vix_252',
    }
    chg_cols = {
        'rates': 'macro_dgs10_chg12m',
        'oil': 'macro_oil_chg12m',
        'usd': 'macro_usd_chg12m',
        'credit': 'macro_baa_spread_chg12m',
        'vix': 'macro_vix_chg12m',
    }
    contributions = []
    valid_masks = []
    for f in beta_cols:
        beta_col = beta_cols[f]
        chg_col = chg_cols[f]
        if beta_col not in panel.columns or chg_col not in panel.columns:
            continue
        beta = panel[beta_col]
        chg = panel[chg_col]
        # cross-sectional zscore per date, clipped
        grp = beta.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        zb = (beta - mean) / std
        zb = zb.where(std &gt; 0, np.nan)
        zb = zb.clip(-3, 3)
        d = np.sign(chg)
        d = d.where(chg.notna() &amp; (chg != 0), np.nan)
        contrib = d * zb
        valid = contrib.notna()
        contributions.append(contrib.fillna(0.0))
        valid_masks.append(valid)
    if not contributions:
        return pd.Series(np.nan, index=panel.index)
    total = contributions[0]
    for c in contributions[1:]:
        total = total + c
    count = valid_masks[0].astype(int)
    for v in valid_masks[1:]:
        count = count + v.astype(int)
    result = total.where(count &gt;= 3, np.nan)
    result.name = None
    return result</pre></div></details><details class="la-sig"><summary><code>macro_trend_beta_alignment</code><span class="la-tag">other</span><span class="gist">Aligns stocks' trailing cross-asset macro betas (oil, USD, rates, credit) with the prevailing direction of…</span></summary><div class="la-sig-body"><p>Aligns stocks' trailing cross-asset macro betas (oil, USD, rates, credit) with the prevailing direction of the corresponding macro trend, exploiting gradual investor diffusion of macro information into stock prices with differentiated factor exposure.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dates = panel.index.get_level_values('date')
    # --- date-level macro trends (unique per date) ---
    macro_cols = [
        'macro_oil', 'macro_oil_chg12m',
        'macro_usd', 'macro_usd_chg12m',
        'macro_dgs10_chg12m', 'macro_baa_spread_chg12m',
    ]
    macro_df = panel[macro_cols].copy()
    macro_df = macro_df.reset_index(level='ticker', drop=True)
    macro_df = macro_df[~macro_df.index.duplicated(keep='first')].sort_index()
    # unitless trends with guarded denominators
    oil_denom = macro_df['macro_oil'].where(macro_df['macro_oil'] &gt; 0)
    usd_denom = macro_df['macro_usd'].where(macro_df['macro_usd'] &gt; 0)
    raw_tr_oil = macro_df['macro_oil_chg12m'] / oil_denom
    raw_tr_usd = macro_df['macro_usd_chg12m'] / usd_denom
    raw_tr_rate = macro_df['macro_dgs10_chg12m']
    raw_tr_cred = macro_df['macro_baa_spread_chg12m']
    tr_oil = self._z_exp_past_only(raw_tr_oil)
    tr_usd = self._z_exp_past_only(raw_tr_usd)
    tr_rate = self._z_exp_past_only(raw_tr_rate)
    tr_cred = self._z_exp_past_only(raw_tr_cred)
    # broadcast date-level trends back onto panel
    tr_oil_b = tr_oil.reindex(dates).to_numpy()
    tr_usd_b = tr_usd.reindex(dates).to_numpy()
    tr_rate_b = tr_rate.reindex(dates).to_numpy()
    tr_cred_b = tr_cred.reindex(dates).to_numpy()
    tr_oil_s = pd.Series(tr_oil_b, index=panel.index)
    tr_usd_s = pd.Series(tr_usd_b, index=panel.index)
    tr_rate_s = pd.Series(tr_rate_b, index=panel.index)
    tr_cred_s = pd.Series(tr_cred_b, index=panel.index)
    # --- cross-sectional z-scores of betas within each date ---
    def cs_z(col: str) -&gt; pd.Series:
        s = panel[col]
        grp = s.groupby(level='date')
        mean = grp.transform('mean')
        std = grp.transform('std')
        z = (s - mean) / std.replace(0, np.nan)
        return z
    bo = cs_z('beta_oil_252')
    bu = cs_z('beta_usd_252')
    br = cs_z('beta_rates_252')
    bc = cs_z('beta_credit_252')
    n_valid = (
        bo.notna().astype(int)
        + bu.notna().astype(int)
        + br.notna().astype(int)
        + bc.notna().astype(int)
    )
    score = (
        bo.fillna(0) * tr_oil_s
        + bu.fillna(0) * tr_usd_s
        + br.fillna(0) * tr_rate_s
        + bc.fillna(0) * tr_cred_s
    )
    score = score.where(n_valid &gt;= 3)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>macro_trend_beta_spillover</code><span class="la-tag">other</span><span class="gist">Interacts trailing macro factor trends (oil, USD, rates, credit) with each stock's estimated macro betas to…</span></summary><div class="la-sig-body"><p>Interacts trailing macro factor trends (oil, USD, rates, credit) with each stock's estimated macro betas to produce a macro-implied expected return that diffuses gradually into equity prices.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dates = panel.index.get_level_values('date')
    # date-level macro series (one value per date)
    macro_oil = panel['macro_oil'].groupby(dates).first()
    macro_oil_chg12m = panel['macro_oil_chg12m'].groupby(dates).first()
    macro_usd = panel['macro_usd'].groupby(dates).first()
    macro_usd_chg12m = panel['macro_usd_chg12m'].groupby(dates).first()
    macro_dgs10_chg12m = panel['macro_dgs10_chg12m'].groupby(dates).first()
    macro_baa_spread_chg12m = panel['macro_baa_spread_chg12m'].groupby(dates).first()
    macro_oil = macro_oil.sort_index()
    macro_oil_chg12m = macro_oil_chg12m.sort_index()
    macro_usd = macro_usd.sort_index()
    macro_usd_chg12m = macro_usd_chg12m.sort_index()
    macro_dgs10_chg12m = macro_dgs10_chg12m.sort_index()
    macro_baa_spread_chg12m = macro_baa_spread_chg12m.sort_index()
    oil_denom = macro_oil - macro_oil_chg12m
    oil_tr = macro_oil_chg12m / oil_denom.where(oil_denom &gt; 0)
    usd_denom = macro_usd - macro_usd_chg12m
    usd_tr = macro_usd_chg12m / usd_denom.where(usd_denom &gt; 0)
    rate_tr = macro_dgs10_chg12m
    cred_tr = macro_baa_spread_chg12m
    def past_only_z(x: pd.Series) -&gt; pd.Series:
        x_shift = x.shift(1)
        std = x_shift.expanding(min_periods=36).std()
        return x / std.where(std &gt; 0)
    z_oil = past_only_z(oil_tr)
    z_usd = past_only_z(usd_tr)
    z_rate = past_only_z(rate_tr)
    z_cred = past_only_z(cred_tr)
    # map back to panel by date
    z_oil_p = dates.map(z_oil)
    z_usd_p = dates.map(z_usd)
    z_rate_p = dates.map(z_rate)
    z_cred_p = dates.map(z_cred)
    z_oil_p = pd.Series(z_oil_p, index=panel.index)
    z_usd_p = pd.Series(z_usd_p, index=panel.index)
    z_rate_p = pd.Series(z_rate_p, index=panel.index)
    z_cred_p = pd.Series(z_cred_p, index=panel.index)
    # winsorize betas cross-sectionally per date
    beta_oil = panel['beta_oil_252'].groupby(level='date').transform(self._winsorize)
    beta_usd = panel['beta_usd_252'].groupby(level='date').transform(self._winsorize)
    beta_rates = panel['beta_rates_252'].groupby(level='date').transform(self._winsorize)
    beta_credit = panel['beta_credit_252'].groupby(level='date').transform(self._winsorize)
    term_oil = beta_oil * z_oil_p
    term_usd = beta_usd * z_usd_p
    term_rate = beta_rates * z_rate_p
    term_cred = beta_credit * z_cred_p
    terms = pd.concat(
        [term_oil.rename('t_oil'), term_usd.rename('t_usd'),
         term_rate.rename('t_rate'), term_cred.rename('t_cred')],
        axis=1
    )
    valid_count = terms.notna().sum(axis=1)
    total = terms.sum(axis=1, skipna=True)
    result = total.where(valid_count &gt;= 3)
    return result</pre></div></details><details class="la-sig"><summary><code>macro_trend_exposure_alignment</code><span class="la-tag">other</span><span class="gist">Aligns firm-level macro betas (oil, USD, rates, credit) with the persistent trend in those macro factors…</span></summary><div class="la-sig-body"><p>Aligns firm-level macro betas (oil, USD, rates, credit) with the persistent trend in those macro factors (expanding z-scored 12m changes), exploiting slow diffusion of cross-asset macro trends into equity cash-flow expectations.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    idx = panel.index
    macro_cols = {
        'oil': 'macro_oil_chg12m',
        'usd': 'macro_usd_chg12m',
        'rates': 'macro_dgs10_chg12m',
        'credit': 'macro_baa_spread_chg12m',
    }
    beta_cols = {
        'oil': 'beta_oil_252',
        'usd': 'beta_usd_252',
        'rates': 'beta_rates_252',
        'credit': 'beta_credit_252',
    }
    # Build date-level macro series (one value per date) and compute expanding z-scores
    z_terms = {}
    dates = idx.get_level_values('date')
    for key, col in macro_cols.items():
        if col in panel.columns:
            per_date = panel[col].groupby(level='date').first()
            per_date = per_date.sort_index()
            z_per_date = self._expanding_z(per_date)
            z_terms[key] = z_per_date.reindex(dates).to_numpy()
        else:
            z_terms[key] = np.zeros(len(idx))
    # Winsorize betas cross-sectionally per date
    beta_win = {}
    beta_missing_mask = pd.Series(False, index=idx)
    for key, col in beta_cols.items():
        if col in panel.columns:
            b = panel[col]
            beta_missing_mask = beta_missing_mask | b.isna()
            b_w = self._winsorize_by_date(b)
            beta_win[key] = b_w
        else:
            beta_missing_mask = pd.Series(True, index=idx)
            beta_win[key] = pd.Series(np.nan, index=idx)
    score = pd.Series(0.0, index=idx)
    for key in macro_cols.keys():
        b = beta_win[key].fillna(0.0)
        z = pd.Series(z_terms[key], index=idx)
        score = score + b * z
    score = score.where(~beta_missing_mask, np.nan)
    return score</pre></div></details><details class="la-sig"><summary><code>oil_beta_trend_interaction</code><span class="la-tag">other</span><span class="gist">Interaction of trailing stock beta to oil price moves with a 12m oil price trend regime scaler</span></summary><div class="la-sig-body"><p>Interaction of trailing stock beta to oil price moves with a 12m oil price trend regime scaler. Long high-oil-beta stocks when oil has trended up over the past year, short them when oil has trended down, capturing gradual diffusion of commodity trends into cash-flow-exposed equity valuations.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta_oil = panel['beta_oil_252']
    corr_oil = panel['corr_oil_252']
    oil_chg12m = panel['macro_oil_chg12m']
    oil_trend = np.tanh(oil_chg12m / 20.0)
    exposure = beta_oil.where(corr_oil.abs() &gt; 0.05)
    def _zscore(x):
        std = x.std()
        if std is None or std == 0 or pd.isna(std):
            return pd.Series(np.nan, index=x.index)
        return (x - x.median()) / std
    exposure_z = exposure.groupby(level='date').transform(_zscore)
    signal = exposure_z * oil_trend
    signal = signal.reindex(panel.index)
    return signal</pre></div></details><details class="la-sig"><summary><code>pre_earnings_filing_clock_premium</code><span class="la-tag">other</span><span class="gist">Within-firm earnings-announcement-clock seasonality: proxies proximity to the next expected SEC filing using…</span></summary><div class="la-sig-body"><p>Within-firm earnings-announcement-clock seasonality: proxies proximity to the next expected SEC filing using days since the last point-in-time filing date. Peaks when days_since_filing is near 85 (i.e., the next filing is likely due within the coming month), capturing the documented earnings announcement premium driven by announcement-risk compensation and attention scarcity.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    datekey = pd.to_datetime(panel["fund_datekey"], errors="coerce")
    asof = pd.to_datetime(panel["asof_date"], errors="coerce")
    valid_dates = datekey.notna() &amp; asof.notna()
    days_since_filing = (asof - datekey).dt.days
    cycle_position = -(days_since_filing - 85).abs().astype(float)
    in_range = (days_since_filing &gt;= 20) &amp; (days_since_filing &lt;= 200)
    signal = cycle_position.where(valid_dates &amp; in_range, np.nan)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>predicted_dividend_month_premium</code><span class="la-tag">other</span><span class="gist">Predicts dividend-clientele driven price pressure by inferring implied monthly dividend yield from the wedge…</span></summary><div class="la-sig-body"><p>Predicts dividend-clientele driven price pressure by inferring implied monthly dividend yield from the wedge between adjusted and raw close returns, then projecting it forward 12 months using sticky dividend payment calendars, with support confirmation from recent dividend history.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    closeadj = panel['closeadj']
    close = panel['close']
    grp_adj = closeadj.groupby(level='ticker')
    grp_close = close.groupby(level='ticker')
    closeadj_prev = grp_adj.shift(1)
    close_prev = grp_close.shift(1)
    valid = (
        closeadj.notna() &amp; closeadj_prev.notna() &amp; (closeadj_prev != 0)
        &amp; close.notna() &amp; close_prev.notna() &amp; (close_prev != 0)
    )
    radj = (closeadj / closeadj_prev) - 1.0
    rraw = (close / close_prev) - 1.0
    diff = radj - rraw
    impl_yld = diff.clip(lower=0.0, upper=0.05)
    impl_yld = impl_yld.where(valid, np.nan)
    # split artifact detection
    split_artifact = diff.abs() &gt; 0.5
    impl_yld = impl_yld.where(~split_artifact, np.nan)
    # need sufficient history for shift(12): closeadj/close shifted 12 must exist
    closeadj_12 = grp_adj.shift(12)
    close_12 = grp_close.shift(12)
    closeadj_13 = grp_adj.shift(13)
    close_13 = grp_close.shift(13)
    hist_ok = (
        closeadj_12.notna() &amp; closeadj_13.notna()
        &amp; close_12.notna() &amp; close_13.notna()
    )
    impl_yld_grp = impl_yld.groupby(level='ticker')
    pred = impl_yld_grp.shift(12)
    pred = pred.where(hist_ok, np.nan)
    shift3 = impl_yld_grp.shift(3)
    shift12 = impl_yld_grp.shift(12)
    support_cond = (shift3 &gt; 0) | (shift12 &gt; 0)
    support = support_cond.astype(float)
    support = support.where(shift3.notna() | shift12.notna(), np.nan)
    score = pred * support
    # no dividend history in past 12m -&gt; score 0
    # define "dividend history in past 12m" as any positive impl_yld in trailing 12 shifts (1..12)
    pos_yld = (impl_yld &gt; 0).astype(float)
    pos_yld_grp = pos_yld.groupby(level='ticker')
    rolling_hist = pos_yld_grp.transform(
        lambda s: s.shift(1).rolling(window=12, min_periods=1).max()
    )
    no_history = rolling_hist.fillna(0) == 0
    score = score.where(~no_history, 0.0)
    score = score.reindex(panel.index)
    return score</pre></div></details><details class="la-sig"><summary><code>rate_beta_duration_mismatch</code><span class="la-tag">other</span><span class="gist">Mismatch between valuation-implied duration and realized rate beta, aligned with the sign of the trailing 12m…</span></summary><div class="la-sig-body"><p>Mismatch between valuation-implied duration and realized rate beta, aligned with the sign of the trailing 12m change in 10y yields; signal is masked where the stock's rate correlation is weak.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    pb = panel['pb']
    ps = panel['ps']
    divyield = panel['divyield'].fillna(0)
    beta_rates = panel['beta_rates_252']
    corr_rates = panel['corr_rates_252']
    macro_chg = panel['macro_dgs10_chg12m']
    def rk(x):
        return x.groupby(level='date').rank(pct=True)
    D = (rk(pb) + rk(ps) + (1 - rk(divyield))) / 3
    beta_mean = beta_rates.groupby(level='date').transform('mean')
    beta_std = beta_rates.groupby(level='date').transform('std')
    zb = (beta_rates - beta_mean) / beta_std.where(beta_std &gt; 0)
    D_std = D.groupby(level='date').transform('std')
    zD = (D - 0.5) / D_std.where(D_std &gt; 0)
    mismatch = zb + zD
    signal = mismatch * np.sign(macro_chg)
    invalid = (
        pb.isna()
        | ps.isna()
        | beta_rates.isna()
        | corr_rates.isna()
        | macro_chg.isna()
        | (corr_rates.abs() &lt; 0.02)
    )
    signal = signal.where(~invalid, np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>relative_month_of_year_seasonality</code><span class="la-tag">other</span><span class="gist">Stock-level calendar month-of-year seasonality: average return in the same calendar month over the past 1-5…</span></summary><div class="la-sig-body"><p>Stock-level calendar month-of-year seasonality: average return in the same calendar month over the past 1-5 years, benchmarked against the average return in adjacent calendar months to strip out level/drift effects. Higher relative same-month seasonality predicts higher future return.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    adj_lags = [11, 13, 23, 25, 35, 37, 47, 49]
    same_shifts = [grp.shift(k) for k in same_lags]
    adj_shifts = [grp.shift(k) for k in adj_lags]
    same_df = pd.concat(same_shifts, axis=1)
    adj_df = pd.concat(adj_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    adj_count = adj_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    adj_mean = adj_df.mean(axis=1)
    signal = same_mean - adj_mean
    valid = (same_count &gt;= 3) &amp; (adj_count &gt;= 5)
    signal = signal.where(valid, np.nan)
    signal = signal.reindex(panel.index)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>residual_same_month_seasonality</code><span class="la-tag">other</span><span class="gist">Heston-Sadka same-calendar-month seasonality: average of a stock's ret_1m in the same calendar month over the…</span></summary><div class="la-sig-body"><p>Heston-Sadka same-calendar-month seasonality: average of a stock's ret_1m in the same calendar month over the trailing 5 years, minus its average ret_12m/12 over the same lags to strip out level/momentum, isolating a firm-specific seasonal return component.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel.groupby(level='ticker')
    ret1m_lags = [g['ret_1m'].shift(k) for k in (12, 24, 36, 48, 60)]
    ret12m_lags = [g['ret_12m'].shift(k) for k in (12, 24, 36, 48, 60)]
    ret1m_df = pd.concat(ret1m_lags, axis=1)
    ret12m_df = pd.concat(ret12m_lags, axis=1)
    valid_count = ret1m_df.notna().sum(axis=1)
    seas = ret1m_df.mean(axis=1, skipna=True)
    seas = seas.where(valid_count &gt;= 3, np.nan)
    base = ret12m_df.mean(axis=1, skipna=True) / 12.0
    base_valid = ret12m_df.notna().any(axis=1)
    base = base.where(base_valid, 0.0)
    signal = seas - base
    signal = signal.where(seas.notna(), np.nan)
    signal.name = self.name
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>same_calendar_month_seasonality</code><span class="la-tag">other</span><span class="gist">Seasonality factor based on Heston and Sadka (2008): averages a stock's same-calendar-month historical…</span></summary><div class="la-sig-body"><p>Seasonality factor based on Heston and Sadka (2008): averages a stock's same-calendar-month historical 1-month returns lagged 12, 24, 36, 48, and 60 months, winsorized cross-sectionally at the 1st/99th percentile per date, requiring at least 3 non-missing lags.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    lags = [12, 24, 36, 48, 60]
    ret = panel["ret_1m"]
    winsorized_lags = []
    for k in lags:
        r_k = ret.groupby(level="ticker").shift(k)
        def _winsorize(s: pd.Series) -&gt; pd.Series:
            valid = s.dropna()
            if valid.empty:
                return s
            lo = valid.quantile(0.01)
            hi = valid.quantile(0.99)
            return s.clip(lower=lo, upper=hi)
        r_k_w = r_k.groupby(level="date").transform(_winsorize)
        winsorized_lags.append(r_k_w)
    lag_df = pd.concat(winsorized_lags, axis=1)
    lag_df.columns = [f"lag_{k}" for k in lags]
    valid_count = lag_df.notna().sum(axis=1)
    mean_ret = lag_df.mean(axis=1, skipna=True)
    signal = mean_ret.where(valid_count &gt;= 3, np.nan)
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>same_month_return_seasonality</code><span class="la-tag">other</span><span class="gist">Same-calendar-month return seasonality: average of ret_1m at lags of 12,24,36,48,60 months (same calendar…</span></summary><div class="la-sig-body"><p>Same-calendar-month return seasonality: average of ret_1m at lags of 12,24,36,48,60 months (same calendar month) minus average of ret_1m at all other lags 1..60 (excluding multiples of 12). Requires at least 3 non-missing same-month observations.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    other_lags = [k for k in range(1, 61) if k % 12 != 0]
    same_shifts = [grp.shift(k) for k in same_lags]
    other_shifts = [grp.shift(k) for k in other_lags]
    same_df = pd.concat(same_shifts, axis=1)
    other_df = pd.concat(other_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1)
    other_mean = other_df.mean(axis=1)
    signal = same_mean - other_mean
    valid = (same_count &gt;= 3) &amp; other_mean.notna()
    signal = signal.where(valid, np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>same_month_return_seasonality_spread</code><span class="la-tag">other</span><span class="gist">Firm-level calendar-month return seasonality: averages a stock's own ret_1m in the same calendar month over…</span></summary><div class="la-sig-body"><p>Firm-level calendar-month return seasonality: averages a stock's own ret_1m in the same calendar month over the last 1-5 years (lags 12,24,36,48,60) and differences it against the average return in non-anniversary months (lags 3,6,9,15,18,21,27,33), isolating a seasonal effect distinct from generic long-horizon momentum/reversal.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    seasonal_lags = [12, 24, 36, 48, 60]
    nonseasonal_lags = [3, 6, 9, 15, 18, 21, 27, 33]
    seasonal_shifts = [g.shift(k) for k in seasonal_lags]
    nonseasonal_shifts = [g.shift(k) for k in nonseasonal_lags]
    seasonal_df = pd.concat(seasonal_shifts, axis=1)
    nonseasonal_df = pd.concat(nonseasonal_shifts, axis=1)
    seasonal_count = seasonal_df.notna().sum(axis=1)
    nonseasonal_count = nonseasonal_df.notna().sum(axis=1)
    seasonal_mean = seasonal_df.mean(axis=1, skipna=True)
    nonseasonal_mean = nonseasonal_df.mean(axis=1, skipna=True)
    seasonal_mean = seasonal_mean.where(seasonal_count &gt;= 3)
    nonseasonal_mean = nonseasonal_mean.where(nonseasonal_count &gt;= 4)
    signal = seasonal_mean - nonseasonal_mean
    signal = signal.reindex(panel.index)
    return signal</pre></div></details><details class="la-sig"><summary><code>same_month_seasonal_persistence</code><span class="la-tag">other</span><span class="gist">Same-calendar-month seasonal persistence: average of a stock's ret_1m lagged by 12,24,36,48,60 months minus…</span></summary><div class="la-sig-body"><p>Same-calendar-month seasonal persistence: average of a stock's ret_1m lagged by 12,24,36,48,60 months minus average of ret_1m lagged by 13,25,37 months (adjacent-month control), requiring at least 3 non-NaN seasonal lags.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    g = panel['ret_1m'].groupby(level='ticker')
    seas_lags = [12, 24, 36, 48, 60]
    nonseas_lags = [13, 25, 37]
    seas_shifts = [g.shift(k) for k in seas_lags]
    seas_df = pd.concat(seas_shifts, axis=1)
    seas_count = seas_df.notna().sum(axis=1)
    seas_mean = seas_df.mean(axis=1, skipna=True)
    seas_mean = seas_mean.where(seas_count &gt;= 3)
    nonseas_shifts = [g.shift(k) for k in nonseas_lags]
    nonseas_df = pd.concat(nonseas_shifts, axis=1)
    nonseas_mean = nonseas_df.mean(axis=1, skipna=True)
    nonseas_mean = nonseas_mean.fillna(0.0)
    score = seas_mean - nonseas_mean
    score = score.where(seas_count &gt;= 3)
    score = score.reindex(panel.index)
    score.name = self.name
    return score</pre></div></details><details class="la-sig"><summary><code>same_month_seasonality_residual</code><span class="la-tag">other</span><span class="gist">Firm-specific calendar-month seasonality: average return in the same calendar month over the past 1-5 years,…</span></summary><div class="la-sig-body"><p>Firm-specific calendar-month seasonality: average return in the same calendar month over the past 1-5 years, minus the average return of adjacent months in those same years, isolating a pure seasonal component net of slow-moving momentum/reversal.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret = panel['ret_1m']
    grp = ret.groupby(level='ticker')
    same_lags = [12, 24, 36, 48, 60]
    adj_lags = [11, 13, 23, 25, 35, 37, 47, 49]
    same_shifts = [grp.shift(k) for k in same_lags]
    adj_shifts = [grp.shift(k) for k in adj_lags]
    same_df = pd.concat(same_shifts, axis=1)
    adj_df = pd.concat(adj_shifts, axis=1)
    same_count = same_df.notna().sum(axis=1)
    same_mean = same_df.mean(axis=1, skipna=True)
    adj_mean = adj_df.mean(axis=1, skipna=True)
    adj_count = adj_df.notna().sum(axis=1)
    signal = same_mean - adj_mean
    valid = (same_count &gt;= 3) &amp; (adj_count &gt; 0)
    signal = signal.where(valid, np.nan)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>same_quarter_earnings_seasonality</code><span class="la-tag">other</span><span class="gist">Seasonal earnings anticipation signal: compares net income from the same fiscal quarter one year ago (about…</span></summary><div class="la-sig-body"><p>Seasonal earnings anticipation signal: compares net income from the same fiscal quarter one year ago (about to be reported again) to the firm's trailing four-quarter average net income, scaled by assets from that period. Captures underreaction to persistent seasonal earnings patterns (Chang, Hartzmark, Solomon, Sosyura 2017) distinct from generic momentum or SUE.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ni = panel['netinc']
    a = panel['assets']
    grp = ni.groupby(level='ticker')
    seasonal_lag = grp.shift(12)
    lags = [grp.shift(k) for k in [3, 6, 9, 12]]
    lag_df = pd.concat(lags, axis=1)
    lag_df.columns = ['l3', 'l6', 'l9', 'l12']
    count_valid = lag_df.notna().sum(axis=1)
    base = lag_df.mean(axis=1, skipna=True)
    base = base.where(count_valid &gt;= 3, np.nan)
    assets_lag12 = a.groupby(level='ticker').shift(12)
    denom = assets_lag12.where(assets_lag12 &gt; 0, np.nan)
    raw = (seasonal_lag - base) / denom
    def winsorize(x):
        valid = x.dropna()
        if valid.empty:
            return x
        lo, hi = valid.quantile(0.01), valid.quantile(0.99)
        return x.clip(lower=lo, upper=hi)
    raw = raw.groupby(level='date').transform(winsorize)
    return raw.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>tax_loss_overhang_seasonal</code><span class="la-tag">other</span><span class="gist">Calendar-conditioned tax-loss-selling reversal: identifies loss-overhang stocks (large distance from 52-week…</span></summary><div class="la-sig-body"><p>Calendar-conditioned tax-loss-selling reversal: identifies loss-overhang stocks (large distance from 52-week high with negative trailing 12m return), tilted toward smaller caps, and shorts them during Oct-Nov selling pressure while going long at end of December to capture the January rebound.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    high_ratio = panel['high_ratio']
    ret_12m = panel['ret_12m']
    marketcap = panel['marketcap']
    valid = high_ratio.notna() &amp; ret_12m.notna() &amp; marketcap.notna() &amp; (marketcap &gt; 0)
    overhang = (1 - high_ratio) * (ret_12m &lt; 0).astype(float)
    neg_log_mcap = -np.log(marketcap.where(marketcap &gt; 0))
    rank = neg_log_mcap.groupby(level='date').rank(pct=True)
    size_tilt = 1 + rank
    ov = overhang * size_tilt
    ov = ov.where(valid, np.nan)
    dates = panel.index.get_level_values('date')
    month = pd.to_datetime(dates).month
    w = pd.Series(0.0, index=panel.index)
    w[month == 12] = 1.0
    w[(month == 10) | (month == 11)] = -1.0
    score = w * ov
    # months where w==0 -&gt; score should be 0, not NaN, even if ov is NaN
    score = score.where(~((w == 0)), 0.0)
    return score</pre></div></details><details class="la-sig"><summary><code>tax_loss_selling_seasonal_reversal</code><span class="la-tag">other</span><span class="gist">Tax-loss selling seasonal reversal: stocks with large embedded losses, far from 52-week highs, high…</span></summary><div class="la-sig-body"><p>Tax-loss selling seasonal reversal: stocks with large embedded losses, far from 52-week highs, high volatility, and small market cap face price pressure from taxable-investor loss harvesting in Nov-Dec, which reverses in January. Score is negative loss-overhang in Nov/Dec (anticipating further pressure downward) and positive loss-overhang in January (anticipating rebound).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    high_ratio = panel['high_ratio']
    ret_12m = panel['ret_12m']
    marketcap = panel['marketcap']
    vol_252 = panel['vol_252']
    valid = (
        (marketcap &gt; 0)
        &amp; high_ratio.gt(0) &amp; high_ratio.le(1)
        &amp; (vol_252 &gt; 0)
        &amp; ret_12m.notna()
    )
    log_mc = np.log(marketcap.where(marketcap &gt; 0))
    loss_overhang = (
        (1 - high_ratio)
        * (ret_12m &lt; 0).astype(float)
        * vol_252
        / log_mc
    )
    loss_overhang = loss_overhang.where(valid)
    month = panel.index.get_level_values('date').month
    month = pd.Series(month, index=panel.index)
    score = pd.Series(np.nan, index=panel.index, dtype=float)
    mask_pressure = month.isin([11, 12])
    mask_rebound = month.eq(1)
    score = score.where(~mask_pressure, -loss_overhang)
    score = score.where(~mask_rebound, loss_overhang)
    return score</pre></div></details><details class="la-sig"><summary><code>tax_loss_selling_turn_of_year_pressure</code><span class="la-tag">other</span><span class="gist">Capital-loss overhang (max(0,-ret_12m)) scaled by proximity-to-low (1 - high_ratio) and volatility, applied…</span></summary><div class="la-sig-body"><p>Capital-loss overhang (max(0,-ret_12m)) scaled by proximity-to-low (1 - high_ratio) and volatility, applied only in Nov/Dec: shorted in November while tax-loss selling pressure is ongoing, bought at end-December to capture the January rebound.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret_12m = panel['ret_12m']
    high_ratio = panel['high_ratio']
    vol_252 = panel['vol_252']
    valid = ret_12m.notna() &amp; high_ratio.notna() &amp; vol_252.notna()
    pressure = (-ret_12m).clip(lower=0.0) * (1.0 - high_ratio) * vol_252
    month = panel.index.get_level_values('date').month
    month = pd.Series(month, index=panel.index)
    signal = pd.Series(np.nan, index=panel.index, dtype=float)
    dec_mask = valid &amp; (month == 12)
    nov_mask = valid &amp; (month == 11)
    signal[dec_mask] = pressure[dec_mask]
    signal[nov_mask] = -pressure[nov_mask]
    return signal</pre></div></details><details class="la-sig"><summary><code>tax_loss_selling_turn_of_year_reversal</code><span class="la-tag">other</span><span class="gist">Tax-loss-selling turn-of-year reversal: stocks far below their 52-week high with poor 12-1 momentum…</span></summary><div class="la-sig-body"><p>Tax-loss-selling turn-of-year reversal: stocks far below their 52-week high with poor 12-1 momentum experience selling pressure from taxable investors harvesting losses in Nov-Dec, which reverses in January. Signal is positive in January, negative in Nov/Dec, and zero otherwise, scaled by an embedded capital-loss proxy normalized by log market cap.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    high_ratio = panel['high_ratio']
    ret_12_1 = panel['ret_12_1']
    marketcap = panel['marketcap']
    valid = (
        high_ratio.notna() &amp; ret_12_1.notna() &amp; marketcap.notna()
        &amp; (marketcap &gt; 1)
        &amp; (high_ratio &gt; 0) &amp; (high_ratio &lt;= 1)
    )
    log_mc = np.log(marketcap.where(valid))
    neg_ret = (-ret_12_1).clip(lower=0.0)
    embedded_loss = (1 - high_ratio) * (1 + neg_ret) / log_mc
    embedded_loss = embedded_loss.where(valid)
    dates = panel.index.get_level_values('date')
    month = pd.Series(dates, index=panel.index).dt.month
    mult = pd.Series(0.0, index=panel.index)
    mult[month == 1] = 1.0
    mult[month.isin([11, 12])] = -1.0
    raw = mult * embedded_loss
    raw = raw.where(valid)
    return raw</pre></div></details><details class="la-sig"><summary><code>turn_of_year_tax_loss_pressure</code><span class="la-tag">other</span><span class="gist">Tax-loss-selling overhang: stocks far below 52-week high, volatile, and down over the trailing year…</span></summary><div class="la-sig-body"><p>Tax-loss-selling overhang: stocks far below 52-week high, volatile, and down over the trailing year experience predictable selling pressure into year-end (Nov/Dec) as taxable investors realize losses, and a mechanical reversal in January when the selling pressure abates. Score is negative overhang in Nov/Dec (short loss-overhang names) and positive overhang in January (own loss-overhang names), NaN otherwise.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    high_ratio = panel['high_ratio']
    ret_12m = panel['ret_12m']
    vol_252 = panel['vol_252']
    valid = high_ratio.notna() &amp; ret_12m.notna() &amp; vol_252.notna()
    loss_flag = (ret_12m &lt; 0).astype(float)
    overhang = (1.0 - high_ratio) * vol_252 * loss_flag
    overhang = overhang.where(valid, np.nan)
    dates = panel.index.get_level_values('date')
    month = pd.Series(dates, index=panel.index).dt.month
    score = pd.Series(np.nan, index=panel.index, dtype=float)
    nov_dec_mask = month.isin([10, 11])
    jan_mask = month == 12
    score = score.where(~nov_dec_mask, -overhang)
    score = score.where(~jan_mask, overhang)
    return score</pre></div></details><details class="la-sig"><summary><code>turn_of_year_tax_loss_rebound_intensity</code><span class="la-tag">other</span><span class="gist">Seasonally-gated tax-loss selling rebound signal: combines proximity to 52-week low, poor 12-month returns,…</span></summary><div class="la-sig-body"><p>Seasonally-gated tax-loss selling rebound signal: combines proximity to 52-week low, poor 12-month returns, and high volatility (scaled inversely by log market cap) into a loss-selling pressure measure. This pressure is expressed long in Nov/Dec/Jan (tax-loss selling and January rebound window) and reversed (shorted) in May/Jun/Jul (mid-year window-dressing/momentum dominance), zero otherwise.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ret_12m = panel['ret_12m']
    high_ratio = panel['high_ratio']
    vol_252 = panel['vol_252']
    marketcap = panel['marketcap']
    valid = (
        ret_12m.notna()
        &amp; high_ratio.notna()
        &amp; vol_252.notna()
        &amp; marketcap.notna()
        &amp; (marketcap &gt; 0)
    )
    log_mktcap = np.log(marketcap.where(marketcap &gt; 0))
    neg_ret = (-ret_12m).clip(lower=0)
    pressure = (1 - high_ratio) * neg_ret * vol_252 / log_mktcap
    dates = panel.index.get_level_values('date')
    month = pd.Series(dates, index=panel.index).dt.month
    gate = pd.Series(0.0, index=panel.index)
    gate[month.isin([11, 12, 1])] = 1.0
    gate[month.isin([5, 6, 7])] = -0.5
    score = gate * pressure
    score = score.where(valid, np.nan)
    return score</pre></div></details><details class="la-sig"><summary><code>usd_exposure_dollar_trend_interaction</code><span class="la-tag">other</span><span class="gist">Interaction of a stock's trailing USD beta with the prevailing 12m dollar trend (normalized by expanding…</span></summary><div class="la-sig-body"><p>Interaction of a stock's trailing USD beta with the prevailing 12m dollar trend (normalized by expanding past-only volatility), capturing slow incorporation of currency exposure into prices conditioned on FX momentum.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    beta = panel['beta_usd_252'].copy()
    corr = panel['corr_usd_252']
    # winsorize beta at 1/99 pct cross-sectionally each date
    def winsorize(s):
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    beta_w = beta.groupby(level='date').transform(winsorize)
    # keep only where corr magnitude significant
    exposure = beta_w.where(corr.abs() &gt; 0.05, np.nan)
    # date-level trend computation
    usd_chg = panel['macro_usd_chg12m'].groupby(level='date').first()
    usd_chg = usd_chg.sort_index()
    # expanding past-only std (exclude current date), min_periods for stability
    past_std = usd_chg.shift(1).expanding(min_periods=12).std()
    trend = usd_chg / past_std
    trend = trend.where(past_std &gt; 0, np.nan)
    trend_full = trend.reindex(panel.index.get_level_values('date')).values
    trend_series = pd.Series(trend_full, index=panel.index)
    signal = exposure * trend_series
    return signal</pre></div></details><details class="la-sig"><summary><code>vix_conditioned_reversal_flip</code><span class="la-tag">other</span><span class="gist">Short-term reversal scaled by trailing volatility and conditioned on a VIX z-score: when VIX is unusually…</span></summary><div class="la-sig-body"><p>Short-term reversal scaled by trailing volatility and conditioned on a VIX z-score: when VIX is unusually elevated relative to its trailing history, reversal is strong and rewarded; when VIX is unusually low, the sign flips toward continuation.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    vix = panel['macro_vix'].groupby(level='date').first()
    vix = vix.sort_index()
    vix_shift = vix.shift(1)
    vix_mean = vix_shift.rolling(60, min_periods=24).mean()
    vix_std = vix_shift.rolling(60, min_periods=24).std()
    vz = (vix - vix_mean) / vix_std
    vz = vz.replace([np.inf, -np.inf], np.nan)
    vz = vz.clip(-2, 2)
    vz_mapped = panel.index.get_level_values('date').map(vz)
    vz_mapped = pd.Series(vz_mapped, index=panel.index)
    vol_252 = panel['vol_252']
    ret_1m = panel['ret_1m']
    vol_safe = vol_252.where(vol_252 &gt; 0)
    scaled_rev = -ret_1m / vol_safe
    score = scaled_rev * vz_mapped
    score = score.where(vol_safe.notna() &amp; ret_1m.notna() &amp; vz_mapped.notna())
    return score</pre></div></details><details class="la-sig"><summary><code>abnormal_reporting_lag</code><span class="la-tag">quality</span><span class="gist">Filing delay (fund_datekey - fund_calendardate) abnormally long relative to the firm's own history 12 months…</span></summary><div class="la-sig-body"><p>Filing delay (fund_datekey - fund_calendardate) abnormally long relative to the firm's own history 12 months ago, combined with staleness of the current filing (asof_date - fund_datekey), predicts underperformance due to market underreaction to soft disclosure-timing signals of bad news.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    datekey = pd.to_datetime(panel['fund_datekey'])
    calendardate = pd.to_datetime(panel['fund_calendardate'])
    asof = pd.to_datetime(panel['asof_date'])
    valid = datekey.notna() &amp; calendardate.notna() &amp; asof.notna()
    lag_days = (datekey - calendardate).dt.days.astype(float)
    lag_days = lag_days.where(valid, np.nan)
    lag_days = lag_days.clip(lower=0, upper=400)
    stale_days = (asof - datekey).dt.days.astype(float)
    stale_days = stale_days.where(valid, np.nan)
    lag_shift12 = lag_days.groupby(level='ticker').shift(12)
    ab_lag = lag_days - lag_shift12
    ab_lag = ab_lag.where(lag_shift12.notna(), np.nan)
    z_ab_lag = _cs_z(ab_lag)
    z_stale = _cs_z(stale_days)
    score = 0.7 * z_ab_lag + 0.3 * z_stale
    score = score.where(valid &amp; lag_shift12.notna(), np.nan)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>accruals</code><span class="la-tag">quality</span><span class="gist">Sloan accruals = (net income - operating cash flow) / assets; high accruals underperform</span></summary><div class="la-sig-body"><p>Sloan accruals = (net income - operating cash flow) / assets; high accruals underperform.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    assets = panel["assets"]
    return ((panel["netinc"] - panel["ncfo"]) / assets).where(assets &gt; 0)</pre></div></details><details class="la-sig"><summary><code>book_tax_gap_earnings_quality</code><span class="la-tag">quality</span><span class="gist">Book-tax gap earnings quality: firms with effective tax rates near peer median (i.e., not unusually low) and…</span></summary><div class="la-sig-body"><p>Book-tax gap earnings quality: firms with effective tax rates near peer median (i.e., not unusually low) and with book income confirmed by taxable income (high netinc/ebt conformity) exhibit more persistent earnings and higher future abnormal returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    taxexp = panel['taxexp']
    ebt = panel['ebt']
    netinc = panel['netinc']
    revenue = panel['revenue']
    sector = panel['sector']
    valid = (
        ebt.notna() &amp; (ebt &gt; 0)
        &amp; taxexp.notna()
        &amp; revenue.notna() &amp; (revenue &gt; 0)
        &amp; sector.notna()
    )
    etr = pd.Series(np.nan, index=panel.index)
    etr_raw = taxexp / ebt
    etr_raw = etr_raw.clip(lower=0, upper=0.6)
    etr[valid] = etr_raw[valid]
    conf = pd.Series(np.nan, index=panel.index)
    conf_raw = netinc / ebt
    conf[valid] = conf_raw[valid]
    date_level = panel.index.get_level_values('date')
    # Median etr within (date, sector), computed only over valid rows.
    etr_valid_only = etr.where(valid)
    group_keys = [date_level, sector]
    sector_median = etr_valid_only.groupby(group_keys).transform('median')
    raw = etr - sector_median
    # Cross-sectional zscore of conf within date, computed only over valid rows.
    conf_valid_only = conf.where(valid)
    grp = conf_valid_only.groupby(date_level)
    mean_c = grp.transform('mean')
    std_c = grp.transform('std')
    zscore_conf = (conf_valid_only - mean_c) / std_c.replace(0, np.nan)
    signal = raw - 0.25 * zscore_conf
    signal = signal.where(valid)
    return signal</pre></div></details><details class="la-sig"><summary><code>cash_backed_earnings_surprise</code><span class="la-tag">quality</span><span class="gist">Standardized unexpected earnings (SUE) combined with a penalty for accrual-driven surprise: isolates the…</span></summary><div class="la-sig-body"><p>Standardized unexpected earnings (SUE) combined with a penalty for accrual-driven surprise: isolates the cash-backed portion of earnings surprises which is expected to drift more persistently, while accrual-tainted surprises mean-revert.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    epsdil = panel['epsdil']
    netinc = panel['netinc']
    ncfo = panel['ncfo']
    assets = panel['assets']
    g = epsdil.groupby(level='ticker')
    # SUE calculation
    d_eps = epsdil - g.shift(12)
    def rolling_std_24(s):
        return s.rolling(24, min_periods=8).std()
    d_eps_std = d_eps.groupby(level='ticker').transform(rolling_std_24)
    d_eps_std = d_eps_std.where(d_eps_std &gt; 0)
    sue = d_eps / d_eps_std
    sue = sue.clip(-5, 5)
    # Accruals calculation
    acc = (netinc - ncfo) / assets
    acc = acc.where(assets &gt; 0)
    acc = acc.clip(-0.5, 0.5)
    d_acc = acc - acc.groupby(level='ticker').shift(12)
    d_acc = d_acc.clip(-0.5, 0.5)
    def zscore_by_date(s):
        def _z(x):
            mu = x.mean()
            sd = x.std()
            if sd is None or sd == 0 or np.isnan(sd):
                return pd.Series(np.nan, index=x.index)
            return (x - mu) / sd
        return s.groupby(level='date').transform(_z)
    z_sue = zscore_by_date(sue)
    z_dacc = zscore_by_date(d_acc)
    score = z_sue - z_dacc
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>depreciation_conservatism_reserve</code><span class="la-tag">quality</span><span class="gist">Accounting conservatism reserve: firms with high depreciation rate (depamor/tangibles) and capital intensity…</span></summary><div class="la-sig-body"><p>Accounting conservatism reserve: firms with high depreciation rate (depamor/tangibles) and capital intensity (capex/assets) relative to industry peers understate earnings/book value, implying hidden value and future upward re-rating.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    depamor = panel["depamor"]
    tangibles = panel["tangibles"]
    capex = panel["capex"]
    assets = panel["assets"]
    industry = panel["industry"]
    dep_rate = pd.Series(np.nan, index=panel.index)
    valid_tang = tangibles &gt; 0
    dep_rate.loc[valid_tang] = depamor.loc[valid_tang] / tangibles.loc[valid_tang]
    capint = pd.Series(np.nan, index=panel.index)
    valid_assets = assets &gt; 0
    capint.loc[valid_assets] = capex.loc[valid_assets] / assets.loc[valid_assets]
    valid = (
        valid_tang
        &amp; valid_assets
        &amp; depamor.notna()
        &amp; tangibles.notna()
        &amp; capex.notna()
        &amp; assets.notna()
        &amp; industry.notna()
    )
    dep_rate = dep_rate.where(valid)
    capint = capint.where(valid)
    df = pd.DataFrame({
        "dep_rate": dep_rate,
        "capint": capint,
        "industry": industry,
    })
    group_keys = [df.index.get_level_values("date"), df["industry"]]
    dep_med = df.groupby(group_keys)["dep_rate"].transform("median")
    capint_med = df.groupby(group_keys)["capint"].transform("median")
    score = (df["dep_rate"] - dep_med) + 0.5 * (df["capint"] - capint_med)
    score = score.where(valid)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>earnings_smoothing_detection</code><span class="la-tag">quality</span><span class="gist">Ratio of rolling volatility of net income (scaled by assets) to rolling volatility of operating cash flow…</span></summary><div class="la-sig-body"><p>Ratio of rolling volatility of net income (scaled by assets) to rolling volatility of operating cash flow (scaled by assets), per ticker over the trailing 36 months (min 24). A low ratio indicates aggressive earnings smoothing via accruals relative to underlying cash flow volatility, which predicts lower future returns; expected_sign=+1 (high ratio = less smoothing = better quality).</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    assets = panel['assets']
    valid_assets = assets.where(assets &gt; 0)
    ni = panel['netinc'] / valid_assets
    cf = panel['ncfo'] / valid_assets
    ni = ni.replace([np.inf, -np.inf], np.nan)
    cf = cf.replace([np.inf, -np.inf], np.nan)
    sd_ni = ni.groupby(level='ticker').rolling(36, min_periods=24).std()
    sd_cf = cf.groupby(level='ticker').rolling(36, min_periods=24).std()
    sd_ni.index = sd_ni.index.droplevel(0)
    sd_cf.index = sd_cf.index.droplevel(0)
    sd_cf_valid = sd_cf.where(sd_cf &gt; 0)
    signal = sd_ni / sd_cf_valid
    signal = signal.replace([np.inf, -np.inf], np.nan)
    signal = signal.reindex(panel.index)
    return signal</pre></div></details><details class="la-sig"><summary><code>earnings_smoothing_opacity</code><span class="la-tag">quality</span><span class="gist">Ratio of trailing cash-flow volatility to trailing earnings volatility (both scaled by assets), capturing…</span></summary><div class="la-sig-body"><p>Ratio of trailing cash-flow volatility to trailing earnings volatility (both scaled by assets), capturing earnings smoothing/opacity from accrual management. Low sd_c/sd_e (smooth earnings vs volatile cash flows) indicates managed, low-information earnings and predicts lower future returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    assets = panel['assets']
    valid_assets = assets &gt; 0
    ni = (panel['netinc'] / assets).where(valid_assets)
    cf = (panel['ncfo'] / assets).where(valid_assets)
    lags = [0, 3, 6, 9, 12, 15, 18, 21]
    ni_cols = []
    cf_cols = []
    for k in lags:
        ni_k = ni.groupby(level='ticker').shift(k)
        cf_k = cf.groupby(level='ticker').shift(k)
        # also require underlying raw fields present at that lag
        netinc_k = panel['netinc'].groupby(level='ticker').shift(k)
        ncfo_k = panel['ncfo'].groupby(level='ticker').shift(k)
        assets_k = panel['assets'].groupby(level='ticker').shift(k)
        valid_k = netinc_k.notna() &amp; ncfo_k.notna() &amp; assets_k.notna() &amp; (assets_k &gt; 0)
        ni_k = ni_k.where(valid_k)
        cf_k = cf_k.where(valid_k)
        ni_cols.append(ni_k)
        cf_cols.append(cf_k)
    ni_df = pd.concat(ni_cols, axis=1)
    cf_df = pd.concat(cf_cols, axis=1)
    ni_count = ni_df.notna().sum(axis=1)
    cf_count = cf_df.notna().sum(axis=1)
    sd_ni = ni_df.std(axis=1, ddof=1)
    sd_cf = cf_df.std(axis=1, ddof=1)
    valid = (ni_count &gt;= 6) &amp; (cf_count &gt;= 6) &amp; (sd_ni &gt; 0)
    smoothing = (sd_cf / sd_ni).where(valid)
    score = np.log(smoothing.where(smoothing &gt; 0))
    score = score.reindex(panel.index)
    return score</pre></div></details><details class="la-sig"><summary><code>earnings_smoothing_vs_cashflow_volatility</code><span class="la-tag">quality</span><span class="gist">Leuz-Nanda-Wysocki style earnings-smoothing proxy: ratio of trailing 24-month rolling standard deviation of…</span></summary><div class="la-sig-body"><p>Leuz-Nanda-Wysocki style earnings-smoothing proxy: ratio of trailing 24-month rolling standard deviation of operating-cash-flow-to-assets to the rolling standard deviation of net-income-to-assets. High values indicate heavily smoothed earnings relative to underlying cash flow volatility, a marker of low earnings quality that predicts underperformance.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    assets = panel['assets']
    netinc = panel['netinc']
    ncfo = panel['ncfo']
    valid_assets = assets &gt; 0
    ni_a = (netinc / assets).where(valid_assets)
    cfo_a = (ncfo / assets).where(valid_assets)
    df = pd.DataFrame({'ni_a': ni_a, 'cfo_a': cfo_a}, index=panel.index)
    # Ensure strictly backward-looking rolling computation: sort by date
    # within each ticker before applying rolling window operations.
    df = df.sort_index(level=['ticker', 'date'])
    def _roll_std(x: pd.Series) -&gt; pd.Series:
        return x.rolling(window=24, min_periods=12).std()
    s_ni = df.groupby(level='ticker')['ni_a'].apply(_roll_std)
    s_cfo = df.groupby(level='ticker')['cfo_a'].apply(_roll_std)
    # groupby.apply with a Series-returning function may produce an extra
    # level in the index; align back to df's index safely.
    s_ni.index = df.index
    s_cfo.index = df.index
    signal = (s_cfo / s_ni).where(s_ni &gt; 0)
    # Reindex to the original panel order.
    signal = signal.reindex(panel.index)
    return signal</pre></div></details><details class="la-sig"><summary><code>earnings_smoothness_vs_cashflow</code><span class="la-tag">quality</span><span class="gist">Ratio of volatility of scaled net income to volatility of scaled operating cash flow, following…</span></summary><div class="la-sig-body"><p>Ratio of volatility of scaled net income to volatility of scaled operating cash flow, following Leuz-Nanda-Wysocki earnings smoothness proxy. High ratio (unmanaged earnings) predicts higher subsequent returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    scale = panel['assets'].where(panel['assets'] &gt; 0)
    e = panel['netinc'] / scale
    c = panel['ncfo'] / scale
    g_e = e.groupby(level='ticker')
    g_c = c.groupby(level='ticker')
    lags = [0, 3, 6, 9, 12, 15, 18, 21]
    E = pd.concat([g_e.shift(k) for k in lags], axis=1)
    C = pd.concat([g_c.shift(k) for k in lags], axis=1)
    sd_e = E.std(axis=1, ddof=1, skipna=True).where(E.count(axis=1) &gt;= 6)
    sd_c = C.std(axis=1, ddof=1, skipna=True).where(C.count(axis=1) &gt;= 6)
    ratio = sd_e / sd_c.where(sd_c &gt; 0)
    ratio = ratio.where(sd_e.notna() &amp; sd_c.notna() &amp; (sd_c &gt; 0))
    return ratio.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>effective_tax_rate_earnings_quality</code><span class="la-tag">quality</span><span class="gist">Effective tax rate (taxexp/ebt) as earnings-quality diagnostic: firms with high, rising cash-taxed earnings…</span></summary><div class="la-sig-body"><p>Effective tax rate (taxexp/ebt) as earnings-quality diagnostic: firms with high, rising cash-taxed earnings (high ETR level and increasing ETR) are flagged as higher quality and expected to outperform, while low/falling ETR firms (book-tax gap) are flagged as low quality earnings likely to mean-revert.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    taxexp = panel['taxexp']
    ebt = panel['ebt']
    netinc = panel['netinc']
    assets = panel['assets']
    valid = (ebt &gt; 0) &amp; (netinc &gt; 0) &amp; (assets &gt; 0)
    etr = taxexp / ebt.where(ebt &gt; 0)
    etr = etr.where(valid)
    etr = etr.clip(lower=-0.2, upper=0.6)
    etr_lag = etr.groupby(level='ticker').shift(12)
    change = etr - etr_lag
    change = change.where(~(etr_lag.isna() &amp; etr.notna()), 0.0)
    score = 0.6 * etr + 0.4 * change
    score = score.where(valid)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>excess_speculative_volatility</code><span class="la-tag">quality</span><span class="gist">Compares price return volatility to earnings (ROA) volatility</span></summary><div class="la-sig-body"><p>Compares price return volatility to earnings (ROA) volatility. Stocks whose return volatility greatly exceeds their fundamental (earnings) volatility are driven by speculative/noise trading rather than fundamentals and tend to underperform. Score = z_cs(log(vol_252)) - z_cs(log(earn_vol + 1e-4)).</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    assets = panel['assets']
    netinc = panel['netinc']
    roa_q = pd.Series(np.nan, index=panel.index)
    valid_assets = assets &gt; 0
    roa_q[valid_assets] = netinc[valid_assets] / assets[valid_assets]
    lags = [0, 3, 6, 9, 12, 15, 18, 21]
    shifted_list = []
    grouped = roa_q.groupby(level='ticker')
    for k in lags:
        shifted_list.append(grouped.shift(k))
    roa_df = pd.concat(shifted_list, axis=1)
    count_valid = roa_df.notna().sum(axis=1)
    earn_vol = roa_df.std(axis=1, ddof=1)
    earn_vol = earn_vol.where(count_valid &gt;= 6, np.nan)
    vol_252 = panel['vol_252']
    log_vol = np.log(vol_252.where(vol_252 &gt; 0))
    log_earn_vol = np.log(earn_vol + 1e-4)
    df = pd.DataFrame({'log_vol': log_vol, 'log_earn_vol': log_earn_vol})
    def zscore(s):
        mean = s.mean()
        std = s.std(ddof=0)
        if std is None or std == 0 or pd.isna(std):
            return pd.Series(np.nan, index=s.index)
        return (s - mean) / std
    z_vol = df['log_vol'].groupby(level='date').transform(zscore)
    z_earn_vol = df['log_earn_vol'].groupby(level='date').transform(zscore)
    score = z_vol - z_earn_vol
    score = score.where(log_vol.notna() &amp; log_earn_vol.notna(), np.nan)
    return score</pre></div></details><details class="la-sig"><summary><code>financial_slack_under_tightening</code><span class="la-tag">quality</span><span class="gist">Balance-sheet slack (cash + 4*OCF - current debt, scaled by assets) cross-sectionally ranked and conditioned…</span></summary><div class="la-sig-body"><p>Balance-sheet slack (cash + 4*OCF - current debt, scaled by assets) cross-sectionally ranked and conditioned on a past-only expanding z-score of the 12m change in fed funds rate and Baa spread, so the slack premium is amplified in monetary/credit tightening regimes and dampened/reversed in easing regimes.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cashneq = panel['cashneq']
    ncfo = panel['ncfo']
    debtc = panel['debtc']
    assets = panel['assets']
    valid = (
        assets.notna() &amp; (assets &gt; 0)
        &amp; cashneq.notna() &amp; ncfo.notna() &amp; debtc.notna()
    )
    slack = (cashneq + 4.0 * ncfo - debtc) / assets
    slack = slack.where(valid)
    def rank_z(s: pd.Series) -&gt; pd.Series:
        r = s.rank(pct=True)
        mean = r.mean()
        std = r.std()
        if std is None or std == 0 or pd.isna(std):
            return pd.Series(np.nan, index=s.index)
        return (r - mean) / std
    slack_z = slack.groupby(level='date').transform(rank_z)
    # date-level macro series
    ff_chg = panel['macro_fedfunds_chg12m'].groupby(level='date').first().fillna(0.0)
    baa_chg = panel['macro_baa_spread_chg12m'].groupby(level='date').first().fillna(0.0)
    def expanding_z(s: pd.Series) -&gt; pd.Series:
        s = s.sort_index()
        mean = s.expanding().mean().shift(1)
        std = s.expanding().std().shift(1)
        z = (s - mean) / std
        z = z.where(std &gt; 0)
        return z
    ff_z = expanding_z(ff_chg)
    baa_z = expanding_z(baa_chg)
    tight = (0.6 * ff_z + 0.4 * baa_z).clip(-2, 2)
    tight = tight.fillna(0.0)
    tight_full = slack_z.index.get_level_values('date').map(tight)
    tight_full = pd.Series(tight_full.values, index=slack_z.index)
    signal = slack_z * tight_full
    signal = signal.where(slack_z.notna())
    return signal</pre></div></details><details class="la-sig"><summary><code>gross_profitability</code><span class="la-tag">quality</span><span class="gist">Novy-Marx gross profitability = gross profit / total assets</span></summary><div class="la-sig-body"><p>Novy-Marx gross profitability = gross profit / total assets.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    assets = panel["assets"]
    return (panel["gp"] / assets).where(assets &gt; 0)</pre></div></details><details class="la-sig"><summary><code>hidden_credit_risk_mismatch</code><span class="la-tag">quality</span><span class="gist">Identifies highly levered firms whose equity shows little or no measured sensitivity to credit spread changes…</span></summary><div class="la-sig-body"><p>Identifies highly levered firms whose equity shows little or no measured sensitivity to credit spread changes (near-zero/positive corr to credit), combined with weak liquidity, indicating dormant/unpriced default risk that tends to resolve violently during funding shocks and is associated with low future returns (distress-risk anomaly).</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debt = panel['debt']
    cashneq = panel['cashneq']
    marketcap = panel['marketcap']
    currentratio = panel['currentratio']
    corr_credit = panel['corr_credit_252']
    valid = (
        debt.notna()
        &amp; cashneq.notna()
        &amp; marketcap.notna()
        &amp; (marketcap &gt; 0)
        &amp; currentratio.notna()
        &amp; corr_credit.notna()
    )
    netdebt_mcap = pd.Series(np.nan, index=panel.index)
    netdebt_mcap.loc[valid] = (debt[valid] - cashneq[valid]) / marketcap[valid]
    currentratio_valid = pd.Series(np.nan, index=panel.index)
    currentratio_valid.loc[valid] = currentratio[valid]
    corr_credit_valid = pd.Series(np.nan, index=panel.index)
    corr_credit_valid.loc[valid] = corr_credit[valid]
    score = pd.Series(np.nan, index=panel.index)
    dates = panel.index.get_level_values('date')
    def process_group(idx):
        g_valid = valid.loc[idx]
        if not g_valid.any():
            return pd.Series(np.nan, index=idx)
        g_netdebt = netdebt_mcap.loc[idx][g_valid]
        g_currentratio = currentratio_valid.loc[idx][g_valid]
        g_corr_credit = corr_credit_valid.loc[idx][g_valid]
        g_netdebt_w = _winsorize(g_netdebt)
        g_currentratio_w = _winsorize(g_currentratio)
        lev_z = _cs_zscore(g_netdebt_w)
        liq_z = -_cs_zscore(g_currentratio_w)
        credit_ins_z = _cs_zscore(g_corr_credit)
        g_score = 0.5 * lev_z + 0.25 * liq_z + 0.5 * credit_ins_z
        result = pd.Series(np.nan, index=idx)
        result.loc[g_score.index] = g_score
        return result
    score = panel.groupby(level='date', group_keys=False).apply(
        lambda g: process_group(g.index)
    )
    score = score.reindex(panel.index)
    return score</pre></div></details><details class="la-sig"><summary><code>hidden_dilution_sbc_yield</code><span class="la-tag">quality</span><span class="gist">Scales share-based compensation by market cap to estimate the annual dilution cost investors bear that is…</span></summary><div class="la-sig-body"><p>Scales share-based compensation by market cap to estimate the annual dilution cost investors bear that is hidden in non-GAAP earnings and operating cash flow. High SBC yield, adjusted for a cash-flow quality check, predicts lower future returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    ncfo = panel['ncfo']
    assets = panel['assets']
    valid = sbcomp.notna() &amp; marketcap.notna() &amp; (marketcap &gt; 0) &amp; (sbcomp &gt;= 0)
    sbc_yield = (4.0 * sbcomp) / marketcap.where(marketcap &gt; 0)
    cash_check_raw = (4.0 * (ncfo - sbcomp)) / assets.where(assets &gt; 0)
    cash_check = cash_check_raw.where(
        ncfo.notna() &amp; assets.notna() &amp; (assets &gt; 0), 0.0
    )
    cash_check_clipped = cash_check.clip(lower=-0.5, upper=0.5)
    signal = sbc_yield - 0.5 * cash_check_clipped
    signal = signal.where(valid, np.nan)
    return signal</pre></div></details><details class="la-sig"><summary><code>hidden_equity_dilution_cost</code><span class="la-tag">quality</span><span class="gist">SBC-to-market and SBC-to-operating-cash-flow burden, demeaned within sector, capturing hidden dilution cost…</span></summary><div class="la-sig-body"><p>SBC-to-market and SBC-to-operating-cash-flow burden, demeaned within sector, capturing hidden dilution cost that the market underreacts to.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    sector = panel['sector']
    ncfo = panel['ncfo']
    sbc_ann = sbcomp * 4.0
    sbc_ann = sbc_ann.where(sbcomp &gt;= 0)
    valid_mc = marketcap.where(marketcap &gt; 0)
    burden = sbc_ann / valid_mc
    burden = burden.where(valid_mc.notna())
    ncfo_ann = ncfo * 4.0
    abs_ncfo_ann = ncfo_ann.abs()
    intensity2 = sbc_ann / abs_ncfo_ann
    intensity2 = intensity2.where(abs_ncfo_ann &gt; 0)
    # winsorize intensity2 at cross-sectional 1/99 pct per date
    def winsorize(s):
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    intensity2_w = intensity2.groupby(level='date').transform(
        lambda s: winsorize(s) if s.notna().sum() &gt; 0 else s
    )
    # base drop conditions
    valid_mask = (sbcomp.notna() &amp; (sbcomp &gt;= 0) &amp;
                  marketcap.notna() &amp; (marketcap &gt; 0) &amp;
                  sector.notna())
    burden_valid = burden.where(valid_mask)
    intensity2_valid = intensity2_w.where(valid_mask)
    # exclude stock-dates where both inputs are NaN
    both_nan = burden_valid.isna() &amp; intensity2_valid.isna()
    def cs_rank(s):
        return s.groupby(level='date').rank(pct=True)
    rank_burden = cs_rank(burden_valid)
    rank_intensity2 = cs_rank(intensity2_valid)
    raw = 0.7 * rank_burden.fillna(0) + 0.3 * rank_intensity2.fillna(0)
    weight_sum = 0.7 * rank_burden.notna().astype(float) + 0.3 * rank_intensity2.notna().astype(float)
    raw = raw / weight_sum.replace(0, np.nan)
    raw = raw.where(~both_nan)
    raw = raw.where(valid_mask)
    df = pd.DataFrame({'raw': raw, 'sector': sector})
    median_by_sector = df.groupby([df.index.get_level_values('date'), 'sector'])['raw'].transform('median')
    score = raw - median_by_sector
    score = score.where(valid_mask &amp; ~both_nan)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>inflation_passthrough_margin_expansion</code><span class="la-tag">quality</span><span class="gist">Year-over-year gross margin expansion, weighted more heavily when aggregate inflation is high, capturing…</span></summary><div class="la-sig-body"><p>Year-over-year gross margin expansion, weighted more heavily when aggregate inflation is high, capturing pricing power that lets firms pass through cost shocks. High inflation regimes better reveal true pricing power, so margin gains carry more signal then.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    grossmargin = panel['grossmargin']
    gp = panel['gp']
    revenue = panel['revenue']
    gm_primary_valid = grossmargin.notna() &amp; (grossmargin &gt; -1) &amp; (grossmargin &lt; 2)
    gm_fallback = gp / revenue.where(revenue &gt; 0)
    gm = grossmargin.where(gm_primary_valid, gm_fallback)
    dgm = gm - gm.groupby(level='ticker').shift(12)
    macro_cpi = panel['macro_cpi']
    macro_cpi_chg12m = panel['macro_cpi_chg12m']
    denom = macro_cpi - macro_cpi_chg12m
    infl = macro_cpi_chg12m / denom.where(denom &gt; 0)
    infl = infl.where(denom.notna() &amp; macro_cpi_chg12m.notna())
    # date-level series
    infl_by_date = infl.groupby(level='date').first()
    infl_by_date = infl_by_date.sort_index()
    # expanding past-only percentile rank (using only prior dates, excluding current)
    w_t = pd.Series(index=infl_by_date.index, dtype=float)
    vals = infl_by_date.values
    for i in range(len(vals)):
        if i == 0:
            w_t.iloc[i] = np.nan
            continue
        past = vals[:i]
        past_valid = past[~np.isnan(past)]
        cur = vals[i]
        if np.isnan(cur) or len(past_valid) == 0:
            w_t.iloc[i] = np.nan
        else:
            rank = (past_valid &lt; cur).sum() / len(past_valid)
            w_t.iloc[i] = rank
    w_t_full = infl.index.get_level_values('date').map(w_t)
    w_t_full = pd.Series(w_t_full, index=infl.index)
    score = dgm * (0.25 + w_t_full)
    valid = dgm.notna() &amp; infl.notna() &amp; w_t_full.notna()
    score = score.where(valid)
    return score.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>net_trade_credit_expansion</code><span class="la-tag">quality</span><span class="gist">Change in net trade credit (receivables minus payables scaled by revenue) over the trailing 12 months</span></summary><div class="la-sig-body"><p>Change in net trade credit (receivables minus payables scaled by revenue) over the trailing 12 months. Rising net trade credit signals loosening credit standards and deteriorating earnings quality, predicting underperformance.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    receivables = panel['receivables']
    payables = panel['payables']
    revenue = panel['revenue']
    valid = receivables.notna() &amp; payables.notna() &amp; revenue.notna() &amp; (revenue &gt; 0)
    ntc = pd.Series(np.nan, index=panel.index)
    ntc[valid] = (receivables[valid] - payables[valid]) / revenue[valid]
    ntc_lag = ntc.groupby(level='ticker').shift(12)
    score = ntc - ntc_lag
    return score</pre></div></details><details class="la-sig"><summary><code>operating_leverage_industrial_cycle</code><span class="la-tag">quality</span><span class="gist">Interaction of firm fixed-cost operating leverage (depreciation/revenue and non-current asset intensity) with…</span></summary><div class="la-sig-body"><p>Interaction of firm fixed-cost operating leverage (depreciation/revenue and non-current asset intensity) with the industrial production cycle relative to its long-run trailing average. High operating-leverage firms are predicted to outperform when the industrial cycle is accelerating above its long-run norm, and underperform when decelerating.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    depamor = panel['depamor']
    revenue = panel['revenue']
    assets = panel['assets']
    assetsc = panel['assetsc']
    # component 1: depreciation intensity relative to revenue
    dep_ratio = depamor / revenue.where(revenue &gt; 0)
    dep_ratio = dep_ratio.where(revenue &gt; 0)
    # component 2: non-current asset intensity
    fixed_ratio = (assets - assetsc) / assets.where(assets &gt; 0)
    fixed_ratio = fixed_ratio.where(assets &gt; 0)
    dates = panel.index.get_level_values('date')
    rank1 = dep_ratio.groupby(dates).rank(pct=True)
    rank2 = fixed_ratio.groupby(dates).rank(pct=True)
    oper_lev = 0.5 * rank1 + 0.5 * rank2
    # demean within date to [-0.5, 0.5]
    oper_lev = oper_lev - oper_lev.groupby(dates).transform('mean')
    # macro cycle at date level (past-only expanding mean)
    macro = panel['macro_indpro_chg12m'].groupby(dates).first()
    macro = macro.sort_index()
    macro_mean = macro.expanding(min_periods=12).mean()
    cyc = macro - macro_mean
    cyc = cyc.clip(-4, 4)
    cyc_aligned = dates.map(cyc)
    cyc_series = pd.Series(cyc_aligned, index=panel.index)
    signal = oper_lev * cyc_series
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>sbc_adjusted_cashflow_yield</code><span class="la-tag">quality</span><span class="gist">SBC-adjusted free cash flow yield: TTM FCF minus TTM |SBC| divided by market cap, capturing the wedge between…</span></summary><div class="la-sig-body"><p>SBC-adjusted free cash flow yield: TTM FCF minus TTM |SBC| divided by market cap, capturing the wedge between headline FCF and shareholder cash flow after charging equity-based compensation as a real expense.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    fcf = panel['fcf']
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    ttm_fcf = self._ttm(fcf)
    ttm_sbc = self._ttm(sbcomp)
    adj = ttm_fcf - ttm_sbc.abs()
    valid = (
        marketcap.notna() &amp; (marketcap &gt; 0) &amp;
        ttm_fcf.notna() &amp; ttm_sbc.notna()
    )
    signal = pd.Series(np.nan, index=panel.index, dtype=float)
    signal[valid] = adj[valid] / marketcap[valid]
    def winsorize(x: pd.Series) -&gt; pd.Series:
        if x.notna().sum() == 0:
            return x
        lo = x.quantile(0.01)
        hi = x.quantile(0.99)
        return x.clip(lower=lo, upper=hi)
    signal = signal.groupby(level='date').transform(winsorize)
    return signal</pre></div></details><details class="la-sig"><summary><code>sbc_inflated_cash_earnings</code><span class="la-tag">quality</span><span class="gist">SBC-intensive firms mechanically inflate cash flow/earnings by treating stock-based compensation as a…</span></summary><div class="la-sig-body"><p>SBC-intensive firms mechanically inflate cash flow/earnings by treating stock-based compensation as a non-cash add-back; high SBC relative to operating cash flow or market cap signals lower earnings quality and future dilution burden, predicting underperformance.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    ncfo = panel['ncfo']
    marketcap = panel['marketcap']
    sbc_ttm = _ttm(sbcomp)
    cfo_ttm = _ttm(ncfo)
    burden1 = pd.Series(np.nan, index=panel.index)
    valid_cfo = cfo_ttm.notna() &amp; (cfo_ttm &gt; 0) &amp; sbc_ttm.notna()
    burden1[valid_cfo] = sbc_ttm[valid_cfo] / cfo_ttm[valid_cfo]
    burden2 = pd.Series(np.nan, index=panel.index)
    valid_mc = marketcap.notna() &amp; (marketcap &gt; 0) &amp; sbc_ttm.notna()
    burden2[valid_mc] = sbc_ttm[valid_mc] / marketcap[valid_mc]
    z1 = _zscore_by_date(burden1)
    z2 = _zscore_by_date(burden2)
    signal = 0.5 * z1 + 0.5 * z2
    return signal</pre></div></details><details class="la-sig"><summary><code>sbc_inflated_operating_cash_flow</code><span class="la-tag">quality</span><span class="gist">Quality-of-earnings signal that flags firms whose operating cash flow and free cash flow are heavily inflated…</span></summary><div class="la-sig-body"><p>Quality-of-earnings signal that flags firms whose operating cash flow and free cash flow are heavily inflated by non-cash share-based compensation add-backs relative to true operating cash generation and revenue. High SBC intensity implies overstated cash-flow quality and ongoing dilution; expected to negatively predict future returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    ncfo = panel['ncfo']
    revenue = panel['revenue']
    valid = sbcomp.notna() &amp; revenue.notna() &amp; (revenue &gt; 0)
    denom = pd.concat(
        [ncfo.abs(), 0.05 * revenue.abs()], axis=1
    ).max(axis=1)
    intensity = pd.Series(np.nan, index=panel.index)
    mask_intensity = valid &amp; denom.notna() &amp; (denom &gt; 0)
    intensity.loc[mask_intensity] = sbcomp.loc[mask_intensity] / denom.loc[mask_intensity]
    alt = pd.Series(np.nan, index=panel.index)
    alt.loc[valid] = sbcomp.loc[valid] / revenue.loc[valid]
    def rank_pct(s: pd.Series) -&gt; pd.Series:
        return s.groupby(level='date').rank(pct=True, na_option='keep')
    rank_intensity = rank_pct(intensity)
    rank_alt = rank_pct(alt)
    signal = 0.5 * rank_intensity + 0.5 * rank_alt
    signal = signal.where(valid)
    return signal</pre></div></details><details class="la-sig"><summary><code>sticky_cost_downturn_drag</code><span class="la-tag">quality</span><span class="gist">Cost stickiness signal: when revenue declines, firms that fail to cut operating costs proportionally…</span></summary><div class="la-sig-body"><p>Cost stickiness signal: when revenue declines, firms that fail to cut operating costs proportionally (opex+cor relative to revenue, log-change difference) carry hidden operating leverage and future earnings drag. Defined only for revenue-declining firms; higher stickiness predicts lower returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    revenue = panel["revenue"]
    opex = panel["opex"]
    cor = panel["cor"]
    opex_tot = opex.where(cor.isna(), opex + cor)
    grp = opex_tot.groupby(level="ticker")
    rev_grp = revenue.groupby(level="ticker")
    rev_lag = rev_grp.shift(12)
    opex_lag = grp.shift(12)
    rev_valid = (revenue &gt; 0) &amp; (rev_lag &gt; 0)
    opex_valid = (opex_tot &gt; 0) &amp; (opex_lag &gt; 0)
    drev = pd.Series(np.nan, index=panel.index)
    drev[rev_valid] = np.log(revenue[rev_valid] / rev_lag[rev_valid])
    dcost = pd.Series(np.nan, index=panel.index)
    dcost[opex_valid] = np.log(opex_tot[opex_valid] / opex_lag[opex_valid])
    stickiness = dcost - drev
    decline_mask = drev &lt; 0
    stickiness = stickiness.where(decline_mask &amp; drev.notna() &amp; dcost.notna())
    return stickiness</pre></div></details><details class="la-sig"><summary><code>stock_comp_burden_yield</code><span class="la-tag">quality</span><span class="gist">Trailing-twelve-month share-based compensation scaled by market cap, demeaned within (date, sector), as a…</span></summary><div class="la-sig-body"><p>Trailing-twelve-month share-based compensation scaled by market cap, demeaned within (date, sector), as a proxy for hidden non-cash compensation burden that overstates reported cash flow quality.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    marketcap = panel['marketcap']
    sector = panel['sector']
    g = sbcomp.groupby(level='ticker')
    lag0 = g.shift(0)
    lag3 = g.shift(3)
    lag6 = g.shift(6)
    lag9 = g.shift(9)
    valid_lags = lag0.notna() &amp; lag3.notna() &amp; lag6.notna() &amp; lag9.notna()
    ttm_sbc = lag0 + lag3 + lag6 + lag9
    valid_mcap = marketcap.notna() &amp; (marketcap &gt; 0)
    valid = valid_lags &amp; valid_mcap &amp; (ttm_sbc &gt;= 0)
    burden = pd.Series(np.nan, index=panel.index, dtype=float)
    burden.loc[valid] = ttm_sbc.loc[valid] / marketcap.loc[valid]
    dates = panel.index.get_level_values('date')
    def cap_99(s):
        valid_vals = s.dropna()
        if valid_vals.empty:
            return s
        cap = valid_vals.quantile(0.99)
        return s.clip(upper=cap)
    burden = burden.groupby(dates).transform(cap_99)
    min_group_size = 3
    group_keys = [dates, sector]
    grp = burden.groupby(group_keys)
    sector_mean = grp.transform('mean')
    sector_count = burden.notna().groupby(group_keys).transform('sum')
    signal = burden - sector_mean
    signal = signal.where(burden.notna() &amp; (sector_count &gt;= min_group_size), np.nan)
    signal.name = "stock_comp_burden_yield"
    return signal</pre></div></details><details class="la-sig"><summary><code>stock_comp_dilution_earnings_overstatement</code><span class="la-tag">quality</span><span class="gist">Flags firms whose profitability/cash flow appear inflated by non-cash stock-based compensation (SBC intensity…</span></summary><div class="la-sig-body"><p>Flags firms whose profitability/cash flow appear inflated by non-cash stock-based compensation (SBC intensity relative to gross profit/revenue, SBC relative to operating cash flow) and whose basic-vs-diluted EPS wedge reveals equity overhang. High scores indicate hidden dilution/earnings overstatement and are expected to underperform (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    gp = panel['gp']
    revenue = panel['revenue']
    ncfo = panel['ncfo']
    eps = panel['eps']
    epsdil = panel['epsdil']
    # sbc_int: sbcomp / gp where gp &gt; 0 and sbcomp &gt;= 0, fallback sbcomp / revenue where revenue &gt; 0
    sbc_int = pd.Series(np.nan, index=panel.index)
    valid_gp = (gp &gt; 0) &amp; (sbcomp &gt;= 0)
    sbc_int.loc[valid_gp] = sbcomp.loc[valid_gp] / gp.loc[valid_gp]
    valid_rev_fallback = (~valid_gp) &amp; (revenue &gt; 0) &amp; (sbcomp &gt;= 0)
    sbc_int.loc[valid_rev_fallback] = sbcomp.loc[valid_rev_fallback] / revenue.loc[valid_rev_fallback]
    # cf_inflation: sbcomp / ncfo where ncfo &gt; 0
    cf_inflation = pd.Series(np.nan, index=panel.index)
    valid_ncfo = ncfo &gt; 0
    cf_inflation.loc[valid_ncfo] = sbcomp.loc[valid_ncfo] / ncfo.loc[valid_ncfo]
    # wedge: (eps - epsdil) / abs(eps) where abs(eps) &gt; 0.01
    wedge = pd.Series(np.nan, index=panel.index)
    valid_eps = eps.abs() &gt; 0.01
    wedge.loc[valid_eps] = (eps.loc[valid_eps] - epsdil.loc[valid_eps]) / eps.loc[valid_eps].abs()
    df = pd.DataFrame({
        'sbc_int': sbc_int,
        'cf_inflation': cf_inflation,
        'wedge': wedge,
    }, index=panel.index)
    dates = panel.index.get_level_values('date')
    ranks = df.groupby(dates).rank(pct=True)
    score = ranks.mean(axis=1, skipna=True)
    all_nan_mask = df.isna().all(axis=1)
    score = score.mask(all_nan_mask, np.nan)
    return score</pre></div></details><details class="la-sig"><summary><code>stock_comp_earnings_dilution</code><span class="la-tag">quality</span><span class="gist">Scales share-based compensation by revenue and by absolute operating cash flow to capture stock-comp-driven…</span></summary><div class="la-sig-body"><p>Scales share-based compensation by revenue and by absolute operating cash flow to capture stock-comp-driven earnings/cash-flow dilution. High values indicate heavy reliance on SBC that inflates cash flow and adjusted earnings while diluting shareholders, a lower quality-of-earnings signal expected to underperform.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    sbcomp = panel['sbcomp']
    revenue = panel['revenue']
    ncfo = panel['ncfo']
    revenue_valid = revenue.where(revenue &gt; 0)
    sbc_rev = sbcomp / revenue_valid
    sbc_rev = sbc_rev.where(revenue_valid.notna() &amp; sbcomp.notna())
    ncfo_abs = ncfo.abs()
    ncfo_valid = ncfo_abs.where(ncfo_abs &gt; 0)
    sbc_ocf = sbcomp / ncfo_valid
    sbc_ocf = sbc_ocf.where(ncfo_valid.notna() &amp; sbcomp.notna())
    r1 = sbc_rev.groupby(level='date').rank(pct=True)
    r2 = sbc_ocf.groupby(level='date').rank(pct=True)
    signal = (r1 + r2) / 2
    signal = signal.where(~(r1.isna() &amp; r2.isna()))
    return signal</pre></div></details><details class="la-sig"><summary><code>trade_credit_stretch_quality</code><span class="la-tag">quality</span><span class="gist">Combines 12-month changes in days payable outstanding (payables/cor) and days sales outstanding…</span></summary><div class="la-sig-body"><p>Combines 12-month changes in days payable outstanding (payables/cor) and days sales outstanding (receivables/revenue) plus an accrual-quality tilt ((netinc-ncfo)/assets) to detect firms stretching trade credit terms to flatter earnings; expected_sign=-1 since higher stretch predicts lower returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    payables = panel['payables']
    cor = panel['cor']
    receivables = panel['receivables']
    revenue = panel['revenue']
    assets = panel['assets']
    ncfo = panel['ncfo']
    netinc = panel['netinc']
    dpo = pd.Series(np.nan, index=panel.index)
    mask_cor = cor &gt; 0
    dpo[mask_cor] = payables[mask_cor] / cor[mask_cor]
    dso = pd.Series(np.nan, index=panel.index)
    mask_rev = revenue &gt; 0
    dso[mask_rev] = receivables[mask_rev] / revenue[mask_rev]
    dpo_lag = dpo.groupby(level='ticker').shift(12)
    dso_lag = dso.groupby(level='ticker').shift(12)
    d_dpo = dpo - dpo_lag
    d_dso = dso - dso_lag
    d_dpo = d_dpo.where(dpo.notna() &amp; dpo_lag.notna())
    d_dso = d_dso.where(dso.notna() &amp; dso_lag.notna())
    accrual = pd.Series(np.nan, index=panel.index)
    mask_assets = assets &gt; 0
    accrual[mask_assets] = (netinc[mask_assets] - ncfo[mask_assets]) / assets[mask_assets]
    def cross_sectional_rank(s):
        return s.groupby(level='date').rank(pct=True)
    rank_dpo = cross_sectional_rank(d_dpo)
    rank_dso = cross_sectional_rank(d_dso)
    rank_accrual = cross_sectional_rank(accrual)
    def neutral_fill(rank_series):
        median_rank = rank_series.groupby(level='date').transform('median')
        return rank_series.fillna(median_rank)
    rank_dpo_filled = neutral_fill(rank_dpo)
    rank_dso_filled = neutral_fill(rank_dso)
    rank_accrual_filled = neutral_fill(rank_accrual)
    signal = rank_dpo_filled + rank_dso_filled + rank_accrual_filled
    return signal</pre></div></details><details class="la-sig"><summary><code>transitory_nonoperating_income_intensity</code><span class="la-tag">quality</span><span class="gist">Absolute magnitude of the gap between pre-tax income (ebt) and operating income (opinc), scaled by total…</span></summary><div class="la-sig-body"><p>Absolute magnitude of the gap between pre-tax income (ebt) and operating income (opinc), scaled by total assets, as a proxy for reliance on transitory non-operating items. Higher magnitude implies lower earnings persistence and predicts lower subsequent returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    ebt = panel['ebt']
    opinc = panel['opinc']
    assets = panel['assets']
    revenue = panel['revenue']
    denom = assets.where(assets &gt; 0)
    nonop = ebt - opinc
    raw = nonop.abs() / denom
    valid = (
        ebt.notna()
        &amp; opinc.notna()
        &amp; assets.notna()
        &amp; revenue.notna()
        &amp; (assets &gt; 0)
        &amp; (revenue.abs() &gt; 0)
    )
    raw = raw.where(valid)
    def _winsorize(s: pd.Series) -&gt; pd.Series:
        if s.dropna().empty:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    result = raw.groupby(level='date').transform(_winsorize)
    return result</pre></div></details><details class="la-sig"><summary><code>working_capital_release_under_tightening</code><span class="la-tag">quality</span><span class="gist">Interacts a firm-level working-capital-efficiency improvement (declining net operating working capital scaled…</span></summary><div class="la-sig-body"><p>Interacts a firm-level working-capital-efficiency improvement (declining net operating working capital scaled by assets, demeaned cross-sectionally) with a macro tightening intensity gauge (rising 2y yield, fed funds, and credit spreads), scaled by revenue growth. Captures funding-relief quality of earnings that binds only in tightening regimes.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    receivables = panel['receivables']
    inventory = panel['inventory']
    payables = panel['payables']
    assets = panel['assets']
    revenue = panel['revenue']
    assets_safe = assets.where(assets &gt; 0)
    nowc = (receivables + inventory - payables) / assets_safe
    nowc_lag12 = nowc.groupby(level='ticker').shift(12)
    d_nowc = nowc - nowc_lag12
    revenue_lag12 = revenue.groupby(level='ticker').shift(12)
    rev_g = (revenue / revenue_lag12 - 1).where(revenue_lag12 &gt; 0)
    rev_g_clipped = rev_g.clip(-0.5, 0.5)
    # cross-sectional median of d_nowc per date, computed only from non-NaN obs
    median_d_nowc = d_nowc.groupby(level='date').transform(
        lambda x: x.median(skipna=True)
    )
    wc_release = -(d_nowc - median_d_nowc)
    # Macro tightening intensity: date-level series
    dates = panel.index.get_level_values('date')
    macro_dgs2 = panel['macro_dgs2_chg12m'].groupby(level='date').first()
    macro_fedfunds = panel['macro_fedfunds_chg12m'].groupby(level='date').first()
    macro_baa = panel['macro_baa_spread_chg12m'].groupby(level='date').first()
    z_dgs2 = self._pastonly_zscore(macro_dgs2)
    z_fedfunds = self._pastonly_zscore(macro_fedfunds)
    z_baa = self._pastonly_zscore(macro_baa)
    tighten = (z_dgs2 + z_fedfunds + z_baa).reindex(dates).values
    tighten_series = pd.Series(tighten, index=panel.index)
    score = wc_release * tighten_series * (1 + rev_g_clipped)
    # NaN propagation for invalid inputs
    invalid_mask = (
        receivables.isna() | inventory.isna() | payables.isna() |
        assets.isna() | (assets &lt;= 0) |
        revenue.isna() | nowc_lag12.isna() | revenue_lag12.isna() |
        (revenue_lag12 &lt;= 0) | tighten_series.isna()
    )
    score = score.where(~invalid_mask)
    return score</pre></div></details><details class="la-sig"><summary><code>zero_earnings_threshold_management</code><span class="la-tag">quality</span><span class="gist">Detects firms with small positive ROA suspiciously close to the zero-earnings threshold (Burgstahler-Dichev…</span></summary><div class="la-sig-body"><p>Detects firms with small positive ROA suspiciously close to the zero-earnings threshold (Burgstahler-Dichev discontinuity), suggesting earnings management to avoid reporting losses. Signal is a Gaussian kernel bump centered at zero-plus, amplified when net income exceeds operating cash flow (weak cash backing). Higher score implies higher suspicion of manipulation and is expected to predict lower future returns.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    assets = panel['assets']
    netinc = panel['netinc']
    ncfo = panel['ncfo']
    revenue = panel['revenue']
    valid_assets = assets.where(assets &gt; 0)
    x = netinc / valid_assets
    bump = np.exp(-0.5 * (x / 0.004) ** 2) * (x &gt; 0)
    cash_gap = ((netinc - ncfo) / valid_assets).clip(-0.5, 0.5)
    cash_gap_rank = cash_gap.groupby(level='date').rank(pct=True)
    signal = bump * (1 + cash_gap_rank)
    invalid = (
        assets.isna() | (assets &lt;= 0)
        | revenue.isna() | (revenue &lt;= 0)
        | netinc.isna()
        | ncfo.isna()
        | x.isna()
    )
    signal = signal.where(~invalid, np.nan)
    return signal</pre></div></details><details class="la-sig"><summary><code>attention_shock_at_the_highs</code><span class="la-tag">size</span><span class="gist">Interaction of abnormal dollar-volume (log dvol_21 vs trailing 12m average) with proximity to 52-week high;…</span></summary><div class="la-sig-body"><p>Interaction of abnormal dollar-volume (log dvol_21 vs trailing 12m average) with proximity to 52-week high; captures attention-driven speculative buying pressure at the highs that tends to mean-revert.</p><div class="la-meta"><span><b>Category</b> size</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dvol = panel['dvol_21']
    high_ratio = panel['high_ratio']
    valid_dvol = dvol &gt; 0
    lv = np.log(dvol.where(valid_dvol))
    grp = lv.groupby(level='ticker')
    lags = [grp.shift(k) for k in range(1, 13)]
    lag_df = pd.concat(lags, axis=1)
    valid_count = lag_df.notna().sum(axis=1)
    baseline = lag_df.mean(axis=1)
    abn = lv - baseline
    abn = abn.where(valid_count &gt;= 9)
    # mask invalid rows
    mask = valid_dvol &amp; high_ratio.notna() &amp; abn.notna()
    abn = abn.where(mask)
    hr = high_ratio.where(mask)
    def pct_rank(s):
        return s.groupby(level='date').rank(pct=True)
    a = pct_rank(abn)
    h = pct_rank(hr)
    signal = a * h
    signal = signal.where(mask)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>conditional_size_credit_cycle</code><span class="la-tag">size</span><span class="gist">Size tilt (small minus large) conditioned on the credit cycle: the tilt is amplified when the Baa credit…</span></summary><div class="la-sig-body"><p>Size tilt (small minus large) conditioned on the credit cycle: the tilt is amplified when the Baa credit spread is trending down (easing) and flipped when it is trending up (tightening), based on the 12-month change in the Baa spread with an expanding, past-only z-score.</p><div class="la-meta"><span><b>Category</b> size</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    marketcap = panel['marketcap']
    baa_chg = panel['macro_baa_spread_chg12m']
    # Per-date macro series (broadcast across tickers, same value per date)
    macro_by_date = baa_chg.groupby(level='date').first()
    macro_by_date = macro_by_date.sort_index()
    # Expanding past-only mean/std (min 36 obs), shifted by 1 to avoid look-ahead
    shifted = macro_by_date.shift(1)
    exp_mean = shifted.expanding(min_periods=36).mean()
    exp_std = shifted.expanding(min_periods=36).std()
    zx = (macro_by_date - exp_mean) / exp_std
    zx = zx.where(exp_std &gt; 0, np.nan)
    zx_clipped = zx.clip(-2, 2)
    easing = -zx_clipped
    # Only valid where macro_baa_spread_chg12m itself is present
    easing = easing.where(macro_by_date.notna(), np.nan)
    # Broadcast easing to panel index
    easing_panel = panel.index.get_level_values('date').map(easing)
    easing_panel = pd.Series(easing_panel, index=panel.index)
    # Size tilt: -log(marketcap), demeaned per date, marketcap&gt;0 only
    valid_mc = marketcap.where(marketcap &gt; 0, np.nan)
    log_size = -np.log(valid_mc)
    date_mean = log_size.groupby(level='date').transform('mean')
    size_tilt = log_size - date_mean
    score = size_tilt * easing_panel
    # Enforce missing-data policy
    score = score.where(marketcap &gt; 0, np.nan)
    score = score.where(easing_panel.notna(), np.nan)
    return score</pre></div></details><details class="la-sig"><summary><code>size_premium_credit_conditions_switch</code><span class="la-tag">size</span><span class="gist">Size premium (-log(marketcap)) whose sign and magnitude are conditioned on the 12m change in the Baa-Treasury…</span></summary><div class="la-sig-body"><p>Size premium (-log(marketcap)) whose sign and magnitude are conditioned on the 12m change in the Baa-Treasury credit spread: when spreads have tightened over the trailing 12 months (credit conditions improving), the small-cap premium is enhanced; when spreads have widened (credit conditions worsening), the size tilt is reversed. cond = tanh(-macro_baa_spread_chg12m / 0.5).</p><div class="la-meta"><span><b>Category</b> size</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    marketcap = panel['marketcap']
    baa_chg = panel['macro_baa_spread_chg12m']
    dvol = panel.get('dvol_21')
    valid = marketcap.notna() &amp; (marketcap &gt; 0) &amp; baa_chg.notna()
    if dvol is not None:
        valid = valid &amp; dvol.notna()
    log_mcap = np.log(marketcap.where(valid))
    size_score = -log_mcap
    # Winsorize size_score cross-sectionally (by date) at 1%/99%
    def _winsorize(s: pd.Series) -&gt; pd.Series:
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    size_score_w = size_score.groupby(level='date').transform(_winsorize)
    cond = np.tanh(-baa_chg / 0.5)
    signal = cond * size_score_w
    signal = signal.where(valid)
    return signal.reindex(panel.index)</pre></div></details><details class="la-sig"><summary><code>turnover_shock_price_run_reversal</code><span class="la-tag">size</span><span class="gist">Abnormal turnover (dollar volume scaled by market cap) relative to a stock's own trailing 12-month history,…</span></summary><div class="la-sig-body"><p>Abnormal turnover (dollar volume scaled by market cap) relative to a stock's own trailing 12-month history, interacted with 1-month return, proxies for attention-driven overshoot that subsequently reverses.</p><div class="la-meta"><span><b>Category</b> size</span><span><b>Direction</b> Long the low scores</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    marketcap = panel['marketcap']
    dvol_21 = panel['dvol_21']
    ret_1m = panel['ret_1m']
    valid_denom = (marketcap &gt; 0) &amp; (dvol_21 &gt; 0)
    turn = pd.Series(np.nan, index=panel.index, dtype=float)
    turn.loc[valid_denom] = dvol_21.loc[valid_denom] / marketcap.loc[valid_denom]
    lturn = np.log(turn)
    grouped = lturn.groupby(level='ticker')
    base = grouped.shift(1).groupby(level='ticker').rolling(12, min_periods=6).mean()
    base = base.reset_index(level=0, drop=True)
    base = base.reindex(panel.index)
    abn_turn = lturn - base
    raw = abn_turn * ret_1m
    raw = raw.where(abn_turn.notna() &amp; ret_1m.notna() &amp; base.notna())
    return raw</pre></div></details><details class="la-sig"><summary><code>book_to_market</code><span class="la-tag">value</span><span class="gist">Fama-French HML: book-to-market = 1/pb (high = cheap = long)</span></summary><div class="la-sig-body"><p>Fama-French HML: book-to-market = 1/pb (high = cheap = long).</p><div class="la-meta"><span><b>Category</b> value</span><span><b>Direction</b> Long the high scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    pb = panel["pb"]
    return (1.0 / pb).where(pb &gt; 0)</pre></div></details><details class="la-sig"><summary><code>cash_vs_accounting_value_divergence</code><span class="la-tag">value</span><span class="gist">Spread between cross-sectional rank of cash-flow yield (FCF/marketcap) and accounting-earnings yield…</span></summary><div class="la-sig-body"><p>Spread between cross-sectional rank of cash-flow yield (FCF/marketcap) and accounting-earnings yield (netinc/marketcap), industry-neutralized within date (fallback to full cross-section median if industry missing). Captures mispricing where GAAP earnings are depressed by non-cash charges relative to cash generation.</p><div class="la-meta"><span><b>Category</b> value</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> price, fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    fcf = panel["fcf"]
    netinc = panel["netinc"]
    marketcap = panel["marketcap"]
    industry = panel["industry"]
    valid = (
        marketcap.notna()
        &amp; (marketcap &gt; 0)
        &amp; fcf.notna()
        &amp; netinc.notna()
    )
    cash_yield = pd.Series(np.nan, index=panel.index, dtype=float)
    acct_yield = pd.Series(np.nan, index=panel.index, dtype=float)
    cash_yield.loc[valid] = fcf.loc[valid] / marketcap.loc[valid]
    acct_yield.loc[valid] = netinc.loc[valid] / marketcap.loc[valid]
    date_level = panel.index.get_level_values("date")
    r1 = cash_yield.groupby(date_level).rank(pct=True)
    r2 = acct_yield.groupby(date_level).rank(pct=True)
    div = r1 - r2
    div = div.where(valid)
    # Industry-date median (fallback to date-level median if industry missing)
    has_industry = industry.notna()
    industry_group_key = pd.Series(
        np.where(has_industry, industry.astype(str), "__NA__"),
        index=panel.index,
    )
    combined_key = pd.MultiIndex.from_arrays(
        [date_level, industry_group_key.values], names=["__date__", "__ind__"]
    )
    industry_median = div.groupby(combined_key).transform("median")
    date_median = div.groupby(date_level).transform("median")
    baseline = pd.Series(
        np.where(has_industry.values, industry_median.values, date_median.values),
        index=panel.index,
    )
    score = div - baseline
    score = score.where(valid)
    return score</pre></div></details><details class="la-sig"><summary><code>earnings_yield</code><span class="la-tag">value</span><span class="gist">Earnings yield = 1/pe (high = cheap = long)</span></summary><div class="la-sig-body"><p>Earnings yield = 1/pe (high = cheap = long).</p><div class="la-meta"><span><b>Category</b> value</span><span><b>Direction</b> Long the high scores</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    pe = panel["pe"]
    return (1.0 / pe).where(pe &gt; 0)</pre></div></details><details class="la-sig"><summary><code>equity_duration_rate_trend_interaction</code><span class="la-tag">value</span><span class="gist">Fundamental equity-duration proxy (high PB, low dividend yield, low FCF yield) interacted with the trailing…</span></summary><div class="la-sig-body"><p>Fundamental equity-duration proxy (high PB, low dividend yield, low FCF yield) interacted with the trailing 12m change in the 10y Treasury yield. Long-duration growth equity is favored when yields have been falling and disfavored when yields have been rising, conditioning a value/duration tilt on the macro rate trend.</p><div class="la-meta"><span><b>Category</b> value</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    pb = panel['pb']
    divyield = panel['divyield']
    fcf = panel['fcf']
    marketcap = panel['marketcap']
    macro_chg = panel['macro_dgs10_chg12m']
    valid_mc = marketcap &gt; 0
    cfy = pd.Series(np.nan, index=panel.index, dtype=float)
    cfy[valid_mc] = fcf[valid_mc] / marketcap[valid_mc]
    valid = (
        valid_mc
        &amp; pb.notna()
        &amp; divyield.notna()
        &amp; fcf.notna()
        &amp; cfy.notna()
        &amp; macro_chg.notna()
    )
    pb_v = pb.where(valid)
    divyield_v = divyield.where(valid)
    cfy_v = cfy.where(valid)
    rank_pb = pb_v.groupby(level='date').rank(pct=True)
    rank_neg_dy = (-divyield_v).groupby(level='date').rank(pct=True)
    rank_neg_cfy = (-cfy_v).groupby(level='date').rank(pct=True)
    dur = 0.4 * rank_pb + 0.3 * rank_neg_dy + 0.3 * rank_neg_cfy
    dur_mean = dur.groupby(level='date').transform('mean')
    dur_c = dur - dur_mean
    signal = -dur_c * macro_chg
    signal = signal.where(valid)
    return signal</pre></div></details><details class="la-sig"><summary><code>retained_vs_contributed_capital_value</code><span class="la-tag">value</span><span class="gist">Value signal splitting book equity into retained earnings (internally generated) vs contributed capital…</span></summary><div class="la-sig-body"><p>Value signal splitting book equity into retained earnings (internally generated) vs contributed capital (externally raised): (2*retearn - equity) / marketcap, winsorized at 1/99 pct cross-sectionally per date. Higher score (more retained-earnings-driven equity relative to market cap) predicts higher returns.</p><div class="la-meta"><span><b>Category</b> value</span><span><b>Direction</b> Long the high scores</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    equity = panel['equity']
    retearn = panel['retearn']
    marketcap = panel['marketcap']
    valid = (marketcap &gt; 0) &amp; equity.notna() &amp; retearn.notna()
    raw = (2 * retearn - equity) / marketcap
    raw = raw.where(valid, np.nan)
    def _winsorize(s: pd.Series) -&gt; pd.Series:
        if s.notna().sum() == 0:
            return s
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    result = raw.groupby(level='date').transform(_winsorize)
    return result</pre></div></details><h4 class="la-group">Market-timing signals (49)</h4><p class="la-group-note">Scored once per month for the market as a whole. The oriented score sets the desired equity exposure rather than picking names.</p><details class="la-sig"><summary><code>aggregate_equity_issuance_market_timing</code><span class="la-tag">investment</span><span class="gist">Aggregate equity issuance market timing signal (Baker-Wurgler style): cross-sectional sum of positive net…</span></summary><div class="la-sig-body"><p>Aggregate equity issuance market timing signal (Baker-Wurgler style): cross-sectional sum of positive net equity issuance (ncfcommon) scaled by aggregate market cap, smoothed with a 12-month rolling (past-only) mean. High aggregate issuance signals overvaluation and predicts low subsequent market returns, so exposure should be reduced (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    mcap = panel['marketcap']
    ncf = panel['ncfcommon']
    valid = mcap.notna() &amp; (mcap &gt; 0)
    ncf_pos = ncf.where(ncf.notna() &amp; (ncf &gt; 0), 0.0)
    ncf_pos = ncf_pos.where(valid)
    mcap_valid = mcap.where(valid)
    num = ncf_pos.groupby(level='date').sum(min_count=1)
    den = mcap_valid.groupby(level='date').sum(min_count=1)
    count = valid.groupby(level='date').sum()
    iss = num / den.where(den &gt; 0)
    iss = iss.where(count &gt;= 10)
    iss = iss.sort_index()
    smooth = iss.rolling(window=12, min_periods=6).mean()
    return smooth</pre></div></details><details class="la-sig"><summary><code>aggregate_inventory_overhang_timing</code><span class="la-tag">investment</span><span class="gist">Cross-sectional median inventory/revenue ratio aggregated over time, normalized by its own trailing 36-month…</span></summary><div class="la-sig-body"><p>Cross-sectional median inventory/revenue ratio aggregated over time, normalized by its own trailing 36-month mean/std to form an inventory overhang z-score, amplified when trailing 12m industrial production growth is negative (slowing IP cycle). High overhang during slowdowns signals margin compression and weaker forward equity returns.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">from __future__ import annotations
import pandas as pd
import numpy as np
from llm_alpha.signals.base import BaseSignal
class AggregateInventoryOverhangTiming(BaseSignal):
    name = "aggregate_inventory_overhang_timing"
    category = "other"
    strategy_type = "macro_timing"
    expected_sign = -1
    description = (
        "Market-timing signal based on the cross-sectional median change in "
        "inventory-to-sales ratio (12m delta), expanding past-only z-scored, "
        "adjusted for the trend in industrial production growth. Rising "
        "inventory overhang combined with decelerating industrial production "
        "signals a destocking phase historically associated with poor "
        "equity returns."
    )
    data_sources = ('fundamental', 'macro')
    def compute(self, panel) -&gt; pd.Series:
        df = panel[['inventory', 'revenue']].copy()
        valid = (df['inventory'] &gt; 0) &amp; (df['revenue'] &gt; 0)
        x = pd.Series(np.nan, index=df.index)
        x[valid] = df.loc[valid, 'inventory'] / df.loc[valid, 'revenue']
        x_shift = x.groupby(level='ticker').shift(12)
        dx = x - x_shift
        valid_dx = dx.dropna()
        counts = valid_dx.groupby(level='date').size()
        agg = valid_dx.groupby(level='date').median()
        agg = agg.where(counts &gt;= 100)
        agg = agg.sort_index()
        # expanding past-only z-score of agg
        agg_mean = agg.expanding().mean().shift(1)
        agg_std = agg.expanding().std().shift(1)
        agg_z = (agg - agg_mean) / agg_std
        agg_z = agg_z.where(agg_std &gt; 0)
        # macro indpro chg12m at date level (broadcast per date)
        macro_indpro = panel['macro_indpro_chg12m'].groupby(level='date').mean()
        macro_indpro = macro_indpro.sort_index()
        macro_mean = macro_indpro.expanding().mean().shift(1)
        macro_std = macro_indpro.expanding().std().shift(1)
        macro_z = (macro_indpro - macro_mean) / macro_std
        macro_z = macro_z.where(macro_std &gt; 0)
        # align indices
        all_dates = agg_z.index.union(macro_z.index)
        agg_z = agg_z.reindex(all_dates)
        macro_z = macro_z.reindex(all_dates)
        state = agg_z - 0.5 * macro_z
        return state</pre></div></details><details class="la-sig"><summary><code>aggregate_inventory_sales_cycle_timing</code><span class="la-tag">investment</span><span class="gist">Aggregate (bottom-up, ex-Financials) inventory/sales ratio built from the equity panel's fundamentals</span></summary><div class="la-sig-body"><p>Aggregate (bottom-up, ex-Financials) inventory/sales ratio built from the equity panel's fundamentals. Rising aggregate inventory intensity (log ratio minus its 12-month lag) signals involuntary inventory buildup, which precedes production cuts and weak market returns, hence a negative expected sign for market timing.</p><div class="la-meta"><span><b>Category</b> investment</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['inventory', 'revenue', 'sector']].copy()
    mask = (
        df['inventory'].notna()
        &amp; df['revenue'].notna()
        &amp; (df['inventory'] &gt; 0)
        &amp; (df['revenue'] &gt; 0)
        &amp; (df['sector'] != 'Financials')
    )
    df = df.loc[mask]
    grouped = df.groupby(level='date').agg(
        inv_sum=('inventory', 'sum'),
        rev_sum=('revenue', 'sum'),
    )
    ratio = grouped['inv_sum'] / grouped['rev_sum']
    ratio = ratio.where(grouped['rev_sum'] &gt; 0)
    s = np.log(ratio)
    s = s.sort_index()
    raw = s - s.shift(12)
    return raw</pre></div></details><details class="la-sig"><summary><code>credit_spread_timing</code><span class="la-tag">low_risk</span><span class="gist">Time the market on credit conditions: lean in when the Baa-Treasury spread is tight, out when it is wide</span></summary><div class="la-sig-body"><p>Time the market on credit conditions: lean in when the Baa-Treasury spread is tight, out when it is wide.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    return panel["macro_baa_spread"].groupby(level="date").first()</pre></div></details><details class="la-sig"><summary><code>cross_sectional_beta_dispersion_timing</code><span class="la-tag">low_risk</span><span class="gist">Market-timing signal based on cross-sectional dispersion of market betas among the most liquid names</span></summary><div class="la-sig-body"><p>Market-timing signal based on cross-sectional dispersion of market betas among the most liquid names. High dispersion (relative to mean beta) indicates a fragile, disagreement-laden market prone to weak/crash-risk future returns; compressed dispersion follows panic comovement and precedes recoveries. expected_sign=-1: high dispersion -&gt; low exposure.</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['dvol_21', 'beta_252']].copy()
    df = df.dropna(subset=['dvol_21', 'beta_252'])
    df = df[df['dvol_21'] &gt; 0]
    def per_date(g):
        g = g.sort_values('dvol_21', ascending=False)
        g = g.head(1500)
        n = len(g)
        if n &lt; 30:
            return np.nan
        beta = g['beta_252']
        lo = beta.quantile(0.01)
        hi = beta.quantile(0.99)
        beta_w = beta.clip(lower=lo, upper=hi)
        disp = beta_w.std()
        mean_beta = beta_w.mean()
        if not (mean_beta &gt; 0):
            return np.nan
        return disp / mean_beta
    result = df.groupby(level='date').apply(per_date)
    result = result.astype(float)
    result.index.name = 'date'
    return result</pre></div></details><details class="la-sig"><summary><code>cross_sectional_return_dispersion_timing</code><span class="la-tag">low_risk</span><span class="gist">Cross-sectional dispersion of trailing 1-month returns, scaled by its own trailing 36-month average, smoothed…</span></summary><div class="la-sig-body"><p>Cross-sectional dispersion of trailing 1-month returns, scaled by its own trailing 36-month average, smoothed over 3 months, used as a market-timing signal. High dispersion signals elevated uncertainty/regime instability and predicts weak forward market returns (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> low_risk</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = ['ret_1m', 'ret_12m', 'marketcap', 'dvol_21']
    df = panel[cols].copy()
    # replace inf with nan and drop invalid rows
    df = df.replace([np.inf, -np.inf], np.nan)
    df = df.dropna(subset=cols)
    min_n = 30
    def winsorize(s: pd.Series) -&gt; pd.Series:
        lo = s.quantile(0.01)
        hi = s.quantile(0.99)
        return s.clip(lower=lo, upper=hi)
    def disp_for_date(g: pd.DataFrame):
        if len(g) &lt; min_n:
            return np.nan
        w = winsorize(g['ret_1m'])
        return w.std()
    disp = df.groupby(level='date').apply(disp_for_date)
    disp.index = disp.index.get_level_values(0) if isinstance(disp.index, pd.MultiIndex) else disp.index
    disp = disp.sort_index()
    # trailing 36-month rolling std of ret_1m dispersion (past-only, shifted by 1)
    trailing_scale = disp.shift(1).rolling(window=36, min_periods=12).mean()
    scaled_disp = disp / trailing_scale.replace(0, np.nan)
    # rolling 3-month mean, past-only
    signal = scaled_disp.rolling(window=3, min_periods=1).mean()
    signal.name = "cross_sectional_return_dispersion_timing"
    return signal</pre></div></details><details class="la-sig"><summary><code>halloween_volatility_scaled_timing</code><span class="la-tag">momentum</span><span class="gist">Halloween/'Sell in May' seasonal timer scaled by inverse VIX regime (vs expanding past median) and…</span></summary><div class="la-sig-body"><p>Halloween/'Sell in May' seasonal timer scaled by inverse VIX regime (vs expanding past median) and downweighted in NBER recessions or rising-unemployment regimes.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    vix = panel['macro_vix'].groupby(level='date').mean()
    usrec = panel['macro_usrec'].groupby(level='date').mean()
    unrate_chg = panel['macro_unrate_chg12m'].groupby(level='date').mean()
    dates = vix.index.sort_values()
    vix = vix.reindex(dates)
    usrec = usrec.reindex(dates)
    unrate_chg = unrate_chg.reindex(dates)
    # forward-fill at most 1 month for sporadic gaps
    vix = vix.ffill(limit=1)
    usrec = usrec.ffill(limit=1)
    unrate_chg = unrate_chg.ffill(limit=1)
    # fill remaining NaNs in usrec/unrate_chg with 0
    usrec = usrec.fillna(0.0)
    unrate_chg = unrate_chg.fillna(0.0)
    # seasonal component
    months = pd.Series(dates.month, index=dates)
    seas = months.apply(lambda m: 1.0 if m in {11, 12, 1, 2, 3, 4} else -0.5)
    # expanding past-only median of vix, min 24 obs
    vix_shifted = vix.shift(1)
    vixmed = vix_shifted.expanding(min_periods=24).median()
    count = vix_shifted.expanding(min_periods=1).count()
    ratio = vix / vixmed
    vscale = 1.0 / (1.0 + np.maximum(0.0, ratio - 1.0))
    vscale = vscale.where(count &gt;= 24, 1.0)
    vscale = vscale.where(vixmed.notna() | (count &lt; 24), 1.0)
    # recession adjustment
    unrate_clip = unrate_chg.clip(lower=0.0, upper=3.0)
    rec_adj = 1.0 - 0.5 * usrec - 0.25 * unrate_clip
    rec_adj = rec_adj.clip(lower=0.0, upper=1.0)
    exposure = seas * vscale * rec_adj
    # NaN where vix itself is fully missing after fallback
    exposure = exposure.where(vix.notna(), np.nan)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>market_breadth_high_ratio_timing</code><span class="la-tag">momentum</span><span class="gist">Market breadth signal based on the cross-sectional average and fraction of stocks trading near their 52-week…</span></summary><div class="la-sig-body"><p>Market breadth signal based on the cross-sectional average and fraction of stocks trading near their 52-week high (high_ratio). Broadening breadth predicts positive forward market returns; narrowing breadth (deterioration relative to its own trailing 12-month average) predicts weak forward returns.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    hr = panel['high_ratio']
    valid_count = hr.groupby(level='date').apply(lambda s: s.notna().sum())
    brd = hr.groupby(level='date').mean()
    pct_hi = hr.groupby(level='date').apply(lambda s: (s &gt; 0.95).sum() / max(s.notna().sum(), 1))
    # invalidate months with insufficient valid tickers
    insufficient = valid_count &lt; self.MIN_VALID
    brd = brd.where(~insufficient, np.nan)
    pct_hi = pct_hi.where(~insufficient, np.nan)
    brd = brd.sort_index()
    pct_hi = pct_hi.sort_index()
    # rolling 12-month mean, past-only (shifted 1)
    brd_roll_mean = brd.rolling(window=12, min_periods=12).mean().shift(1)
    pct_hi_roll_mean = pct_hi.rolling(window=12, min_periods=12).mean().shift(1)
    brd_dev = brd - brd_roll_mean
    pct_hi_dev = pct_hi - pct_hi_roll_mean
    def zscore_past(s: pd.Series) -&gt; pd.Series:
        mean_past = s.expanding(min_periods=12).mean().shift(1)
        std_past = s.expanding(min_periods=12).std().shift(1)
        z = (s - mean_past) / std_past.replace(0, np.nan)
        return z
    z_brd = zscore_past(brd_dev)
    z_pct_hi = zscore_past(pct_hi_dev)
    raw = 0.5 * z_brd + 0.5 * z_pct_hi
    # require at least MIN_HISTORY prior months before emitting non-NaN exposure
    n_prior = pd.Series(np.arange(len(raw)), index=raw.index)
    raw = raw.where(n_prior &gt;= self.MIN_HISTORY, np.nan)
    exposure = raw.fillna(0.0)
    return exposure</pre></div></details><details class="la-sig"><summary><code>market_trend_timing</code><span class="la-tag">momentum</span><span class="gist">Time-series trend: hold the market when its trailing 12m return is strong, lighten when weak</span></summary><div class="la-sig-body"><p>Time-series trend: hold the market when its trailing 12m return is strong, lighten when weak.</p><div class="la-meta"><span><b>Category</b> momentum</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel: pd.DataFrame) -&gt; pd.Series:
    # market trend proxy = cross-sectional mean of trailing 12m return, per date
    return panel["ret_12m"].groupby(level="date").mean()</pre></div></details><details class="la-sig"><summary><code>aggregate_illiquidity_shock_timing</code><span class="la-tag">other</span><span class="gist">Market-timing signal based on Amihud-style aggregate illiquidity shocks: computes cross-sectional median of…</span></summary><div class="la-sig-body"><p>Market-timing signal based on Amihud-style aggregate illiquidity shocks: computes cross-sectional median of |ret_1m|/dvol_21 across liquid stocks, log-transforms it, and measures its deviation from a trailing 12-month average (min 6 obs). Spikes in aggregate illiquidity relative to trend signal funding/liquidity stress and precede high subsequent market returns.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['ret_1m', 'dvol_21', 'marketcap', 'closeunadj']].copy()
    valid = (
        df['dvol_21'].notna() &amp; (df['dvol_21'] &gt; 0) &amp;
        df['ret_1m'].notna() &amp;
        df['marketcap'].notna() &amp;
        df['closeunadj'].notna()
    )
    df = df[valid]
    illiq = df['ret_1m'].abs() / df['dvol_21']
    illiq = illiq.replace([np.inf, -np.inf], np.nan).dropna()
    agg = illiq.groupby(level='date').median()
    agg = np.log(agg)
    agg = agg.sort_index()
    trailing_mean = agg.rolling(12, min_periods=6).mean().shift(1)
    exposure = agg - trailing_mean
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>aggregate_rollover_risk_funding_shock_timing</code><span class="la-tag">other</span><span class="gist">Aggregate corporate debt rollover fragility (median debtc/debt across tickers) deviated from its 36m rolling…</span></summary><div class="la-sig-body"><p>Aggregate corporate debt rollover fragility (median debtc/debt across tickers) deviated from its 36m rolling mean, interacted with a 12m funding shock (fed funds change + Baa spread change), to time market exposure. Short-maturity debt structures amplify sensitivity to rate/credit shocks, so higher exposure_score predicts lower forward returns.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    debt = panel['debt']
    debtc = panel['debtc']
    valid = (debt &gt; 0) &amp; debtc.notna() &amp; debt.notna()
    ratio = pd.Series(np.nan, index=panel.index)
    ratio[valid] = debtc[valid] / debt[valid]
    roll = ratio.groupby(level='date').median()
    roll = roll.sort_index()
    roll_mean = roll.rolling(window=36, min_periods=36).mean()
    roll_dev = roll - roll_mean.shift(1)
    macro_ff = panel['macro_fedfunds_chg12m'].groupby(level='date').first()
    macro_baa = panel['macro_baa_spread_chg12m'].groupby(level='date').first()
    macro_ff, macro_baa = macro_ff.reindex(roll.index), macro_baa.reindex(roll.index)
    shock = macro_ff + macro_baa
    exposure_score = roll_dev * shock
    return exposure_score</pre></div></details><details class="la-sig"><summary><code>aggregate_turnover_sentiment_timing</code><span class="la-tag">other</span><span class="gist">Baker-Stein style aggregate turnover sentiment timing: high market-wide turnover (dollar volume / market cap)…</span></summary><div class="la-sig-body"><p>Baker-Stein style aggregate turnover sentiment timing: high market-wide turnover (dollar volume / market cap) relative to its trailing multi-year norm signals sentiment-driven overpricing under short-sale constraints, forecasting LOW subsequent market returns. Exposure is set inverse to the deviation of log turnover from its trailing 36m mean.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dvol = panel['dvol_21']
    mcap = panel['marketcap']
    valid = dvol.notna() &amp; mcap.notna() &amp; (dvol &gt; 0) &amp; (mcap &gt; 0)
    dollar_vol_monthly = (dvol * 21).where(valid)
    mcap_valid = mcap.where(valid)
    agg_dvol = dollar_vol_monthly.groupby(level='date').sum(min_count=1)
    agg_mcap = mcap_valid.groupby(level='date').sum(min_count=1)
    turn = agg_dvol / agg_mcap
    turn = turn.where(agg_mcap &gt; 0)
    x = np.log(turn.where(turn &gt; 0))
    x = x.sort_index()
    rolling_mean = x.shift(1).rolling(window=36, min_periods=24).mean()
    signal = x - rolling_mean
    exposure = -signal
    return exposure</pre></div></details><details class="la-sig"><summary><code>average_systematic_comovement_timing</code><span class="la-tag">other</span><span class="gist">Pollet and Wilson (2010)-style average comovement timer: cross-sectional mean absolute trailing correlation…</span></summary><div class="la-sig-body"><p>Pollet and Wilson (2010)-style average comovement timer: cross-sectional mean absolute trailing correlation of stocks to VIX, credit, and rates macro shocks. High average comovement signals a high quantity of undiversifiable systematic risk, which should be compensated by higher subsequent market returns.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = ['corr_vix_252', 'corr_credit_252', 'corr_rates_252']
    comps = []
    for col in cols:
        if col not in panel.columns:
            continue
        s = panel[col].dropna()
        abs_s = s.abs()
        grp = abs_s.groupby(level='date')
        mean_c = grp.mean()
        count_c = grp.count()
        mean_c = mean_c.where(count_c &gt;= self.MIN_STOCKS, np.nan)
        comps.append(mean_c)
    if not comps:
        dates = panel.index.get_level_values('date').unique()
        return pd.Series(np.nan, index=dates)
    df = pd.concat(comps, axis=1)
    comove = df.mean(axis=1, skipna=True)
    exposure = comove
    return exposure</pre></div></details><details class="la-sig"><summary><code>consumer_sentiment_credit_divergence_timing</code><span class="la-tag">other</span><span class="gist">Times equity exposure using the divergence between consumer sentiment (U</span></summary><div class="la-sig-body"><p>Times equity exposure using the divergence between consumer sentiment (U. Michigan) and corporate credit spreads (Baa - 10y): gloomy consumers combined with tight/stable credit spreads implies excess equity pessimism and predicts high forward returns, while euphoric sentiment with widening spreads signals hidden balance-sheet stress and predicts poor returns. A momentum overlay on credit spread and unemployment 12m changes refines the timing signal further.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    umcsent = panel['macro_umcsent'].groupby(level='date').mean().sort_index()
    baa_spread = panel['macro_baa_spread'].groupby(level='date').mean().sort_index()
    baa_chg12m = panel['macro_baa_spread_chg12m'].groupby(level='date').mean().sort_index()
    unrate_chg12m = panel['macro_unrate_chg12m'].groupby(level='date').mean().sort_index()
    umcsent = umcsent.ffill()
    baa_spread = baa_spread.ffill()
    baa_chg12m = baa_chg12m.ffill()
    unrate_chg12m = unrate_chg12m.ffill()
    s_sent = _zscore_pastonly(umcsent)
    s_credit = _zscore_pastonly(baa_spread)
    s_baa_chg = _zscore_pastonly(baa_chg12m)
    s_unrate_chg = _zscore_pastonly(unrate_chg12m)
    div = (-s_sent) - (-s_credit)
    div2 = div - 0.5 * s_baa_chg - 0.5 * s_unrate_chg
    exposure = div2.dropna()
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>credit_equity_divergence_timing</code><span class="la-tag">other</span><span class="gist">Times equity market exposure using the divergence between credit-spread improvement and equity-market momentum</span></summary><div class="la-sig-body"><p>Times equity market exposure using the divergence between credit-spread improvement and equity-market momentum. When credit spreads have been tightening while equities have lagged, exposure rises; the reverse divergence (spreads widening into equity strength) lowers exposure.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    macro_spread = panel['macro_baa_spread'].groupby(level='date').mean()
    ret_3m = panel['ret_3m']
    # market return proxy: cross-sectional mean of ret_3m per date, dropping NaN
    mkt = ret_3m.dropna().groupby(level='date').mean()
    # credit improvement: negative of 3-month change in spread
    macro_spread = macro_spread.sort_index()
    credit_imp = -(macro_spread - macro_spread.shift(3))
    credit_imp = credit_imp.dropna()
    # align dates
    common_dates = credit_imp.index.intersection(mkt.index)
    credit_imp = credit_imp.loc[common_dates].sort_index()
    mkt = mkt.loc[common_dates].sort_index()
    min_obs = 12
    def expanding_z(s: pd.Series) -&gt; pd.Series:
        mean = s.expanding().mean()
        std = s.expanding().std()
        count = s.expanding().count()
        z = (s - mean) / std
        z = z.where((count &gt;= min_obs) &amp; (std &gt; 0))
        return z
    z1 = expanding_z(credit_imp)
    z2 = expanding_z(mkt)
    exposure = z1 - z2
    exposure = exposure.reindex(common_dates)
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>credit_vol_complacency_divergence_timing</code><span class="la-tag">other</span><span class="gist">Cross-asset divergence between credit spread stress and equity implied volatility complacency</span></summary><div class="la-sig-body"><p>Cross-asset divergence between credit spread stress and equity implied volatility complacency. When Baa-Treasury credit spreads are historically wide relative to VIX, credit markets are pricing stress that equities have not yet discounted, predicting poor subsequent market returns; the converse signals fear-driven overreaction and a buying opportunity.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    baa = panel['macro_baa_spread'].groupby(level='date').first().sort_index()
    vix = panel['macro_vix'].groupby(level='date').first().sort_index()
    baa_chg = panel['macro_baa_spread_chg12m'].groupby(level='date').first().sort_index()
    vix_chg = panel['macro_vix_chg12m'].groupby(level='date').first().sort_index()
    z_credit = self._past_z(baa)
    z_vix = self._past_z(vix)
    d_credit = self._past_z(baa_chg)
    d_vix = self._past_z(vix_chg)
    raw = 0.5 * (z_credit - z_vix) + 0.5 * (d_credit - d_vix)
    raw.name = self.name
    return raw</pre></div></details><details class="la-sig"><summary><code>dollar_liquidity_cycle_timing</code><span class="la-tag">other</span><span class="gist">Times market exposure using the 12-month relative trend in the broad USD index, amplified by the…</span></summary><div class="la-sig-body"><p>Times market exposure using the 12-month relative trend in the broad USD index, amplified by the cross-sectional average absolute USD beta relative to its own expanding history. A strengthening dollar tightens global financial conditions and should be followed by weaker equity risk premia, especially when the equity universe is currently more FX-sensitive than usual; the effect is inverted (i.e. dollar depreciation with high FX-sensitivity favors risk-taking).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    # Date-level average absolute USD beta (ignore NaNs), requiring a minimum count
    beta_usd = panel['beta_usd_252']
    grp = beta_usd.abs().groupby(level='date')
    A_t = grp.mean()
    counts = grp.count()
    min_count = 5
    A_t = A_t.where(counts &gt;= min_count)
    A_t = A_t.sort_index()
    # Expanding mean, past-only (shifted by 1 to avoid look-ahead), min 24 obs
    expanding_mean = A_t.expanding(min_periods=24).mean().shift(1)
    A_hat = A_t / expanding_mean.where(expanding_mean &gt; 0)
    A_hat_clipped = A_hat.clip(lower=0.5, upper=2.0)
    # Macro USD series at date level (broadcast, take first/mean per date)
    macro_usd = panel['macro_usd'].groupby(level='date').mean().sort_index()
    macro_usd_chg12m = panel['macro_usd_chg12m'].groupby(level='date').mean().sort_index()
    macro_usd_lag12 = macro_usd.shift(12)
    trend = macro_usd_chg12m / macro_usd_lag12.where(macro_usd_lag12 &gt; 0)
    exposure_raw = -trend * A_hat_clipped
    exposure_raw = exposure_raw.reindex(A_t.index)
    return exposure_raw</pre></div></details><details class="la-sig"><summary><code>equity_fear_vs_credit_stress_divergence_timing</code><span class="la-tag">other</span><span class="gist">Cross-asset divergence between equity fear (VIX) and credit stress (Baa spread): z-scores each series over a…</span></summary><div class="la-sig-body"><p>Cross-asset divergence between equity fear (VIX) and credit stress (Baa spread): z-scores each series over a trailing 60m window (min 36m history) and takes their difference. High values mean equity implied volatility is elevated relative to what credit spreads imply, signalling unconfirmed panic/hedging pressure that should mean-revert, warranting higher market exposure; low values mean credit stress is not matched by equity vol, signalling unpriced deterioration and lower exposure.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    vix = panel["macro_vix"].groupby(level="date").mean().sort_index()
    baa = panel["macro_baa_spread"].groupby(level="date").mean().sort_index()
    vix = vix.ffill()
    baa = baa.ffill()
    min_p = 36
    window = 60
    vix_mean = vix.rolling(window=window, min_periods=min_p).mean()
    vix_std = vix.rolling(window=window, min_periods=min_p).std()
    baa_mean = baa.rolling(window=window, min_periods=min_p).mean()
    baa_std = baa.rolling(window=window, min_periods=min_p).std()
    z_v = (vix - vix_mean) / vix_std
    z_c = (baa - baa_mean) / baa_std
    z_v = z_v.where(vix_std &gt; 0, np.nan)
    z_c = z_c.where(baa_std &gt; 0, np.nan)
    signal = z_v - z_c
    signal = signal.where(vix.notna() &amp; baa.notna())
    return signal</pre></div></details><details class="la-sig"><summary><code>equity_rate_correlation_regime_timing</code><span class="la-tag">other</span><span class="gist">Times the market using the date-level average stock correlation to rate changes (corr_rates_252)</span></summary><div class="la-sig-body"><p>Times the market using the date-level average stock correlation to rate changes (corr_rates_252). Positive average correlation signals a growth-driven regime where bonds hedge equities and risk premia are earned; negative correlation signals an inflation/discount-rate regime with weaker forward equity returns. A small penalty is applied when 10y breakeven inflation expectations are elevated (past-only z-score).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    corr = panel['corr_rates_252'].dropna()
    m = corr.groupby(level='date').mean()
    # ensure full date index, forward-fill dates with no valid observations
    all_dates = panel.index.get_level_values('date').unique().sort_values()
    m = m.reindex(all_dates).ffill()
    exposure = m.rolling(3, min_periods=1).mean()
    # inflation penalty using macro_breakeven10, past-only z-score
    if 'macro_breakeven10' in panel.columns:
        be = panel['macro_breakeven10'].groupby(level='date').mean()
        be = be.reindex(all_dates).ffill()
        be_mean = be.expanding(min_periods=1).mean().shift(1)
        be_std = be.expanding(min_periods=1).std().shift(1)
        z = (be - be_mean) / be_std.where(be_std &gt; 0)
        z = z.fillna(0.0)
        exposure = exposure - 0.25 * z
    exposure.name = self.name
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>equity_yield_vs_real_rate_gap_timing</code><span class="la-tag">other</span><span class="gist">Market-timing signal based on the gap between the cross-sectional median earnings yield of large-cap stocks…</span></summary><div class="la-sig-body"><p>Market-timing signal based on the gap between the cross-sectional median earnings yield of large-cap stocks and the real 10y rate (nominal 10y yield minus 10y breakeven inflation). A wide gap (equities cheap relative to real rates) predicts elevated forward market returns; the level is combined with a deviation from its trailing 36-month mean.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['pe', 'marketcap', 'macro_dgs10', 'macro_breakeven10']].copy()
    # keep valid pe and marketcap
    valid = df['pe'].notna() &amp; (df['pe'] &gt; 0) &amp; df['marketcap'].notna() &amp; (df['marketcap'] &gt; 0)
    df = df[valid]
    def median_top_half_ey(group):
        g = group.dropna(subset=['marketcap'])
        if len(g) == 0:
            return np.nan
        median_mc = g['marketcap'].median()
        top_half = g[g['marketcap'] &gt;= median_mc]
        ey = 100.0 / top_half['pe']
        if len(ey) == 0:
            return np.nan
        return ey.median()
    ey_by_date = df.groupby(level='date').apply(median_top_half_ey)
    ey_by_date = ey_by_date.sort_index()
    # macro series at date level (broadcast, take first per date)
    dgs10 = panel['macro_dgs10'].groupby(level='date').first().sort_index()
    breakeven10 = panel['macro_breakeven10'].groupby(level='date').first().sort_index()
    # forward-fill from prior month-end (no future values)
    dgs10 = dgs10.ffill()
    breakeven10 = breakeven10.ffill()
    real_y = dgs10 - breakeven10
    # align all series on the common date index
    all_dates = ey_by_date.index.union(real_y.index).sort_values()
    ey_by_date = ey_by_date.reindex(all_dates)
    real_y = real_y.reindex(all_dates)
    gap = ey_by_date - real_y
    # if macro missing (dgs10 or breakeven10 not available even after ffill), gap is NaN -&gt; skip
    gap = gap.where(real_y.notna() &amp; ey_by_date.notna())
    # trailing 36-month mean of gap, past-only (exclude current observation)
    trailing_mean = gap.shift(1).rolling(window=36, min_periods=36).mean()
    trailing_count = gap.shift(1).rolling(window=36, min_periods=1).count()
    signal = gap.copy()
    has_enough = trailing_count &gt;= 36
    adj = gap - trailing_mean
    signal = gap + 0.5 * adj.where(has_enough, 0.0)
    signal = signal.where(gap.notna())
    signal.name = self.name
    return signal</pre></div></details><details class="la-sig"><summary><code>household_sentiment_fundamental_divergence_timing</code><span class="la-tag">other</span><span class="gist">Contrarian macro timer using the divergence between consumer sentiment (UMich) and hard real-economy…</span></summary><div class="la-sig-body"><p>Contrarian macro timer using the divergence between consumer sentiment (UMich) and hard real-economy fundamentals (industrial production growth and unemployment dynamics). When sentiment is unusually pessimistic relative to fundamentals, risk premia are elevated and expected returns are high; exposure is negative of this expanding-z-score gap.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    senti = panel['macro_umcsent'].groupby(level='date').mean()
    indpro_chg = panel['macro_indpro_chg12m'].groupby(level='date').mean()
    unrate_chg = panel['macro_unrate_chg12m'].groupby(level='date').mean()
    senti = senti.sort_index().ffill()
    indpro_chg = indpro_chg.sort_index().ffill()
    unrate_chg = unrate_chg.sort_index().ffill()
    first_valid = max(
        senti.first_valid_index(),
        indpro_chg.first_valid_index(),
        unrate_chg.first_valid_index(),
    )
    senti = senti.loc[first_valid:]
    indpro_chg = indpro_chg.loc[first_valid:]
    unrate_chg = unrate_chg.loc[first_valid:]
    z_senti = self._expanding_z(senti)
    z_indpro = self._expanding_z(indpro_chg)
    z_unrate_inv = self._expanding_z(-unrate_chg)
    fund = 0.5 * (z_indpro + z_unrate_inv)
    gap = z_senti - fund
    exposure = -gap
    exposure = exposure.reindex(senti.index)
    exposure = exposure.fillna(0.0)
    return exposure</pre></div></details><details class="la-sig"><summary><code>labor_cycle_turning_point_timing</code><span class="la-tag">other</span><span class="gist">Market timing signal based on labor-market inflection points: combines the unemployment-rate gap from its…</span></summary><div class="la-sig-body"><p>Market timing signal based on labor-market inflection points: combines the unemployment-rate gap from its trailing 12-month trough (Sahm-rule style) with confirmation from industrial production growth and consumer sentiment growth. A rising unemployment gap off its trough signals elevated drawdown risk (negative exposure), while unemployment at its trough with expanding industrial production and improving sentiment signals low drawdown risk (positive exposure).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    def z(x: pd.Series) -&gt; pd.Series:
        mean = x.expanding(24).mean()
        std = x.expanding(24).std()
        out = (x - mean) / std
        out = out.where(std &gt; 0)
        return out
    u = panel['macro_unrate'].groupby(level='date').first()
    u = u.sort_index()
    gap = u - u.rolling(12).min()
    ip = panel['macro_indpro'].groupby(level='date').first()
    ip = ip.sort_index()
    ipg = ip / ip.shift(12) - 1.0
    sent = panel['macro_umcsent'].groupby(level='date').first()
    sent = sent.sort_index()
    sg = sent / sent.shift(12) - 1.0
    z_gap = z(gap)
    z_ipg = z(ipg)
    z_sg = z(sg)
    exposure = -1.0 * z_gap + 0.5 * z_ipg + 0.25 * z_sg
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>macro_momentum_diffusion_timing</code><span class="la-tag">other</span><span class="gist">Date-level macro breadth/diffusion index counting how many independent macro dimensions (industrial…</span></summary><div class="la-sig-body"><p>Date-level macro breadth/diffusion index counting how many independent macro dimensions (industrial production, unemployment, credit spread, consumer sentiment, VIX, term spread) are improving over the trailing 12 months. Exposure is raised when macro breadth broadly improves and cut when several dimensions deteriorate together.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    components = [
        ('macro_indpro_chg12m', 1),
        ('macro_unrate_chg12m', -1),
        ('macro_baa_spread_chg12m', -1),
        ('macro_umcsent_chg12m', 1),
        ('macro_vix_chg12m', -1),
        ('macro_t10y2y_chg12m', 1),
    ]
    date_level_series = {}
    for col, _ in components:
        s = panel[col].groupby(level='date').mean()
        date_level_series[col] = s
    dates = None
    for s in date_level_series.values():
        if dates is None:
            dates = s.index
        else:
            dates = dates.union(s.index)
    dates = dates.sort_values()
    signed_sum = pd.Series(0.0, index=dates)
    count = pd.Series(0, index=dates)
    for col, weight in components:
        s = date_level_series[col].reindex(dates)
        sign_s = np.sign(s)
        valid = sign_s.notna()
        signed_sum = signed_sum.add(sign_s.where(valid, 0.0) * weight, fill_value=0.0)
        count = count.add(valid.astype(int), fill_value=0)
    exposure = signed_sum / count.replace(0, np.nan)
    exposure = exposure.where(count &gt; 0, np.nan)
    return exposure</pre></div></details><details class="la-sig"><summary><code>macro_pessimism_contrarian_market_timing</code><span class="la-tag">other</span><span class="gist">Contrarian macro timer: goes long equities when consumer sentiment is depressed relative to its own history…</span></summary><div class="la-sig-body"><p>Contrarian macro timer: goes long equities when consumer sentiment is depressed relative to its own history and unemployment has recently risen, harvesting a countercyclical equity risk premium. Built purely from survey/labor/industrial-production data, uncorrelated with credit-spread or volatility-based timers.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = [
        "macro_umcsent",
        "macro_umcsent_chg12m",
        "macro_unrate_chg12m",
        "macro_indpro_chg12m",
    ]
    def first_per_date(col):
        return panel[col].groupby(level="date").first()
    data = pd.DataFrame({c: first_per_date(c) for c in cols})
    data = data.sort_index()
    data = data.ffill()
    def zexp(x: pd.Series) -&gt; pd.Series:
        mean = x.expanding().mean().shift(1)
        std = x.expanding().std().shift(1)
        z = (x - mean) / std
        z = z.where(std &gt; 0, np.nan)
        return z
    z_sent = zexp(data["macro_umcsent"])
    z_sent_chg = zexp(data["macro_umcsent_chg12m"])
    z_unrate_chg = zexp(data["macro_unrate_chg12m"])
    z_indpro_chg = zexp(data["macro_indpro_chg12m"])
    raw = (
        -z_sent
        - 0.5 * z_sent_chg
        + 0.5 * z_unrate_chg
        - 0.25 * z_indpro_chg
    )
    exposure = raw.clip(-2, 2)
    exposure = exposure.where(raw.notna(), 0.0)
    return exposure</pre></div></details><details class="la-sig"><summary><code>macro_risk_on_breadth_contrarian_timing</code><span class="la-tag">other</span><span class="gist">Counts breadth of risk-on macro trends (tightening credit spreads, falling VIX, steepening curve, rising…</span></summary><div class="la-sig-body"><p>Counts breadth of risk-on macro trends (tightening credit spreads, falling VIX, steepening curve, rising industrial production, rising consumer sentiment, rising oil) and returns the negative breadth as market exposure, contrarian to macro momentum: buy when macro consensus is uniformly deteriorating, de-risk when uniformly benign.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = [
        'macro_baa_spread_chg12m',
        'macro_vix_chg12m',
        'macro_t10y2y_chg12m',
        'macro_indpro_chg12m',
        'macro_umcsent_chg12m',
        'macro_oil_chg12m',
    ]
    date_series = {}
    for c in cols:
        s = panel[c].groupby(level='date').mean()
        s = s.sort_index()
        s = s.ffill()
        date_series[c] = s
    df = pd.DataFrame(date_series)
    def safe_sign(x):
        return x.apply(lambda v: 0.0 if pd.isna(v) else np.sign(v))
    breadth = (
        safe_sign(-df['macro_baa_spread_chg12m'])
        + safe_sign(-df['macro_vix_chg12m'])
        + safe_sign(df['macro_t10y2y_chg12m'])
        + safe_sign(df['macro_indpro_chg12m'])
        + safe_sign(df['macro_umcsent_chg12m'])
        + safe_sign(df['macro_oil_chg12m'])
    )
    breadth.name = self.name
    return breadth</pre></div></details><details class="la-sig"><summary><code>oil_demand_vs_supply_shock_timing</code><span class="la-tag">other</span><span class="gist">Interacts the cross-sectional average stock-oil correlation with the trailing oil price trend to distinguish…</span></summary><div class="la-sig-body"><p>Interacts the cross-sectional average stock-oil correlation with the trailing oil price trend to distinguish demand-driven vs supply-driven oil shocks, timing market exposure accordingly.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    min_obs = 12
    # Cross-sectional average correlation with oil, per date
    avgcorr_t = panel['corr_oil_252'].groupby(level='date').mean()
    avgcorr_t = avgcorr_t.sort_index()
    # Macro oil series (broadcast per date -&gt; take date-level mean)
    macro_oil = panel['macro_oil'].groupby(level='date').mean().sort_index()
    macro_oil_chg12m = panel['macro_oil_chg12m'].groupby(level='date').mean().sort_index()
    # Oil trend measure: 12m change normalized by lagged price level
    macro_oil_lag12 = macro_oil.shift(12)
    o_raw = macro_oil_chg12m / macro_oil_lag12.where(macro_oil_lag12 &gt; 0)
    # Past-only expanding z-score helper
    def past_only_zscore(s: pd.Series) -&gt; pd.Series:
        mean_shifted = s.expanding().mean().shift(1)
        std_shifted = s.expanding().std().shift(1)
        count_shifted = s.expanding().count().shift(1)
        z = (s - mean_shifted) / std_shifted
        z = z.where((count_shifted &gt;= min_obs) &amp; (std_shifted &gt; 0))
        return z
    c = past_only_zscore(avgcorr_t)
    o = past_only_zscore(o_raw)
    exposure = c * o
    # Neutralize (set to 0) where inputs are missing per policy
    invalid = avgcorr_t.isna() | macro_oil.isna() | macro_oil_chg12m.isna() | c.isna() | o.isna()
    exposure = exposure.where(~invalid, 0.0)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>oil_shock_demand_supply_timing</code><span class="la-tag">other</span><span class="gist">Cross-sectional average trailing correlation of stocks with oil classifies whether oil moves are…</span></summary><div class="la-sig-body"><p>Cross-sectional average trailing correlation of stocks with oil classifies whether oil moves are demand-driven (growth-friendly, positive correlation) or supply-driven (cost shock, negative correlation). Multiplying this revealed correlation by the recent oil trend gives a market-exposure signal: long equities during demand-led oil rallies, defensive during supply-led ones.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    mean_corr = panel['corr_oil_252'].groupby(level='date').mean()
    macro_oil = panel['macro_oil'].groupby(level='date').first()
    macro_oil_chg12m = panel['macro_oil_chg12m'].groupby(level='date').first()
    denom = macro_oil.where(macro_oil &gt; 0)
    oil_mom = macro_oil_chg12m / denom
    oil_mom = oil_mom.clip(-1, 1)
    exposure = mean_corr * oil_mom
    # forward-fill exposure when oil_mom is NaN (macro_oil missing/invalid)
    exposure = exposure.where(oil_mom.notna())
    exposure = exposure.ffill()
    # remaining NaNs (e.g., at start) treated as neutral
    exposure = exposure.fillna(0.0)
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>profitless_market_cap_share_timing</code><span class="la-tag">other</span><span class="gist">Aggregate market-cap share held by firms with negative trailing twelve-month net income, measured as a…</span></summary><div class="la-sig-body"><p>Aggregate market-cap share held by firms with negative trailing twelve-month net income, measured as a deviation from its own 36-month trailing norm. Rising 'froth' share signals speculative excess and contrarian-low future market returns; exposure is the negative of the deviation (harness applies expected_sign).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['netinc', 'marketcap', 'closeunadj', 'dvol_21']].copy()
    # liquidity/data filters
    valid = (
        df['marketcap'].notna() &amp; (df['marketcap'] &gt; 0) &amp;
        df['closeunadj'].notna() &amp;
        df['dvol_21'].notna()
    )
    df = df[valid]
    # TTM net income via lags per ticker
    netinc = df['netinc']
    shift3 = netinc.groupby(level='ticker').shift(3)
    shift6 = netinc.groupby(level='ticker').shift(6)
    shift9 = netinc.groupby(level='ticker').shift(9)
    ttm_netinc = netinc + shift3 + shift6 + shift9
    ttm_valid = netinc.notna() &amp; shift3.notna() &amp; shift6.notna() &amp; shift9.notna()
    df['ttm_netinc'] = ttm_netinc
    df = df[ttm_valid]
    # profitless flag
    df['profitless_mcap'] = np.where(df['ttm_netinc'] &lt; 0, df['marketcap'], 0.0)
    grouped = df.groupby(level='date')
    sum_profitless = grouped['profitless_mcap'].sum()
    sum_total = grouped['marketcap'].sum()
    froth = sum_profitless / sum_total.where(sum_total &gt; 0)
    froth = froth.sort_index()
    rolling_mean = froth.shift(1).rolling(window=36, min_periods=24).mean()
    signal = froth - rolling_mean
    exposure = -signal
    return exposure</pre></div></details><details class="la-sig"><summary><code>real_policy_stance_timing</code><span class="la-tag">other</span><span class="gist">Real policy stance timing: combines the real fed funds rate (fed funds minus trailing 12m CPI inflation) with…</span></summary><div class="la-sig-body"><p>Real policy stance timing: combines the real fed funds rate (fed funds minus trailing 12m CPI inflation) with the 12m change in fed funds to gauge monetary tightness. A high/rising real policy stance signals scarce liquidity and elevated discount rates, predicting lower forward equity returns; a deeply negative/easing stance predicts higher forward returns (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cpi = panel['macro_cpi'].groupby(level='date').first().sort_index()
    fedfunds = panel['macro_fedfunds'].groupby(level='date').first().sort_index()
    fedfunds_chg12m = panel['macro_fedfunds_chg12m'].groupby(level='date').first().sort_index()
    cpi = cpi.ffill()
    fedfunds = fedfunds.ffill()
    fedfunds_chg12m = fedfunds_chg12m.ffill()
    cpi_lag12 = cpi.shift(12)
    infl = 100.0 * (cpi / cpi_lag12 - 1.0)
    infl = infl.where(cpi_lag12 &gt; 0, np.nan)
    real_rate = fedfunds - infl
    stance = 0.6 * real_rate + 0.4 * fedfunds_chg12m
    smooth = stance.rolling(3, min_periods=1).mean()
    return smooth</pre></div></details><details class="la-sig"><summary><code>real_yield_shock_timing</code><span class="la-tag">other</span><span class="gist">Decomposes nominal 10y yield into breakeven inflation and real yield</span></summary><div class="la-sig-body"><p>Decomposes nominal 10y yield into breakeven inflation and real yield. Rising real yields (12m shock) and elevated real yield relative to its own 60m history signal tightening financial conditions and predict weak equity returns; falling real yields support risk-taking.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    dgs10 = panel['macro_dgs10'].groupby(level='date').first()
    breakeven10 = panel['macro_breakeven10'].groupby(level='date').first()
    dgs10_chg12m = panel['macro_dgs10_chg12m'].groupby(level='date').first()
    breakeven10_chg12m = panel['macro_breakeven10_chg12m'].groupby(level='date').first()
    dates = dgs10.index.sort_values()
    dgs10 = dgs10.reindex(dates)
    breakeven10 = breakeven10.reindex(dates)
    dgs10_chg12m = dgs10_chg12m.reindex(dates)
    breakeven10_chg12m = breakeven10_chg12m.reindex(dates)
    real10 = dgs10 - breakeven10
    shock12_direct = real10 - real10.shift(12)
    shock12_fallback = dgs10_chg12m - breakeven10_chg12m
    shock12 = shock12_direct.where(shock12_direct.notna(), shock12_fallback)
    roll_mean = real10.rolling(60, min_periods=24).mean().shift(1)
    gap = real10 - roll_mean
    shock12_valid = shock12.notna()
    gap_valid = gap.notna()
    shock12_term = 0.6 * shock12.fillna(0.0)
    gap_term = 0.4 * gap.fillna(0.0)
    signal = shock12_term + gap_term
    signal = signal.where(shock12_valid | gap_valid, np.nan)
    signal.index.name = 'date'
    return signal</pre></div></details><details class="la-sig"><summary><code>sahm_labor_deterioration_timing</code><span class="la-tag">other</span><span class="gist">Sahm-rule-style labor deterioration timing signal: computes the gap between current unemployment and its…</span></summary><div class="la-sig-body"><p>Sahm-rule-style labor deterioration timing signal: computes the gap between current unemployment and its trailing 12-month minimum, applies a hinge at 0.2pp (nonlinear, near-zero in expansions, positive once labor markets crack), and scales confidence by whether consumer sentiment is deteriorating. Higher exposure_raw indicates rising recession risk and is expected to precede negative equity returns (expected_sign = -1).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    unrate = panel['macro_unrate'].groupby(level='date').mean().sort_index()
    umcsent_chg = panel['macro_umcsent_chg12m'].groupby(level='date').mean().sort_index()
    unrate = unrate.ffill()
    umcsent_chg = umcsent_chg.ffill()
    u_min12 = unrate.rolling(window=12, min_periods=1).min()
    gap = unrate - u_min12
    hinge = (gap - 0.2).clip(lower=0.0)
    conf = pd.Series(0.5, index=umcsent_chg.index)
    conf[umcsent_chg &lt; 0] = 1.0
    exposure_raw = hinge * conf
    missing_mask = unrate.isna() | umcsent_chg.isna()
    exposure_raw = exposure_raw.where(~missing_mask, 0.0)
    exposure_raw.name = self.name
    return exposure_raw</pre></div></details><details class="la-sig"><summary><code>seasonal_risk_appetite_credit_timing</code><span class="la-tag">other</span><span class="gist">Halloween/Sell-in-May seasonal timing rule conditioned on the level of the Baa credit spread: long risk in…</span></summary><div class="la-sig-body"><p>Halloween/Sell-in-May seasonal timing rule conditioned on the level of the Baa credit spread: long risk in the winter half (Nov-Apr) and short/defensive in the summer half (May-Oct), with the winter exposure amplified when the expanding past-only z-score of the Baa credit spread is elevated (wide risk premia).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    baa = panel['macro_baa_spread'].groupby(level='date').mean()
    baa = baa.sort_index()
    baa = baa.ffill()
    baa = baa.dropna()
    dates = baa.index
    # seasonal sign: +1 for Nov-Apr, -1 for May-Oct
    months = pd.Series(dates.month, index=dates)
    hall = months.apply(lambda m: 1.0 if m in (11, 12, 1, 2, 3, 4) else -1.0)
    # expanding past-only z-score, min 36 obs, shift(1)
    expanding_mean = baa.expanding(min_periods=36).mean().shift(1)
    expanding_std = baa.expanding(min_periods=36).std().shift(1)
    z = (baa - expanding_mean) / expanding_std
    z = z.where(expanding_std &gt; 0, np.nan)
    z = z.fillna(0.0)
    z_clipped = z.clip(lower=0, upper=2)
    exposure = hall * (1.0 + z_clipped)
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_capitulation_recovery_timing</code><span class="la-tag">other</span><span class="gist">Countercyclical market-timing signal that goes long equity exposure when consumer sentiment is deeply…</span></summary><div class="la-sig-body"><p>Countercyclical market-timing signal that goes long equity exposure when consumer sentiment is deeply depressed relative to its own trailing history (contrarian level) AND is showing signs of stabilization/improvement over the last 3 months (confirming trend), with a secondary boost from elevated unemployment relative to its trailing history. Combines a contrarian level with a confirming direction to capture the capitulation-then-recovery phase of the sentiment cycle.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    umc = panel['macro_umcsent'].groupby(level='date').mean().sort_index()
    umc = umc.ffill()
    lvl_mean = umc.rolling(60, min_periods=24).mean()
    lvl_std = umc.rolling(60, min_periods=24).std()
    lvl_z = (umc - lvl_mean) / lvl_std
    lvl_z = lvl_z.where(lvl_std &gt; 0)
    d3 = umc - umc.shift(3)
    d3_mean = d3.rolling(60, min_periods=24).mean()
    d3_std = d3.rolling(60, min_periods=24).std()
    mom_z = (d3 - d3_mean) / d3_std
    mom_z = mom_z.where(d3_std &gt; 0)
    ur = panel['macro_unrate'].groupby(level='date').mean().sort_index()
    ur = ur.ffill()
    ur_mean = ur.rolling(60, min_periods=24).mean()
    ur_std = ur.rolling(60, min_periods=24).std()
    ur_z = (ur - ur_mean) / ur_std
    ur_z = ur_z.where(ur_std &gt; 0)
    exposure = (-lvl_z + mom_z + 0.5 * ur_z) / 2.5
    exposure = exposure.where(~(lvl_z.isna() | mom_z.isna() | ur_z.isna()), 0.0)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_fundamental_disagreement_timing</code><span class="la-tag">other</span><span class="gist">Orthogonalizes consumer sentiment against a composite of hard macro fundamentals (unemployment level/change,…</span></summary><div class="la-sig-body"><p>Orthogonalizes consumer sentiment against a composite of hard macro fundamentals (unemployment level/change, industrial production growth, CPI inflation change). When sentiment is far more pessimistic than hard data warrant, equity risk premia are elevated and subsequent market returns tend to be high; excess euphoria predicts the reverse.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    m = panel.groupby(level='date').first()
    m = m.sort_index()
    umcsent = m['macro_umcsent'].ffill()
    unrate = m['macro_unrate'].ffill()
    unrate_chg12m = m['macro_unrate_chg12m'].ffill()
    indpro_chg12m = m['macro_indpro_chg12m'].ffill()
    cpi_chg12m = m['macro_cpi_chg12m'].ffill()
    z_sent = self._zscore(umcsent)
    z_unrate = self._zscore(unrate)
    z_unrate_chg = self._zscore(unrate_chg12m)
    z_indpro_chg = self._zscore(indpro_chg12m)
    z_cpi_chg = self._zscore(cpi_chg12m)
    z_fund = (-z_unrate - z_unrate_chg + z_indpro_chg - z_cpi_chg) / 4.0
    signal = z_fund - z_sent
    valid = (
        z_sent.notna()
        &amp; z_unrate.notna()
        &amp; z_unrate_chg.notna()
        &amp; z_indpro_chg.notna()
        &amp; z_cpi_chg.notna()
    )
    signal = signal.where(valid)
    return signal</pre></div></details><details class="la-sig"><summary><code>sentiment_fundamental_divergence_timing</code><span class="la-tag">other</span><span class="gist">Market-timing signal based on divergence between consumer sentiment (level + 12m change) and hard economic…</span></summary><div class="la-sig-body"><p>Market-timing signal based on divergence between consumer sentiment (level + 12m change) and hard economic data (industrial production and unemployment 12m changes). When sentiment is far more depressed than fundamentals justify, expected equity risk premium is high and exposure is increased; when sentiment overshoots fundamentals, exposure is reduced.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    umcsent = panel['macro_umcsent'].groupby(level='date').first().sort_index()
    umcsent_chg12m = panel['macro_umcsent_chg12m'].groupby(level='date').first().sort_index()
    indpro_chg12m = panel['macro_indpro_chg12m'].groupby(level='date').first().sort_index()
    unrate_chg12m = panel['macro_unrate_chg12m'].groupby(level='date').first().sort_index()
    umcsent = umcsent.ffill()
    umcsent_chg12m = umcsent_chg12m.ffill()
    indpro_chg12m = indpro_chg12m.ffill()
    unrate_chg12m = unrate_chg12m.ffill()
    z_umcsent = self._zexp(umcsent)
    z_umcsent_chg12m = self._zexp(umcsent_chg12m)
    z_indpro_chg12m = self._zexp(indpro_chg12m)
    z_unrate_chg12m = self._zexp(unrate_chg12m)
    sent = 0.5 * z_umcsent + 0.5 * z_umcsent_chg12m
    fund = z_indpro_chg12m - z_unrate_chg12m
    exposure = (fund - sent).clip(-2, 2)
    exposure = exposure.where(sent.notna() &amp; fund.notna(), 0.0)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_fundamental_gap_timing</code><span class="la-tag">other</span><span class="gist">Gap between consumer sentiment (level + 12m change) and hard macro fundamentals (unemployment rate/change,…</span></summary><div class="la-sig-body"><p>Gap between consumer sentiment (level + 12m change) and hard macro fundamentals (unemployment rate/change, industrial production growth), z-scored with expanding past-only moments. High sentiment relative to fundamentals signals compressed risk premia and low forward returns (behavioral/affect component isolated from the business cycle itself).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cols = ['macro_umcsent', 'macro_umcsent_chg12m', 'macro_unrate',
            'macro_unrate_chg12m', 'macro_indpro_chg12m']
    date_level = {}
    for c in cols:
        s = panel[c].groupby(level='date').mean()
        s = s.sort_index()
        s = s.ffill()
        date_level[c] = s
    z_umcsent = self._zx(date_level['macro_umcsent']).fillna(0.0)
    z_umcsent_chg = self._zx(date_level['macro_umcsent_chg12m']).fillna(0.0)
    z_unrate = self._zx(date_level['macro_unrate']).fillna(0.0)
    z_unrate_chg = self._zx(date_level['macro_unrate_chg12m']).fillna(0.0)
    z_indpro_chg = self._zx(date_level['macro_indpro_chg12m']).fillna(0.0)
    sentiment = 0.5 * z_umcsent + 0.5 * z_umcsent_chg
    fundamentals = 0.5 * (-z_unrate) + 0.25 * (-z_unrate_chg) + 0.25 * z_indpro_chg
    signal_val = sentiment - fundamentals
    # dates with all-NaN raw inputs should yield NaN
    raw_all_nan = pd.concat(
        [date_level[c] for c in cols], axis=1
    ).isna().all(axis=1)
    signal_val = signal_val.where(~raw_all_nan, np.nan)
    exposure = -signal_val
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_fundamental_pessimism_gap_timing</code><span class="la-tag">other</span><span class="gist">Residual of consumer sentiment vs</span></summary><div class="la-sig-body"><p>Residual of consumer sentiment vs. macro-fundamentals-implied sentiment (unemployment, CPI change, oil change, industrial production change). When sentiment is unjustifiably worse than fundamentals imply, subsequent market returns tend to be higher; positioned as a contrarian macro-timing signal.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    def get_date_series(col):
        s = panel[col].groupby(level='date').mean()
        s = s.sort_index()
        s = s.ffill()
        return s
    umcsent = get_date_series('macro_umcsent')
    unrate = get_date_series('macro_unrate')
    cpi_chg12m = get_date_series('macro_cpi_chg12m')
    oil_chg12m = get_date_series('macro_oil_chg12m')
    indpro_chg12m = get_date_series('macro_indpro_chg12m')
    unrate_chg12m = get_date_series('macro_unrate_chg12m')
    def zx(s):
        mean = s.expanding(36).mean()
        std = s.expanding(36).std()
        z = (s - mean) / std
        z = z.where(std &gt; 0, np.nan)
        return z
    z_umcsent = zx(umcsent)
    z_unrate = zx(unrate)
    z_cpi_chg12m = zx(cpi_chg12m)
    z_oil_chg12m = zx(oil_chg12m)
    z_indpro_chg12m = zx(indpro_chg12m)
    z_unrate_chg12m = zx(unrate_chg12m)
    components = pd.DataFrame({
        'unrate': z_unrate,
        'cpi_chg12m': z_cpi_chg12m,
        'oil_chg12m': z_oil_chg12m,
        'indpro_chg12m': -z_indpro_chg12m,
        'unrate_chg12m': z_unrate_chg12m,
    })
    valid_mask = components.notna().any(axis=1)
    components_filled = components.fillna(0.0)
    misery = (
        components_filled['unrate']
        + components_filled['cpi_chg12m']
        + components_filled['oil_chg12m']
        + components_filled['indpro_chg12m']
        - components_filled['unrate_chg12m']
    )
    implied_sent = -misery / 4.0
    exposure = implied_sent - z_umcsent.fillna(0.0)
    # if z_umcsent itself is NaN and no other component valid either, mark NaN
    all_nan_mask = components.isna().all(axis=1) &amp; z_umcsent.isna()
    exposure = exposure.where(~all_nan_mask, np.nan)
    exposure = exposure.where(valid_mask | z_umcsent.notna(), np.nan)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_gap_contrarian_timing</code><span class="la-tag">other</span><span class="gist">Contrarian macro timing signal using consumer sentiment deviation from its trailing 3-year mean…</span></summary><div class="la-sig-body"><p>Contrarian macro timing signal using consumer sentiment deviation from its trailing 3-year mean (countercyclical) combined with unemployment slack and rising unemployment momentum. Elevated risk aversion (low sentiment, high slack, rising unemployment) predicts higher subsequent equity risk premia.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    s = panel['macro_umcsent'].groupby(level='date').first()
    u = panel['macro_unrate'].groupby(level='date').first()
    du = panel['macro_unrate_chg12m'].groupby(level='date').first()
    s_mean = s.rolling(36, min_periods=18).mean()
    s_std = s.rolling(36, min_periods=18).std()
    sent_gap = (s - s_mean) / s_std.where(s_std &gt; 0)
    u_mean = u.rolling(36, min_periods=18).mean()
    u_std = u.rolling(36, min_periods=18).std()
    slack = (u - u_mean) / u_std.where(u_std &gt; 0)
    exposure = (-sent_gap + 0.5 * slack + 0.5 * du.clip(lower=0)).clip(-3, 3)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_hard_data_divergence_timing</code><span class="la-tag">other</span><span class="gist">Contrarian macro timing signal using the divergence between household sentiment (UMich) and hard data…</span></summary><div class="la-sig-body"><p>Contrarian macro timing signal using the divergence between household sentiment (UMich) and hard data (industrial production, unemployment). When sentiment is high relative to hard data, expected market returns are low (euphoria priced in); when sentiment is depressed relative to solid hard data, expected returns are high (elevated risk premium). Exposure is hard_z - sent_z, expanding-window z-scored, past-only, min 36 obs.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    umcsent = panel['macro_umcsent'].groupby(level='date').mean().sort_index()
    indpro_chg = panel['macro_indpro_chg12m'].groupby(level='date').mean().sort_index()
    unrate_chg = panel['macro_unrate_chg12m'].groupby(level='date').mean().sort_index()
    umcsent = umcsent.ffill()
    indpro_chg = indpro_chg.ffill()
    unrate_chg = unrate_chg.ffill()
    sent_z = self._expanding_z(umcsent, min_obs=36)
    indpro_z = self._expanding_z(indpro_chg, min_obs=36)
    unrate_z = self._expanding_z(unrate_chg, min_obs=36)
    hard_z = 0.5 * indpro_z - 0.5 * unrate_z
    exposure = hard_z - sent_z
    exposure = exposure.clip(-2.0, 2.0)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>sentiment_misery_residual_timing</code><span class="la-tag">other</span><span class="gist">Times the market using the residual of consumer sentiment relative to the misery index (unemployment +…</span></summary><div class="la-sig-body"><p>Times the market using the residual of consumer sentiment relative to the misery index (unemployment + realized inflation). Excess pessimism relative to observable hardship (a large negative sentiment+misery residual) is a contrarian bullish signal for future market returns; unjustified optimism amid high misery is bearish.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cpi = panel['macro_cpi'].groupby(level='date').first()
    cpi_chg12m = panel['macro_cpi_chg12m'].groupby(level='date').first()
    unrate = panel['macro_unrate'].groupby(level='date').first()
    umcsent = panel['macro_umcsent'].groupby(level='date').first()
    dates = cpi.index.sort_values()
    cpi = cpi.reindex(dates).ffill()
    cpi_chg12m = cpi_chg12m.reindex(dates).ffill()
    unrate = unrate.reindex(dates).ffill()
    umcsent = umcsent.reindex(dates).ffill()
    infl = 100.0 * cpi_chg12m / cpi.where(cpi &gt; 0)
    misery = unrate + infl
    required_valid = umcsent.notna() &amp; misery.notna()
    s_mean = umcsent.expanding(min_periods=36).mean()
    s_std = umcsent.expanding(min_periods=36).std()
    m_mean = misery.expanding(min_periods=36).mean()
    m_std = misery.expanding(min_periods=36).std()
    zs = (umcsent - s_mean) / s_std.where(s_std &gt; 0)
    zm = (misery - m_mean) / m_std.where(m_std &gt; 0)
    residual = zs + zm
    exposure = -residual
    exposure = exposure.clip(-2, 2)
    count = pd.Series(np.arange(1, len(dates) + 1), index=dates)
    has_history = count &gt;= 36
    valid = required_valid &amp; has_history &amp; exposure.notna()
    exposure = exposure.where(valid, 0.0)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>stagflation_supply_shock_timing</code><span class="la-tag">other</span><span class="gist">Decomposes oil price moves by concurrent industrial production impulse to distinguish demand-driven oil…</span></summary><div class="la-sig-body"><p>Decomposes oil price moves by concurrent industrial production impulse to distinguish demand-driven oil rallies (benign for equities) from adverse supply shocks (oil and breakeven inflation rising while industrial production stalls). Exposure is reduced when supply-shock stagflationary conditions are detected.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    oil_chg = panel['macro_oil_chg12m'].groupby(level='date').first().sort_index()
    breakeven_chg = panel['macro_breakeven10_chg12m'].groupby(level='date').first().sort_index()
    cpi_chg = panel['macro_cpi_chg12m'].groupby(level='date').first().sort_index()
    indpro_chg = panel['macro_indpro_chg12m'].groupby(level='date').first().sort_index()
    oil_chg = oil_chg.ffill()
    breakeven_chg = breakeven_chg.ffill()
    cpi_chg = cpi_chg.ffill()
    indpro_chg = indpro_chg.ffill()
    z_oil = self._zexp(oil_chg)
    z_breakeven = self._zexp(breakeven_chg)
    z_cpi = self._zexp(cpi_chg)
    z_indpro = self._zexp(indpro_chg)
    components = pd.concat(
        [
            0.5 * z_oil,
            0.25 * z_breakeven,
            0.25 * z_cpi,
            -0.6 * z_indpro,
        ],
        axis=1,
    )
    all_nan_mask = components.isna().all(axis=1)
    components_filled = components.fillna(0.0)
    supply_shock = components_filled.sum(axis=1)
    supply_shock[all_nan_mask] = np.nan
    exposure = -supply_shock.clip(lower=-2.5, upper=2.5)
    exposure = exposure.where(~all_nan_mask, 0.0)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>taylor_rule_policy_accommodation_timing</code><span class="la-tag">other</span><span class="gist">Rule-relative monetary policy stance: compares the effective fed funds rate to a Taylor-rule-implied rate…</span></summary><div class="la-sig-body"><p>Rule-relative monetary policy stance: compares the effective fed funds rate to a Taylor-rule-implied rate based on realized inflation and deviation of unemployment from its trailing 10-year average. Positive accommodation (rule rate above actual fed funds) signals abundant funding liquidity and lower discount rates, predicting higher subsequent equity market returns.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    cpi = panel['macro_cpi'].groupby(level='date').first().sort_index()
    cpi_chg = panel['macro_cpi_chg12m'].groupby(level='date').first().sort_index()
    unrate = panel['macro_unrate'].groupby(level='date').first().sort_index()
    fedfunds = panel['macro_fedfunds'].groupby(level='date').first().sort_index()
    # forward-fill isolated publication gaps (up to a few months)
    cpi = cpi.ffill(limit=3)
    cpi_chg = cpi_chg.ffill(limit=3)
    unrate = unrate.ffill(limit=3)
    fedfunds = fedfunds.ffill(limit=3)
    # inflation rate (annualized, %) from cpi level and its 12m change
    cpi_base = cpi - cpi_chg
    infl = 100.0 * cpi_chg / cpi_base.where(cpi_base &gt; 0)
    # past-only rolling 120-month mean of unemployment, shifted 1 to avoid look-ahead
    ustar = unrate.shift(1).rolling(window=120, min_periods=36).mean()
    taylor = 2.0 + infl + 0.5 * (infl - 2.0) - 2.0 * (unrate - ustar)
    accommodation = taylor - fedfunds
    exposure = accommodation.dropna()
    exposure.index.name = 'date'
    return exposure</pre></div></details><details class="la-sig"><summary><code>unemployment_news_regime_asymmetry_timing</code><span class="la-tag">other</span><span class="gist">Sign-flipping labour-market timing signal: rising unemployment is treated as good news for the market during…</span></summary><div class="la-sig-body"><p>Sign-flipping labour-market timing signal: rising unemployment is treated as good news for the market during expansions (implying policy easing) but as bad news during contractions. Recession state is proxied in real time by a Sahm-rule-style rolling-minimum trigger on the unemployment rate (or the NBER indicator), avoiding look-ahead from publication-lagged recession dating.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    u = panel['macro_unrate'].groupby(level='date').first()
    u_chg12m = panel['macro_unrate_chg12m'].groupby(level='date').first()
    usrec = panel['macro_usrec'].groupby(level='date').first()
    u = u.sort_index()
    u_chg12m = u_chg12m.reindex(u.index)
    usrec = usrec.reindex(u.index)
    # rolling 12-month min of past unemployment (excludes current month)
    rolling_min = u.rolling(12, closed='left').min()
    sahm_gap = u - rolling_min
    recession_state = ((sahm_gap &gt; 0.4) | (usrec == 1)).astype(float)
    exposure = u_chg12m * (1 - 2 * recession_state)
    valid = u.notna() &amp; u_chg12m.notna() &amp; usrec.notna() &amp; sahm_gap.notna()
    exposure = exposure.where(valid, np.nan)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>unemployment_trough_breakout_timing</code><span class="la-tag">other</span><span class="gist">Sahm-rule-like gap between unemployment rate and its trailing 12-month minimum, amplified when industrial…</span></summary><div class="la-sig-body"><p>Sahm-rule-like gap between unemployment rate and its trailing 12-month minimum, amplified when industrial production momentum is negative. Higher gap signals labor-market deterioration and impending equity drawdown; harness de-risks exposure when the raw score is large (expected_sign=-1).</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    unrate = panel["macro_unrate"].groupby(level="date").first().sort_index()
    indpro_chg = panel["macro_indpro_chg12m"].groupby(level="date").first().sort_index()
    unrate = unrate.ffill()
    indpro_chg = indpro_chg.ffill()
    rolling_min = unrate.rolling(window=12, min_periods=6).min()
    gap = unrate - rolling_min
    slow = (indpro_chg &lt; 0).astype(float)
    raw = gap * (1.0 + slow)
    missing_mask = unrate.isna() | indpro_chg.isna()
    raw = raw.where(~missing_mask, np.nan)
    return raw</pre></div></details><details class="la-sig"><summary><code>valuation_dispersion_market_timing</code><span class="la-tag">other</span><span class="gist">Cross-sectional dispersion of log(P/B) across the equity universe, measured as the 90th minus 10th percentile…</span></summary><div class="la-sig-body"><p>Cross-sectional dispersion of log(P/B) across the equity universe, measured as the 90th minus 10th percentile each month. Wide dispersion signals speculative excess/disagreement and historically precedes weak aggregate returns; the signal is dispersion minus its trailing 36-month (past-only) mean, so high (wide) dispersion implies reduced market exposure.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> price, fundamental</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['pb', 'marketcap']].copy()
    df = df[(df['pb'] &gt; 0) &amp; (df['marketcap'] &gt; 0)]
    df['x'] = np.log(df['pb'])
    def disp_fn(group):
        if len(group) &lt; 200:
            return np.nan
        p90 = group.quantile(0.90)
        p10 = group.quantile(0.10)
        return p90 - p10
    disp = df.groupby(level='date')['x'].apply(disp_fn)
    disp = disp.sort_index()
    rolling_mean = disp.rolling(window=36, min_periods=24).mean()
    # ensure past-only: shift by 1 so current month's disp is not in its own rolling mean
    rolling_mean = rolling_mean.shift(1)
    signal = disp - rolling_mean
    signal = signal.reindex(disp.index)
    return signal</pre></div></details><details class="la-sig"><summary><code>variance_risk_premium_timing</code><span class="la-tag">other</span><span class="gist">Variance risk premium timing signal: gap between implied volatility (VIX) and realized equity volatility…</span></summary><div class="la-sig-body"><p>Variance risk premium timing signal: gap between implied volatility (VIX) and realized equity volatility (cross-sectional median of trailing 1y stock-level realized vol), smoothed with a 3-month rolling mean. A wide gap indicates elevated compensation for bearing variance risk, forecasting higher subsequent market excess returns.</p><div class="la-meta"><span><b>Category</b> other</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> macro, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    vix = panel['macro_vix'].groupby(level='date').first()
    rv = panel['vol_252'].groupby(level='date').median() * 100.0
    vrp = vix - rv
    raw = vrp.rolling(3, min_periods=1).mean()
    return raw</pre></div></details><details class="la-sig"><summary><code>aggregate_accruals_market_timing</code><span class="la-tag">quality</span><span class="gist">Aggregate accruals (netinc - ncfo)/assets, median across large liquid names, as a macro earnings-quality…</span></summary><div class="la-sig-body"><p>Aggregate accruals (netinc - ncfo)/assets, median across large liquid names, as a macro earnings-quality timing signal; high aggregate accruals predict low subsequent market returns, so exposure = -zscore of smoothed aggregate accruals.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['netinc', 'ncfo', 'assets', 'marketcap']].copy()
    valid = (
        df['assets'].notna() &amp; (df['assets'] &gt; 0) &amp;
        df['netinc'].notna() &amp; df['ncfo'].notna() &amp;
        df['marketcap'].notna()
    )
    df = df[valid]
    acc = (df['netinc'] - df['ncfo']) / df['assets']
    acc = acc.clip(-0.5, 0.5)
    df['acc'] = acc
    # market-cap 40th percentile filter, per date
    pct40 = df.groupby(level='date')['marketcap'].transform(
        lambda x: x.quantile(0.40)
    )
    mask = df['marketcap'] &gt;= pct40
    df = df[mask]
    # count valid names per date
    counts = df.groupby(level='date')['acc'].count()
    agg_t = df.groupby(level='date')['acc'].median()
    agg_t = agg_t.where(counts &gt;= self.MIN_NAMES, np.nan)
    agg_t = agg_t.sort_index()
    smooth = agg_t.rolling(3, min_periods=3).mean()
    exp_mean = smooth.expanding(36).mean()
    exp_std = smooth.expanding(36).std()
    z = (smooth - exp_mean) / exp_std
    z = z.where(exp_std &gt; 0, np.nan)
    exposure = -z
    exposure = exposure.clip(-2, 2)
    exposure.name = self.name
    return exposure</pre></div></details><details class="la-sig"><summary><code>aggregate_earnings_growth_breadth_timing</code><span class="la-tag">quality</span><span class="gist">Timing signal based on the cross-sectional breadth of YoY earnings growth among top liquid-cap firms</span></summary><div class="la-sig-body"><p>Timing signal based on the cross-sectional breadth of YoY earnings growth among top liquid-cap firms. Breadth is the fraction of firms with positive YoY net income growth; the signal is the deviation of current breadth from its trailing 12m average, deviation-detrended breadth. Wide breadth (broad earnings improvement) historically precedes weak subsequent market returns (Kothari, Lewellen, Warner), so expected_sign=-1.</p><div class="la-meta"><span><b>Category</b> quality</span><span><b>Direction</b> High score → risk-off (cut exposure)</span><span><b>Data</b> fundamental, price</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    netinc = panel['netinc']
    marketcap = panel['marketcap']
    ni_lag = netinc.groupby(level='ticker').shift(12)
    valid = (
        netinc.notna() &amp; ni_lag.notna()
        &amp; (ni_lag != 0)
        &amp; marketcap.notna() &amp; (marketcap &gt; 0)
    )
    # top liquid universe: top 50% by marketcap within each date among valid entries
    mc_valid = marketcap.where(valid)
    mc_rank = mc_valid.groupby(level='date').rank(pct=True, ascending=True)
    top_liquid = valid &amp; (mc_rank &gt;= 0.5)
    growth = (netinc - ni_lag) / ni_lag.abs()
    imp = (growth &gt; 0).astype(float)
    imp = imp.where(top_liquid)
    breadth_t = imp.groupby(level='date').mean()
    rolling_mean = breadth_t.rolling(12, min_periods=6).mean()
    raw_t = breadth_t - rolling_mean
    return raw_t</pre></div></details><details class="la-sig"><summary><code>money_illusion_real_yield_gap_timing</code><span class="la-tag">value</span><span class="gist">Modigliani-Cohn money illusion timing signal: compares aggregate market earnings yield (median of 1/pe among…</span></summary><div class="la-sig-body"><p>Modigliani-Cohn money illusion timing signal: compares aggregate market earnings yield (median of 1/pe among top-70% marketcap stocks) to the REAL 10y Treasury yield (nominal 10y minus trailing CPI inflation). A wide gap signals equities are irrationally cheap relative to real cash-flow discounting, forecasting high subsequent market returns as the illusion unwinds.</p><div class="la-meta"><span><b>Category</b> value</span><span><b>Direction</b> High score → risk-on (raise exposure)</span><span><b>Data</b> price, fundamental, macro</span></div>
<pre class="la-code">def compute(self, panel) -&gt; pd.Series:
    df = panel[['pe', 'marketcap', 'macro_dgs10', 'macro_cpi', 'macro_cpi_chg12m']].copy()
    valid = (df['pe'] &gt; 0) &amp; df['marketcap'].notna() &amp; (df['marketcap'] &gt; 0)
    df = df[valid]
    # top 70% marketcap per date
    def top70_mask(x):
        thresh = x.quantile(0.30)
        return x &gt;= thresh
    mc = df['marketcap']
    thresh_by_date = mc.groupby(level='date').transform(lambda x: x.quantile(0.30))
    top_df = df[mc &gt;= thresh_by_date]
    ey = 1.0 / top_df['pe']
    agg_ey = ey.groupby(level='date').median()
    macro = panel[['macro_dgs10', 'macro_cpi', 'macro_cpi_chg12m']].groupby(level='date').mean()
    denom = macro['macro_cpi'] - macro['macro_cpi_chg12m']
    valid_macro = macro['macro_dgs10'].notna() &amp; macro['macro_cpi'].notna() &amp; \
        macro['macro_cpi_chg12m'].notna() &amp; (denom &gt; 0)
    infl = 100.0 * macro['macro_cpi_chg12m'] / denom
    real_y = macro['macro_dgs10'] - infl
    signal = 100.0 * agg_ey - real_y
    signal = signal.where(valid_macro)
    signal = signal.dropna()
    signal.index.name = 'date'
    return signal</pre></div></details>
</div>
</details>


::: {.callout-warning appearance="simple"}
**Research, not investment advice.** Data ends around February 2024. Transaction
costs use a simple per-unit-traded model with no market-impact term, so capacity
is not addressed. Delisting returns are approximated rather than sourced from the
corporate-actions table. Every number here is a backtest.
:::

© 2025 Simen Bjerkelund · Built with Quarto