A real-time macro signal stack where every raw macro series is transformed into a comparable stress-oriented signal.
A set of economic blocks for inflation, policy/rates, growth, labor, housing, external trade, and macro breadth/conflict.
Several Financial Conditions Index models: an economic-weighted index, a PCA index, a supervised PLS index, a stress-probability index, and a blended index.
A validation layer that checks whether tighter financial conditions actually line up with future equity stress, higher future volatility, and worse drawdowns.
A sector-regime study that connects macro conditions to sector behavior instead of treating sector rotation as pure momentum.
A macro-gated allocation strategy where the FCI changes the risky/defensive split and macro blocks change which sectors are preferred.
A secondary Canada implementation through the library, using Canadian macro data and Canadian sector ETFs.
This project is mainly about turning macro data into a tradable state variable. A single inflation number or unemployment number doesn’t tell us enough by itself. Markets react to combinations: inflation plus policy, growth plus labor, housing plus rates, domestic demand plus external demand, and whether stress is narrow or spreading across the system.
A Financial Conditions Index, or FCI, tries to compress that large macro state into a smaller signal. In this project, higher FCI values mean tighter / more stressful conditions. Lower values mean easier conditions, stronger risk support, or at least less macro pressure. The important part is that we don’t use one black-box index only. We build the index from interpretable blocks first, then compare several ways of combining those blocks.
The goal is to make the macro signal teachable, testable, and usable.
1) The Macro Object We Want to Build
The object we want is a monthly signal that summarizes how difficult the economic environment is for risk assets. We can write the raw macro panel as:
where each \(x_{i,t}\) is one raw macro series at month \(t\). One series might be CPI inflation, another might be the unemployment rate, another might be industrial production, and another might be a yield or policy rate.
The problem is that these variables live in different units. CPI might be an index, fed funds is a percentage rate, jobless claims are people, oil is dollars per barrel, and a confidence survey may have its own scale. We can’t average them directly. We first need to transform each series into a common language.
Here \(\bar{x}_{i,t}^{\text{expanding}}\) is the mean of series \(i\) using only data available up to month \(t\), and \(s_{i,t}^{\text{expanding}}\) is the corresponding expanding standard deviation. A value of \(z_{i,t}=2\) means the current observation is about two historical standard deviations above its own past normal level.
The real-time part matters. We don’t compute the mean and standard deviation using the full sample, because that would let the future define the past. A macro index built for trading has to use information that would’ve been known at the time.
Macro Transmission Into Asset Prices
A macro variable matters for a portfolio only if it changes cash flows, discount rates, risk premia, or liquidity. A simple present-value formula makes this connection clear:
where \(P_t\) is the asset price, \(CF_{t+h}\) is the future cash flow, \(r_{f,t,h}\) is the risk-free discount rate for horizon \(h\), and \(\lambda_{t,h}\) is the risk premium investors demand for bearing uncertainty. Macro conditions enter both the numerator and the denominator.
Inflation and policy mainly push through the denominator. Higher inflation can raise expected policy rates and bond yields, so \(r_f\) rises. Recession risk and credit stress mainly push through both parts: expected cash flows fall and the risk premium \(\lambda\) rises. Labor and housing affect consumer income, credit losses, and domestic demand. External trade affects exporters, commodities, the dollar, and global earnings.
This is why the project doesn’t treat macro data as decoration. The FCI is a way to summarize whether the macro environment is raising the discount-rate burden, weakening cash-flow expectations, or increasing the required risk premium.
Stress Orientation
After standardization, we also need a sign convention. In this project, higher means more stress.
That convention is straightforward for variables like unemployment, jobless claims, inflation pressure, and policy rates. Higher values usually mean more macro pressure. For variables like industrial production, retail sales, payroll growth, housing activity, and confidence surveys, the direction is reversed. Higher growth is good, so lower growth is the stressful state.
For a raw series where high values are stressful, we keep the sign:
\[
s_{i,t}=z_{i,t}
\]
For a raw series where low values are stressful, we flip it:
\[
s_{i,t}=-z_{i,t}
\]
The final \(s_{i,t}\) is a stress-oriented macro signal. This lets us compare inflation, labor, housing, and growth in the same direction.
A useful example is industrial production. If industrial production is far above its historical expanding mean, the raw z-score is positive. But strong production isn’t recession stress, so the stress signal is negative. If production collapses during a recession, the raw z-score becomes negative and the stress signal becomes positive. That positive value then lines up with other stress variables like rising claims or falling surveys.
A Small Numerical Example of Stress Orientation
Suppose an industrial production series has an expanding mean of 100 and an expanding standard deviation of 5. If the latest value is 110, the raw z-score is:
\[
z_t=\frac{110-100}{5}=2
\]
But industrial production being two standard deviations above normal is usually not recession stress. For a growth variable, we flip the sign:
\[
s_t^{growth}=-2
\]
Now suppose a few months later industrial production falls to 90. The raw z-score is:
\[
z_t=\frac{90-100}{5}=-2
\]
After the sign flip:
\[
s_t^{growth}=2
\]
That positive value now means growth stress. The same stress orientation lets us average growth weakness with labor weakness, housing weakness, and policy pressure. Without the sign convention, the FCI would mix variables where positive means good with variables where positive means bad, and the index would become economically confused.
2) Setup and Reused Data Layer
The first code cells load the scientific Python stack, the project library, and the plotting settings. This part doesn’t need heavy interpretation. The important thing is that the project works at two frequencies:
Monthly macro frequency, where the raw economic indicators are transformed into macro signals and FCI models.
Daily or monthly market frequency, where ETF prices are used for allocation and performance testing.
For reproducibility, the data should be understood through the repo’s data-generation folders, not as isolated final files. Check the relevant folders inside the repo’s data folder. For this project, the important folders are the ones corresponding to the US macro factors, NFCI, sector ETFs, core cross-asset ETFs, Canadian macro factors, Canadian sector ETFs, and international hedging ETFs.
The sector and cross-asset ETF data has already appeared earlier in the series, so we keep it short here. The new and important part is the macro panel: what each group measures, how it becomes a stress signal, and how those signals are turned into regimes.
The raw macro factor panel runs from January 1990 to January 2026, with 433 monthly rows and 56 macro factors. That is a long enough span to include the early 1990s cycle, the dot-com recession, the 2008 crisis, the COVID shock, the 2022 inflation/rate shock, and the post-2023 normalization period.
The availability table tells us that not every series has the same sample. Some external trade series end in 2014. Some OECD survey series end in 2024. Many core macro series, such as CPI/PCE, PPI, import prices, oil, fed funds, and Treasury yields, run through 2026. This is why the project later builds block-level averages and coverage checks rather than requiring every raw input to be present at every date.
This is realistic macro data behavior. Macro datasets are messy because agencies revise series, discontinue series, add new survey formats, and report different variables at different frequencies. The goal isn’t to force a perfectly rectangular panel; the goal is to build a robust index that can still work when some inputs are unavailable.
Timing, Revisions, and Real-Time Discipline
Macro data has a timing problem that price data usually doesn’t have. Many macro variables are released with delays, then revised later. A payroll number, CPI print, GDP estimate, or retail-sales value may be different in the first release than in the final database. This project doesn’t fully reconstruct historical release calendars, but it still keeps an important real-time discipline: transformations use expanding history and allocation features are shifted before trading.
The expanding z-score prevents future distribution information from entering past signals. The one-month shift before allocation prevents the strategy from using a same-month macro reading as if it were known at the rebalance. That is a practical compromise for a notebook project.
A fully production-grade macro strategy would go further. It would use vintage macro data, release timestamps, nowcasting rules, and publication lags. But even without that, the project is much safer than computing full-sample z-scores and immediately trading same-month signals.
This timing discipline is why the strategy interpretation is more credible. The FCI isn’t just a historical chart; it is built in a way that approximates how a real investor could’ve used the information.
3) Real-Time Standardization Functions
Before building the macro blocks, we define a small transformation toolkit.
The most important function is the expanding z-score. For a series \(x_t\), the expanding mean and variance at time \(t\) are:
The implementation requires a minimum number of observations before the score is trusted. In this project the main US run uses a 60-month warmup, so the first valid score needs about five years of history. The score is also clipped, usually inside \([-5,5]\), because macro outliers can otherwise dominate an average. A pandemic collapse or oil shock is real information, but a single extreme value shouldn’t numerically drown every other variable in the block.
Two additional operations matter:
Level pressure, where we standardize the level of a variable.
Impulse pressure, where we standardize the recent change.
For a three-month change:
\[
\Delta_3 x_t=x_t-x_{t-3}
\]
and its stress score is the expanding z-score of \(\Delta_3 x_t\), possibly with a sign flip. This is how we separate a variable being high from a variable getting worse quickly.
Level, Impulse, Acceleration, and Diffusion
Many macro regimes are defined by both level and speed. Inflation at 4% is different if it is falling from 8% versus rising from 2%. Growth that is weak but improving is different from growth that is strong but decelerating fast.
That is why we build several signal types:
Signal type
Mathematical object
Economic meaning
Level
\(z(x_t)\)
Is the variable high or low relative to its own history?
Impulse
\(z(x_t-x_{t-3})\)
Has the variable moved unusually over the last three months?
Acceleration
\(z((x_t-x_{t-3})-(x_{t-3}-x_{t-6}))\)
Is the recent change itself speeding up or cooling?
Diffusion
share of variables above a stress threshold
Is stress broad across many indicators or concentrated in a few?
The diffusion idea is especially important. A single inflation series can spike because of one component. A broad inflation regime means many price measures are elevated together. Formally, if we have \(N\) stress-oriented indicators inside a group, the diffusion share is:
where \(c\) is the threshold, here around \(0.5\), and \(N_t\) is the number of available series at month \(t\). The diffusion share is then standardized through time. High diffusion means stress is not just intense; it is broad.
Show code
def expanding_zscore(series, min_obs=60, clip=5.0): series = pd.Series(series, dtype=float).replace([np.inf, -np.inf], np.nan) mean = series.expanding(min_periods=min_obs).mean() std = series.expanding(min_periods=min_obs).std(ddof=1).replace(0.0, np.nan) z = (series - mean) / stdreturn z.clip(-clip, clip) if clip isnotNoneelse zdef prefix_cols(data, prefixes): prefixes =tuple(prefixes)return [col for col in data.columns if col.startswith(prefixes)]def mean_z(data, columns, min_obs=60, sign=1.0):iflen(columns) ==0:return pd.Series(np.nan, index=data.index) z = data[columns].apply(lambda s: expanding_zscore(s, min_obs=min_obs))return sign * z.mean(axis=1, skipna=True)def change_z(data, columns, periods=3, min_obs=60, sign=1.0):iflen(columns) ==0:return pd.Series(np.nan, index=data.index) z = data[columns].diff(periods).apply(lambda s: expanding_zscore(s, min_obs=min_obs))return sign * z.mean(axis=1, skipna=True)def stress_share(data, columns, min_obs=60, threshold=0.5, sign=1.0):iflen(columns) ==0:return pd.Series(np.nan, index=data.index) z = data[columns].apply(lambda s: expanding_zscore(s, min_obs=min_obs)) events = sign * z > threshold share = events.where(z.notna()).mean(axis=1, skipna=True)return expanding_zscore(share, min_obs=min_obs)def plot_signal_group(data, title, ncols=2): cols =list(data.columns) nrows = math.ceil(len(cols) / ncols) fig, axes = plt.subplots(nrows, ncols, figsize=(7* ncols, 2.6* nrows), sharex=True) axes = np.array(axes).reshape(-1)for i, col inenumerate(cols): series = data[col].loc[data.index >= minimum_visible_date].dropna() series.plot(ax=axes[i], lw=1.2, color=colors[i %len(colors)]) axes[i].axhline(0.0, color="black", lw=0.7, alpha=0.4) axes[i].set_title(col, fontsize=9) axes[i].set_xlabel("") axes[i].grid(True, alpha=0.25)for ax in axes[len(cols):]: ax.set_axis_off() fig.suptitle(title, y=1.02, fontsize=11) plt.tight_layout() plt.show()return fig, axes
4) Macro Factor Groups
The macro panel is divided into economically meaningful groups. This grouping is one of the most important design choices in the project because the later FCI is only as good as the information blocks that enter it.
Fed funds, short rates, Treasury yields, curve variables
Monetary tightness and rate pressure
Growth
GDP, industrial production, retail sales, PMIs, ISM, leading indicators
Recession and demand stress when growth weakens
Labor
unemployment, jobless claims, payrolls, hours worked
Labor-market cooling and recession confirmation
Housing
permits, starts, sales, prices, builders’ survey
Rate-sensitive domestic demand stress
External trade
exports, imports, trade/current account balances
Global demand and external vulnerability
Surveys
consumer confidence, business confidence, sentiment surveys
Soft-data warnings and expectations pressure
The design uses both hard data and soft data. Hard data, like payrolls or industrial production, is slower but concrete. Soft data, like surveys, often moves earlier but can be noisy. Combining both helps the index respond to regime changes without letting one noisy survey dominate the whole system.
The Macro Blocks as a Regime Map
Before we build the first block, it helps to think of the seven blocks as a regime map rather than a list of variables.
Inflation and policy describe the nominal pressure side of the economy. Growth, labor, and housing describe the real activity side. External trade adds the global demand and currency-sensitive side. Macro breadth/conflict describes whether the pressure is localized or spreading.
A few common macro regimes can be described in this block language:
Regime
Inflation block
Policy block
Growth/labor blocks
Market interpretation
Goldilocks
low
low
low stress
equities supported, rates less threatening
Classic recession
low or falling
falling
high stress
bonds may help, defensives usually improve
Inflation shock
high
rising/high
mixed
commodities may help, bonds can fail
Stagflation
high
high
high stress
difficult for both equities and bonds
Liquidity crisis
mixed
high or unstable
rising stress
risky assets and credit usually reprice
This framing is important because the strategy later doesn’t only ask whether conditions are tight. It also asks what kind of tightness we have.
Inflation enters asset pricing through several channels at once.
First, it changes the discount rate because central banks raise policy rates when inflation is above target. Second, it changes real purchasing power and can compress household demand. Third, it changes profit margins because firms may face higher input costs before they can pass those costs through to consumers. Fourth, it changes the bond-equity correlation structure because bonds stop behaving like a clean recession hedge when inflation is the shock.
The level tells us whether inflation is high. The impulse tells us whether inflation has recently moved upward. The acceleration tells us whether the inflation impulse is speeding up.
Why Inflation Needs More Than One Measure
Inflation is not one object. CPI, core CPI, PCE, core PCE, PPI, import prices, export prices, and oil all measure different parts of the price system.
CPI is closer to the household cost-of-living basket. PCE is closer to the Federal Reserve’s preferred consumption deflator. Core versions remove food and energy to reduce noise, but they can lag energy-driven shocks. PPI measures producer input and output prices, so it can show cost pressure before it appears in consumer prices. Import prices connect inflation to the dollar and global supply chains. Oil captures a high-volatility but economically important input cost.
A broad inflation block prevents one inflation measure from dominating the story. If oil spikes but core inflation is stable, the diffusion component won’t confirm broad pressure. If CPI, core CPI, PCE, PPI, import prices, and oil all move together, diffusion rises and the FCI treats the inflation regime as more serious.
For asset allocation, broad inflation is more dangerous than narrow inflation. Narrow inflation can rotate between sectors. Broad inflation can force central bank tightening and change the entire discount-rate environment.
From the inflation plots, the strongest inflation-pressure episode is clearly the post-2021 inflation shock. The level pressure rises sharply and stays elevated for a long period, which matches the broad macro story after the pandemic: supply bottlenecks, energy pressure, fiscal demand, and later tighter labor markets.
The latest table is also informative. In January 2026, inflation level pressure is about 1.86, which means inflation-related variables are still high relative to their own history. The latest inflation impulse is about 0.76, and inflation acceleration is about 0.65, so the latest reading is not just a high-level story. There is also some renewed upward movement in the inflation block.
The diffusion component is negative at about -0.28. That tells us the pressure is not broad in the same way it was during the worst part of the 2021–2022 inflation episode. This distinction matters for markets. A high inflation level with low diffusion can mean inflation remains sticky in some components, while a high level with high diffusion signals a more dangerous broad price regime.
Inflation Regime Interpretation for Assets
Inflation affects sectors differently.
Energy and materials may benefit when inflation is commodity-driven because their revenues are directly linked to commodity prices. Gold can benefit when inflation reduces confidence in real purchasing power or when real rates are low. Financials can benefit from higher rates if the yield curve and credit cycle remain healthy, but they can suffer if policy tightening damages loan growth or credit quality.
Growth stocks usually dislike inflation-driven rate shocks because their valuations depend more heavily on long-dated expected cash flows. A higher discount rate hurts those distant cash flows more. If we write a simplified long-duration equity valuation as:
\[
P_t\approx\frac{CF_{t+H}}{(1+r)^H}
\]
then a change in \(r\) has a larger price effect when \(H\) is large. That is the duration logic behind growth-stock sensitivity to rates.
This is why the inflation block later supports inflation beneficiaries but penalizes some rate-sensitive sectors through the policy block. The model separates inflation pressure from policy pressure because markets don’t react to inflation alone. They react to inflation plus the central bank response.
6) Policy and Rate Pressure
Policy pressure measures the tightness of monetary conditions. The central bank affects markets through the short rate, but the market also prices policy through Treasury yields and the yield curve. A higher policy-rate block generally means financing is more expensive and duration-sensitive assets face more pressure.
The basic policy tightness signal is an average of standardized rate-related variables:
This captures the market impact of a fast policy move. A rate level can be high but stable. A rate shock means investors are being forced to reprice discount rates quickly.
The real-rate squeeze combines policy pressure with inflation impulse:
When this is high, policy is tight relative to recent inflation momentum. When it is low or negative, inflation is moving faster than the policy block, so real tightening may be less forceful.
Policy Catch-Up Risk
The policy catch-up component tries to detect periods where inflation is high but policy has not fully caught up. It is based on the positive gap:
\[
g_t=\max(I_t^{level}-P_t^{tight},0)
\]
and then this gap is standardized:
\[
\text{catchup}_t=z(g_t)
\]
The logic is simple. If inflation pressure is high and policy tightness is still low, the market may eventually price future hikes or a delayed tightening campaign. That is a dangerous setup for long-duration bonds and expensive growth equities.
The latest policy table shows a different situation from the 2022 tightening shock. Policy tightness is only around 0.28, policy shock is slightly negative, and policy catch-up risk is around -0.81. This says rate pressure is no longer in the active shock phase. The latest stress is more about inflation still being elevated than about policy rapidly tightening.
That difference matters for sector interpretation. If policy pressure were strongly positive, rate-sensitive assets like utilities, real estate, and long-duration growth would face a direct discount-rate headwind. With policy pressure easing, the macro issue becomes more about sticky inflation and whether growth can absorb it.
Real Rates and the Policy Transmission Channel
A policy rate is a nominal rate. What matters for spending and investment is often the real rate, roughly nominal rate minus expected inflation:
\[
r_t^{real}\approx i_t-\pi_t^e
\]
where \(i_t\) is the nominal policy rate and \(\pi_t^e\) is expected inflation. When real rates rise, borrowing becomes expensive after inflation, and financial conditions tighten. Housing, private investment, leveraged companies, and long-duration assets become more vulnerable.
The project doesn’t directly estimate expected inflation. Instead, it uses the relationship between policy tightness and inflation impulse as a practical signal. If policy tightness is high relative to current inflation impulse, the real-rate squeeze becomes positive. If inflation impulse is running ahead of policy, the squeeze becomes lower.
This is a simplified macro proxy, but it captures a useful idea: the same nominal policy rate can mean different things depending on inflation momentum. A 5% policy rate with collapsing inflation is tight. A 5% policy rate with inflation accelerating can feel less restrictive in real terms until policy catches up.
Policy pressure enters sector returns through both funding costs and discount rates.
Utilities and real estate often behave like bond proxies because they have stable cash flows and high sensitivity to yields. When rates rise, their dividend yields become less attractive and their debt costs rise. Technology and consumer discretionary can also be rate-sensitive because a large share of their value depends on future growth expectations.
Banks and financials are more complicated. Higher rates can increase net interest margins, but if rates rise during a growth slowdown, credit losses and funding stress can dominate. That is why the strategy later applies an extra penalty to XLF when policy pressure and growth recession stress are both positive:
The multiplication matters. Policy pressure alone is not always bad for banks. Growth stress alone is not always enough. The combination is dangerous because loan demand weakens, credit losses rise, and funding conditions tighten together.
7) Growth and Recession Signals
Growth data is sign-flipped because weak growth is the stress state. If a growth variable \(g_t\) is high relative to history, its raw z-score is positive, but its stress score is negative:
\[
s_t^{growth}=-z(g_t)
\]
This means positive growth stress corresponds to falling or weak growth, while negative growth stress corresponds to strong growth.
The growth block includes hard activity data such as production and sales, plus survey and leading-indicator information. We build:
and growth breadth stress from the share of growth indicators that are weak. If many indicators are simultaneously below their own trend, the breadth signal rises.
This group is critical because inflation and policy stress do not always become equity stress unless growth deteriorates. A high-rate environment with resilient growth can still support cyclicals and earnings. A high-rate environment with collapsing growth is much more dangerous.
Hard Data Versus Soft Data
Growth analysis becomes more useful when we separate hard and soft data.
Hard data includes industrial production, retail sales, payrolls, and real activity measures. These variables describe actual spending and production. They are slower, sometimes revised, and often confirmed after the economy has already moved.
Soft data includes surveys such as consumer confidence, business confidence, PMIs, and sentiment measures. These variables describe expectations and reported conditions. They can move earlier, but they can also overreact to news, politics, inflation frustration, or media attention.
The project includes both because markets respond to both. Soft data can move valuations and risk appetite quickly. Hard data confirms whether the earnings and labor consequences are actually happening.
A useful interpretation rule is this: when hard data and soft data agree, the macro signal is stronger. When they disagree, the regime is mixed. In the latest US output, hard growth stress is negative, but survey warning is strongly positive. That is exactly a mixed-growth regime.
The latest growth table gives a very useful mixed signal. Growth momentum stress is around -1.33 and growth breadth stress is around -1.28, which means the hard growth data is still strong relative to its historical distribution. Growth acceleration stress is close to zero, so the immediate hard-data deterioration is limited.
But the survey warning is around 2.00 in January 2026 and was even higher in late 2025. This says soft data is much more pessimistic than hard activity data. That kind of gap is common around turning points: surveys react quickly to uncertainty, inflation frustration, politics, financial tightening, or business caution, while hard data may remain stable until spending and hiring actually change.
For portfolio use, this means we shouldn’t label the environment as a simple recession regime. Hard data is not showing strong recession stress. Surveys are warning that expectations are fragile. A strategy that uses only realized growth data may stay too risk-on, while a strategy that uses only surveys may become too defensive.
Growth Stress and Earnings
Growth stress matters because equity prices ultimately depend on future earnings. At a broad level:
Growth weakness hits revenues first. If companies sell fewer goods or services, revenue expectations fall. If demand weakens while costs remain high, margins compress too. This is why growth recession stress can affect both cyclical sectors and broad equity beta.
Cyclicals like industrials, discretionary, materials, and financials are usually most exposed to growth stress. Defensive sectors like staples and health care have more stable demand, so they tend to hold up better when growth deteriorates.
The strategy later reflects this relation. Cyclical sectors receive support when FCI percentile is low and growth stress is low. Defensive sectors receive support when FCI percentile or growth stress rises. That is not arbitrary sector timing; it comes directly from the macro cash-flow channel.
8) Labor Cooling and Sahm Pressure
The labor market is often the most important confirmation layer in macro analysis. Recessions usually become obvious when labor deteriorates. Equity markets may fall before unemployment rises, but once unemployment and claims move, the recession signal becomes much more reliable.
The labor block combines three types of information:
Unemployment level, where higher unemployment is stress.
Claims, where higher initial or continuing claims are stress.
Jobs and hours, where weak payrolls or hours are stress, so the sign is flipped.
The combined labor cooling signal can be written conceptually as:
The project also computes a Sahm-style pressure variable. The Sahm rule compares the recent unemployment rate with its recent low. A simplified version is:
where \(u_t\) is unemployment. The project standardizes this gap rather than using one fixed recession threshold. Rising Sahm pressure means unemployment has moved up enough relative to its recent low to look economically important.
The labor plots show the large labor stress events clearly: the 2008 crisis, the COVID shock, and smaller labor-cooling periods. The latest values are not recession-like. Labor cooling is around -0.92, and Sahm pressure is slightly negative around -0.08.
That means the labor block is currently supporting the idea that the economy is not in a broad labor-market downturn. The labor market may be cooling at the margin in some recent months, but the aggregate signal is not close to the kind of stress seen in 2008 or 2020.
This is economically important because labor is the bridge between macro data and earnings. If labor is stable, households still have income, defaults stay contained, and consumer sectors can keep spending support. If labor breaks, the macro environment becomes much more dangerous for equities, especially financials, discretionary, industrials, and credit-sensitive exposures.
Labor as a Regime Confirmation Variable
Labor data is often late, but it is powerful. Markets can sometimes ignore weak surveys if employment stays strong. It is harder to ignore rising unemployment and rising claims because those variables directly affect income, defaults, and consumption.
Labor also changes the central bank reaction function. If inflation is high and labor is strong, the central bank has more room to stay tight. If inflation is falling and labor is weakening, policy can shift toward easing. This is why labor matters for both cash flows and rates.
The Sahm-style variable is useful because it focuses on changes from a recent low rather than the absolute unemployment rate. A country can have a low unemployment rate and still enter recession if unemployment has risen sharply from its trough. The gap formulation catches that turning-point behavior.
In the latest US data, Sahm pressure is not elevated. That weakens the recession case even though surveys are warning. The labor block is therefore a stabilizing input in the latest macro state.
9) Housing, Domestic Demand, and External Trade
Housing is rate-sensitive and forward-looking. Mortgage rates affect affordability quickly, and housing activity often weakens before the broader economy. The housing block combines the level and impulse of housing indicators, then adds a rate-squeeze component.
This catches the situation where housing is weakening while rates are still restrictive. That mix can be especially painful because housing is a large transmission channel for monetary policy.
External trade is separate. External demand stress captures weakening exports and imports, while external vulnerability captures pressure from trade or current-account balances. For a large domestic economy like the US, external trade may be less dominant than for Canada, but it still matters through global demand, dollar strength, commodities, and multinational earnings.
The housing plots show a strong stress episode during the housing bubble collapse and the 2008 crisis. That is exactly what we want: housing stress should become visible before and during the financial crisis.
The latest housing values are mostly negative: housing impulse stress is around -0.39 and housing rate squeeze is around -0.05. This says housing is not currently driving the broad macro stress index in the same way it did before 2008. Rate pressure exists, but the combined housing stress block is not elevated in the latest reading.
External demand stress is also negative in the latest table, around -0.50, while external vulnerability is slightly negative. So the latest US macro picture is not one of trade-driven weakness either. For the US section, the main current pressure is inflation, while growth, labor, housing, and external trade are not broadly stressed.
Housing and Trade as Transmission Channels
Housing is one of the main channels through which rates affect the real economy. Higher mortgage rates reduce affordability, lower transaction volumes, pressure construction activity, and can eventually affect employment in construction, real estate services, home improvement, and credit.
where \(i_m\) is the monthly mortgage rate and \(n\) is the number of monthly payments. When the mortgage rate rises, the payment rises nonlinearly for a fixed loan size. That is why housing can weaken quickly when rates jump.
External trade matters through global demand and currency channels. A strong dollar can pressure exporters and emerging-market demand, while weak global demand can reduce revenues for multinational firms. For Canada, external trade is even more central because commodities and US demand play a larger role.
These blocks are not always the largest drivers, but they give the FCI more economic coverage than an index built only from inflation, rates, and unemployment.
10) Breadth, Severe Breadth, Stagflation, and Goldilocks
After building the first-level signals, we build aggregate macro-state signals. These are not tied to one specific economic category. They measure how stress is distributed across the whole macro system.
The stress breadth signal begins with the share of first-level signals above a stress threshold:
This breadth logic is useful because regimes differ by concentration. A single inflation pressure variable above one standard deviation is not the same as inflation, policy, labor, housing, and growth all above one standard deviation.
The stagflation pressure signal averages the main conflict variables:
High stagflation pressure means inflation and rates are high while growth is weak. That is one of the worst environments for balanced portfolios because equities face earnings pressure and bonds face inflation/rate pressure.
Goldilocks support is the opposite type of macro environment. It is built from inflation pressure, policy pressure, growth stress, and labor cooling:
Higher goldilocks support means inflation is not pressuring policy, rates are not tight, growth stress is low, and labor is healthy. In that environment, risk assets usually get support because earnings can grow while discount-rate pressure is limited.
The latest breadth table shows stress breadth is negative, severe stress breadth is negative, and stagflation pressure is only around 0.27. Goldilocks support is near zero. The interpretation is a mixed but contained macro environment: inflation is still high, but stress is not broad across the whole economy.
That is exactly the type of state where a macro model can help. Pure momentum might keep buying what has worked. Pure defensive allocation might overreact to inflation. A block-level FCI can say: inflation pressure exists, but broad recession stress is not yet confirmed.
where the blocks are inflation pressure, policy/rate pressure, growth recession, labor cooling, housing/domestic demand, external trade, and macro breadth/conflict.
Each block is an average of related signals. For example:
The same logic applies to the other groups. Averaging reduces noise because one macro variable can be revised, delayed, or distorted. The block is meant to capture the common stress inside a category.
A block-level FCI has two advantages:
It remains interpretable. We can explain whether the index is high because of inflation, labor, growth, or housing.
It becomes statistically more stable. PCA, PLS, and logistic models work better on seven economic blocks than on dozens of noisy raw macro variables.
After coverage filtering, the analysis starts in December 1994 and runs to January 2026, giving 374 monthly observations. All seven blocks are available across the analysis sample, which is important because the FCI models later compare block combinations through time.
The availability table shows that most first-level signals and all block variables have full coverage inside the analysis window. Some inputs like policy catch-up risk, stress breadth, and severe stress breadth have fewer observations because they need additional warmup history. But once blocks are formed, the final macro state is complete.
This is a strong design choice for macro work. We let raw-series availability be uneven, but we require enough block coverage before the FCI is considered valid. That avoids making early-sample FCIs from only one or two macro categories.
Coverage Scores and Why We Keep Blocks Stable
Coverage is a quiet but important part of macro modeling. If a macro index changes because the economy changed, that is useful. If it changes because the available variable set changed, that is a data artifact.
The analysis keeps rows where at least six blocks are available and enough raw series are present. This protects the FCI from early-sample distortion. If only inflation and policy were available in early years, the FCI would look like an inflation/rate index rather than a broad financial conditions index.
The stable block coverage from 1994 onward is therefore a major strength of the dataset preparation.
12) ETF and NFCI Inputs
The market data section loads US sector ETFs, defensive assets, and signal assets. The sector ETFs are used later for allocation. The defensive assets include cash, short/intermediate/long Treasuries, gold, and commodities. The signal assets include broad equity, growth equity, small caps, credit, and the dollar.
We don’t re-explain these ETFs here because the sector and cross-asset data has already appeared earlier. The important point is how they are used:
Sector ETFs let us test whether macro regimes line up with sector performance.
Defensive assets let the strategy reduce risky sector exposure when the FCI tightens.
Signal assets support the broader allocation environment, but the macro blocks are the main new information layer.
We also load the external NFCI series. It acts as a benchmark financial-conditions index. Comparing our index with NFCI is useful because it tells us whether our macro-built FCI agrees with an established external stress gauge during major crises.
nfci_raw = pd.read_csv(nfci_path)nfci_raw.columns = [str(col).lower() for col in nfci_raw.columns]nfci_raw["date"] = pd.to_datetime(nfci_raw["date"], errors="coerce")nfci = nfci_raw.dropna(subset=["date"]).set_index("date").sort_index().drop(columns=["date"], errors="ignore")nfci = nfci.apply(pd.to_numeric, errors="coerce")nfci_month = nfci.groupby(nfci.index.to_period("M")).last()nfci_month.index = nfci_month.index.to_timestamp("M")nfci_month = nfci_month.loc[nfci_month.index >= macro_start]
The ETF coverage table confirms that all US sector ETFs start in 1999 and run through June 2026. Defensive ETFs start later: SHY, IEF, TLT, and LQD begin around 2002, GLD begins in 2004, DBC starts in 2006, and HYG/UUP start in 2007.
That coverage pattern matters for allocation. The strategy can run sector-only from the late 1990s, but a full defensive sleeve with gold and commodities becomes available later. The project handles this by using available assets at each date and by reporting a separate full-defensive-window performance table.
Cash is included as a synthetic defensive asset using a monthly risk-free rate assumption. That matters because a macro strategy needs somewhere to go when risk is unattractive and bond/commodity ETFs are unavailable or unsuitable.
13) Block Correlations and Macro Structure
The block correlation heatmap checks whether the macro blocks are truly different. If every block were almost perfectly correlated, then a seven-block FCI would mostly be pretending to be diversified. If correlations are moderate and economically sensible, the blocks are adding different regime information.
The heatmap shows several meaningful relationships.
Inflation and policy pressure have a positive relationship, around 0.41. That makes sense: when inflation rises, policy and yields tend to rise. Growth recession and labor cooling are strongly related, around 0.71, because labor usually deteriorates when growth weakens. Growth recession and housing stress also have positive correlation, around 0.52, consistent with housing acting as a recession-sensitive domestic-demand channel.
External trade has weaker relationships with most domestic blocks. That is useful because it gives the FCI an external dimension instead of just repeating domestic growth and labor information. Macro breadth/conflict is positively related to several blocks because it measures whether stress is spreading.
The heatmap supports the block design. The blocks are connected enough to represent one macro system, but different enough to justify separate interpretation.
14) Recent Macro Blocks and Historical Conflict Peaks
The recent block table shows the last 12 months of the macro state. In January 2026, the largest positive block is inflation pressure, around 0.75. Policy pressure is negative, growth recession is negative, labor cooling is negative, housing stress is negative, external trade is negative, and macro breadth/conflict is slightly negative.
The recent picture is therefore not a broad stress regime. Inflation pressure remains the main concern, while the rest of the macro system is not confirming a recessionary shock.
The historical top macro-breadth/conflict readings are concentrated around 2000–2001 and 2008. That is exactly what we want from a breadth/conflict block. It becomes high when many macro categories start sending stress information at the same time. A high conflict/breadth reading is usually more important than a single block spike because it tells us the stress has spread across the macro system.
Show code
selected_dates = ["2008-10", "2020-03", "2022-09", str(macro.index.max().date())[:7]]contribution_rows = []for text in selected_dates: date = pd.to_datetime(text).to_period("M").to_timestamp("M") loc = macro.index.searchsorted(date, side="right") -1 date = macro.index[loc]for block, value in macro.loc[date, block_cols].items(): contribution_rows.append({"date": date, "block": block, "value": value})block_contribution_table = pd.DataFrame(contribution_rows).pivot(index="date", columns="block", values="value")display(block_contribution_table.round(2))
block
external_trade_block
growth_recession_block
housing_domestic_block
inflation_pressure_block
labor_cooling_block
macro_breadth_conflict_block
policy_rate_pressure_block
date
2008-10-31
0.47
1.66
0.71
-1.24
2.05
1.46
0.71
2020-03-31
-0.82
0.87
-0.13
-0.60
0.52
-0.17
-0.79
2022-09-30
-0.11
-0.01
-0.06
-0.18
-0.82
0.28
0.78
2026-01-31
-0.32
-0.17
-0.22
0.75
-0.50
-0.13
-0.28
15) Crisis Block Contributions
The crisis contribution table is one of the most useful macro diagnostics in the project because it explains why each stress period is different.
In October 2008, the stress is broad and classic recessionary. Labor cooling is very high at 2.05, growth recession is 1.66, macro breadth/conflict is 1.46, housing/domestic stress is positive, policy/rate pressure is positive, and external trade stress is positive. Inflation pressure is negative because the crisis was deflationary rather than inflationary.
In March 2020, growth recession is positive and labor cooling is positive, but policy pressure is strongly negative because policy was easing aggressively. Inflation pressure is also negative. This matches the COVID shock: a sudden real-economy collapse with emergency policy support.
In September 2022, policy/rate pressure is the main positive block at 0.78, while the growth and labor blocks are not strongly stressed. This captures the rate-shock nature of 2022. The macro pain came more from inflation and policy repricing than from a classic labor-led recession.
In January 2026, inflation pressure is positive, but most other blocks are negative. The macro condition is mixed rather than crisis-like.
Why We Need More Than One FCI
A macro index is a model, and every model has a bias.
An economic-weighted index is stable and explainable, but its weights are chosen before seeing the data relationship. A PCA index is data-driven and long-history friendly, but it chases the largest common variation, which may not always be the most market-relevant variation. A supervised PLS index targets future stress, but it can overfit the few crisis episodes in the sample. A logistic stress probability gives a clean high-risk interpretation, but it compresses the target into a binary label.
Using several FCIs lets us see whether the macro conclusion is robust. If all models say conditions are tight, the message is strong. If only supervised models say tight while economic and PCA models are neutral, the interpretation becomes more cautious.
This multi-model design is especially important in macro because we only have a few true crisis episodes. A model that looks excellent on three recessions may simply be memorizing the structure of those three events. Comparing model families helps reduce that risk.
16) The FCI Modeling Layer
Once the seven blocks are built, we create multiple FCI models. Each model answers a different statistical question.
where each block is stress-oriented. We want a scalar index:
\[
F_t=f(B_t)
\]
where higher \(F_t\) means tighter conditions.
The project builds five versions:
Model
Main idea
What it can do well
Economic FCI
Weighted block average
Interpretable and stable
PCA FCI
First common statistical factor
Finds the dominant co-movement in blocks
PLS FCI
Supervised factor targeting future stress
Learns which block combinations predict market stress
Probability FCI
Logistic probability of high future stress
Produces a stress-likelihood style signal
Blend FCI
Weighted mixture of PLS, PCA, and economic FCI
Combines supervised and unsupervised information
We don’t pick one model immediately. We compare them through future stress tests, crisis behavior, and external NFCI validation.
Rolling Percentiles
The raw FCI value is useful, but the percentile is often easier to interpret. A value of \(F_t=1.2\) means something only relative to the model’s own distribution. The rolling or expanding percentile asks:
where \(n_t\) is the number of past valid FCI observations. A percentile of 0.80 means the current FCI is tighter than about 80% of its own history up to that point.
This matters for allocation. We can use the percentile to gate risk:
Below 0.40: conditions are relatively easy or neutral.
Around 0.40 to 0.60: conditions are mixed.
Above 0.60 or 0.80: conditions are tight enough to justify reducing risky exposure.
Percentiles help compare different FCI models even when they have different raw scales.
17) Economic-Weighted FCI
The economic FCI is the most transparent model. We assign weights to the seven blocks and compute a weighted average:
The denominator is important because it handles missing block values. If one block is unavailable at some date, the model renormalizes the available weights instead of treating the missing value as zero.
The weights are economically motivated. Inflation, policy, and growth receive the largest weights because they are the primary macro drivers of discount rates, earnings, and risk appetite. Labor, housing, external trade, and macro breadth/conflict receive smaller but still meaningful weights.
The advantage is interpretability. If the economic FCI rises, we know exactly which blocks contributed. The weakness is that the weights are chosen by economic reasoning rather than estimated from data, so it may miss relationships that change across regimes.
This score is then sign-aligned so that higher values correspond to higher average stress. Without sign alignment, PCA can flip signs because both \(v_1\) and \(-v_1\) explain the same variance.
Financially, PCA is asking: what is the dominant common macro pressure running through the blocks? During a broad crisis, many blocks move together, so PC1 can become a clean stress factor. During a narrow shock, like inflation without labor stress, PCA may be less aggressive because not all blocks agree.
PCA Loadings Intuition Without Overcomplicating It
Even when we don’t print loadings in this section, the PCA score has a clear interpretation. The first principal component is a weighted average of standardized blocks:
Each loading \(v_{1,j}\) tells us how much block \(j\) contributes to the dominant common macro factor. If crisis periods make growth, labor, housing, and breadth move together, PCA will often assign similar signs to those blocks. If inflation behaves differently from classic recession blocks, its loading can be smaller or even different depending on the sample.
The sign alignment step compares the historical PCA score with average stress. If the correlation is negative, we multiply the score by \(-1\). This doesn’t change the statistical content, but it makes interpretation consistent: higher PCA FCI means tighter conditions.
This is one of the cleanest uses of PCA in the project series. We are not trying to forecast with dozens of components. We are extracting the first macro stress factor and checking whether it lines up with crises and future risk.
Show code
def pca_index(blocks, min_obs=60, min_cols=6): out = pd.Series(np.nan, index=blocks.index, name="fci_pca") average_stress = blocks.mean(axis=1, skipna=True)for date in blocks.index: hist = blocks.loc[:date] good_cols = [col for col in blocks.columns if hist[col].notna().sum() >= min_obs] row = blocks.loc[date, good_cols] good_cols = [col for col in good_cols if np.isfinite(row.get(col, np.nan))]iflen(good_cols) < min_cols:continue sample = hist[good_cols].dropna(how="any")iflen(sample) < min_obs:continue scaler = StandardScaler() train = scaler.fit_transform(sample) model = PCA(n_components=1) model.fit(train) current = scaler.transform(blocks.loc[[date], good_cols]) score =float(model.transform(current)[0, 0]) hist_score = pd.Series(model.transform(train)[:, 0], index=sample.index) align = pd.concat([hist_score, average_stress.reindex(sample.index)], axis=1).dropna()iflen(align) >12and align.iloc[:, 0].corr(align.iloc[:, 1]) <0: score =-score out.loc[date] = scorereturn zscore(out, 12).rename("fci_pca")fci_pca_full = pca_index(macro_full[block_cols], 36, 6)fci_pca = fci_pca_full.reindex(macro.index)
19) The Future Stress Target
The supervised models need a target. We define future stress from the next three months of SPY behavior. The target combines three pieces:
Future 3-month return, with losses treated as stress.
Future 3-month volatility.
Future 3-month maximum drawdown, with deeper drawdowns treated as stress.
High \(Y_t\) means future returns were weak, future volatility was high, and future drawdowns were severe. This is the market outcome we want the FCI to anticipate.
Why the Target Combines Return, Volatility, and Drawdown
A future stress target based only on return would miss important market states. Sometimes returns are mildly positive but volatility is high. Sometimes returns recover by month-end, but the path includes a severe intra-window drawdown. A risk manager cares about the path, not only the endpoint.
That is why the target combines three dimensions.
The return term captures realized direction. The volatility term captures uncertainty and choppiness. The drawdown term captures path pain. Together, they describe the kind of future environment that a macro FCI should warn about.
For example, if SPY loses 2% over three months with low volatility and shallow drawdown, the stress target may only be mildly elevated. If SPY ends flat but falls 8% inside the window and volatility spikes, the target can still be high. That makes the supervised FCI more aligned with portfolio risk than a simple next-return forecast.
Embargo and Lookahead Control
When we train supervised FCI models, we need to avoid leaking future information. The target uses returns from the next three months, so we apply an embargo. At date \(t\), training only uses observations whose target window ends before the current date.
Conceptually, for a current month \(t\), the training sample is:
\[
\mathcal{T}_t=\{s: s<t-3\text{ months}\}
\]
This avoids using a target that overlaps with the period we are trying to predict.
This detail is important because macro signals are slow moving. Without an embargo, nearby observations can share future return windows and make the supervised model look more predictive than it really is. The project is careful about this by training PLS and logistic probability models with a three-month gap.
Supervision Changes the Meaning of the Index
The unsupervised FCIs, economic and PCA, summarize macro conditions themselves. The supervised FCIs, PLS and probability, summarize macro conditions through their historical relationship with future market stress. That changes the meaning of the number.
If a supervised FCI is high, it doesn’t necessarily mean every macro block is high. It means the current combination of blocks resembles historical combinations that were followed by high future stress. A moderate inflation block, weak survey data, and a certain policy configuration may be more predictive than a single extreme block.
Mathematically, the supervised index is closer to a fitted conditional expectation:
\[
F_t^{sup}\approx E[Y_t\mid B_t]
\]
where \(Y_t\) is the future stress target. The unsupervised index is closer to a common macro-state score:
\[
F_t^{unsup}=g(B_t)
\]
This distinction explains why the latest PLS and blend readings can be tighter than the economic and PCA readings. The broad macro state is not strongly stressed, but the supervised relationship sees a higher-risk combination.
Partial Least Squares, or PLS, is a supervised dimension-reduction method. PCA finds the direction in macro blocks with the most variance. PLS finds a direction in macro blocks with the strongest relationship to the target.
Let \(X\) be the standardized block matrix and \(y\) be the future stress target. A one-component PLS model finds a weight vector \(w\) such that the latent factor:
\[
t=Xw
\]
has high covariance with \(y\). A simplified objective is:
after the PLS transformation and regression steps.
The financial interpretation is direct: PLS tries to learn which combination of macro blocks historically came before worse future SPY stress. It may put more weight on blocks that matter for asset markets, even if those blocks are not the largest source of macro variation.
The risk is sample dependence. A supervised model can overfit a few crisis patterns, especially when the number of macro cycles is small. That is why we later score the models across full-sample, common-sample, and test windows.
Show code
def pls_index(blocks, target, min_obs=60, min_cols=6, embargo_months=3): out = pd.Series(np.nan, index=blocks.index, name="fci_pls") target = pd.Series(target, dtype=float)for date in blocks.index: cutoff = pd.Timestamp(date) - pd.offsets.MonthEnd(embargo_months) hist_x = blocks.loc[blocks.index < cutoff] hist_y = target.reindex(hist_x.index) good_cols = [col for col in blocks.columns if hist_x[col].notna().sum() >= min_obs] row = blocks.loc[date, good_cols] good_cols = [col for col in good_cols if np.isfinite(row.get(col, np.nan))]iflen(good_cols) < min_cols:continue train = pd.concat([hist_x[good_cols], hist_y.rename("target")], axis=1).dropna()iflen(train) < min_obs:continue scaler = StandardScaler() train_x = scaler.fit_transform(train[good_cols]) components =min(2, len(good_cols), len(train) -1) model = PLSRegression(n_components=components) model.fit(train_x, train["target"].to_numpy(dtype=float)) current = scaler.transform(blocks.loc[[date], good_cols]) prediction = np.asarray(model.predict(current), dtype=float).reshape(-1) out.loc[date] =float(prediction[0])return zscore(out, 36).rename("fci_pls")fci_pls_full = pls_index(macro_full[block_cols], target_table["future_stress"], 36, 6, 3)fci_pls = fci_pls_full.reindex(macro.index)
21) Stress-Probability FCI
The stress-probability model turns the future stress target into a binary event. The event is defined as future stress being above a rolling high-stress threshold, usually the 75th percentile of historical target values.
The output is a probability between 0 and 1. A high value means the macro block state looks similar to historical states that were followed by high market stress.
We then standardize the probability through time so it can be compared with the other FCIs:
\[
F_t^{prob}=z(P(Y_t^{bin}=1\mid B_t))
\]
This model is appealing because it matches how risk managers often think: the question is not only expected return, but whether we are entering a high-stress zone. The weakness is that binary labels discard information. A mildly bad future and an extremely bad future can both become the same class if they are above the threshold.
Show code
def prob_index(features, target, min_obs=60, embargo_months=3): x = features.apply(pd.to_numeric, errors="coerce").replace([np.inf, -np.inf], np.nan) y = pd.Series(target, dtype=float).reindex(x.index) threshold = y.expanding(min_periods=min_obs).quantile(0.75).shift(1) label = (y > threshold).where(threshold.notna()) out = pd.Series(np.nan, index=x.index, name="fci_prob")for date in x.index: current = x.loc[date].dropna() cutoff = pd.Timestamp(date) - pd.offsets.MonthEnd(embargo_months) history = x.loc[x.index < cutoff] cols = [col for col in current.index if history[col].notna().sum() >= min_obs]iflen(cols) <4:continue train = pd.concat([history[cols], label.loc[history.index].rename("label")], axis=1).dropna()iflen(train) < min_obs or train["label"].nunique() <2:continue scaler = StandardScaler() train_x = scaler.fit_transform(train[cols]) model = LogisticRegression(C=1.0, max_iter=1000, solver="lbfgs") model.fit(train_x, train["label"].astype(int)) current_x = scaler.transform(x.loc[[date], cols]) out.loc[date] =float(model.predict_proba(current_x)[0, 1])return outprob_features = pd.concat([macro_full[block_cols], fci_econ_full, fci_pca_full, fci_pls_full], axis=1)fci_prob_full = prob_index(prob_features, target_table["future_stress"], 36, 3)fci_prob_z_full = zscore(fci_prob_full, 12).rename("fci_prob")fci_prob_z = fci_prob_z_full.reindex(macro.index)
The weights give the largest role to PLS because it is explicitly targeted to future stress. PCA receives a meaningful weight because it captures the common macro factor without needing a market target. The economic index receives a smaller but important weight because it is stable and interpretable.
This blend is a compromise between prediction and robustness. Supervised models can be more responsive to market stress, but they can also overfit. Unsupervised models can have better long-history behavior, but they may ignore the exact combination that matters for future equity stress. A blend gives the strategy more than one source of information.
The model availability table shows the expected pattern. Economic FCI starts earliest, PCA starts after enough block history is available, and the supervised models start later because they need both macro history and target history.
The FCI plot compares all five models. They agree strongly around major crisis periods, but they diverge in calmer or mixed regimes. That divergence is useful information rather than a problem to hide.
The latest model table gives a very clear split. In January 2026:
fci_econ is neutral, with value about -0.18 and percentile around 0.40.
fci_pca is also neutral, with value about -0.34 and percentile around 0.42.
fci_pls is tight, with percentile around 0.82.
fci_prob is neutral, around percentile 0.46.
fci_blend is tight, around percentile 0.83.
The unsupervised models say broad macro stress is not high. The supervised PLS and blend are more cautious, likely because the combination of sticky inflation and survey weakness resembles past market-risk setups more than the broad blocks alone suggest. That difference matters later when choosing which index should control an allocation strategy.
Model Agreement and Disagreement
The latest FCI readings show a useful kind of model disagreement. Economic and PCA indexes are neutral, while PLS and blend are tight. This means the broad macro blocks are not screaming crisis, but the supervised relation to future stress is more cautious.
A portfolio manager can use that disagreement in several ways:
If all indexes are high, reduce risk with more confidence.
If only supervised indexes are high, watch for market fragility but avoid an extreme risk-off move.
If only unsupervised indexes are high, the macro environment may be broadly stressed even if the specific equity-stress target has not learned that pattern strongly.
If all indexes are low, macro conditions are likely supportive or at least not restrictive.
This project later selects PCA for the main strategy because it has long history and avoids supervised model-selection lookahead. But the latest model table still provides useful diagnostic context. It tells us that the current macro regime has some supervised stress features despite neutral broad stress.
A good FCI should have a monotonic relationship with future market stress. We test this by sorting each FCI into quintiles and checking future SPY outcomes.
For each model, we rank the FCI values and place them into five groups:
\[
q_t \in \{Q1,Q2,Q3,Q4,Q5\}
\]
where \(Q1\) means easiest conditions and \(Q5\) means tightest conditions.
Then we compute the average future 1-month return, future 3-month return, future volatility, future max drawdown, and future stress target inside each quintile.
The clean pattern we want is:
higher FCI quintiles have lower future returns,
higher FCI quintiles have higher future volatility,
higher FCI quintiles have deeper future drawdowns,
the future stress target rises from low quintiles to high quintiles.
This is a practical validation test. It doesn’t require the model to predict every month correctly. It checks whether the distribution of future outcomes worsens when macro conditions are tighter.
Show code
def quintile_report(fci, target): data = pd.concat([fci.rename("fci"), target], axis=1).dropna(subset=["fci", "future_stress"])iflen(data) <30:return pd.DataFrame() ranks = data["fci"].rank(method="first") data["fci_quintile"] = pd.qcut(ranks, 5, labels=["q1", "q2", "q3", "q4", "q5"]) report = data.groupby("fci_quintile", observed=False)[["future_1m_return", "future_3m_return", "future_3m_volatility", "future_3m_max_drawdown", "future_stress"]].mean() report["observations"] = data.groupby("fci_quintile", observed=False).size()return reportquintile_reports = {name: quintile_report(fci_models[name], target_table) for name in fci_models.columns}for name, report in quintile_reports.items():print(name) display(report.round(4))
fci_econ
future_1m_return
future_3m_return
future_3m_volatility
future_3m_max_drawdown
future_stress
observations
fci_quintile
q1
0.0119
0.0256
0.1046
-0.0216
-0.5124
58
q2
0.0141
0.0426
0.1177
-0.0167
-0.7198
58
q3
0.0114
0.0360
0.1367
-0.0260
-0.3345
57
q4
0.0064
0.0210
0.1166
-0.0233
-0.7064
58
q5
-0.0003
0.0066
0.1401
-0.0381
0.0958
58
fci_pca
future_1m_return
future_3m_return
future_3m_volatility
future_3m_max_drawdown
future_stress
observations
fci_quintile
q1
0.0128
0.0232
0.0984
-0.0229
-0.5191
58
q2
0.0083
0.0359
0.1181
-0.0170
-0.6656
58
q3
0.0136
0.0331
0.1137
-0.0229
-0.6676
57
q4
0.0067
0.0303
0.1281
-0.0218
-0.5638
58
q5
0.0021
0.0092
0.1570
-0.0411
0.2331
58
fci_pls
future_1m_return
future_3m_return
future_3m_volatility
future_3m_max_drawdown
future_stress
observations
fci_quintile
q1
0.0090
0.0294
0.1263
-0.0228
-0.4229
43
q2
0.0131
0.0360
0.1162
-0.0185
-0.6199
43
q3
0.0070
0.0181
0.1292
-0.0324
0.0937
43
q4
0.0175
0.0461
0.1456
-0.0220
-0.2839
43
q5
0.0047
0.0224
0.1442
-0.0350
0.2608
43
fci_prob
future_1m_return
future_3m_return
future_3m_volatility
future_3m_max_drawdown
future_stress
observations
fci_quintile
q1
0.0136
0.0309
0.1161
-0.0209
-0.6073
41
q2
0.0084
0.0346
0.1237
-0.0189
-0.5463
40
q3
0.0110
0.0342
0.1152
-0.0211
-0.5351
41
q4
0.0179
0.0503
0.1231
-0.0192
-0.7261
40
q5
0.0143
0.0387
0.1534
-0.0291
0.0949
41
fci_blend
future_1m_return
future_3m_return
future_3m_volatility
future_3m_max_drawdown
future_stress
observations
fci_quintile
q1
0.0152
0.0447
0.1278
-0.0199
-0.7383
41
q2
0.0115
0.0262
0.1104
-0.0215
-0.5882
41
q3
0.0091
0.0325
0.1007
-0.0186
-0.7017
40
q4
0.0120
0.0269
0.1329
-0.0261
-0.0404
41
q5
0.0189
0.0635
0.1578
-0.0225
-0.3508
41
The quintile reports show that the relationship is present, but not perfectly monotonic for every model. That is normal for macro data. Macro conditions shape probabilities, not exact next-month outcomes.
For the economic FCI, the tightest quintile has weaker future returns and worse future drawdowns than easier quintiles. In the fci_econ report, the tightest quintile has a future 3-month return around 0.66%, compared with 2.56% in the easiest quintile and 4.26% in the second quintile. Its future max drawdown is also worse, around -3.81%.
For the PCA FCI, the tightest quintile has higher future volatility, around 15.7%, and the worst future max drawdown, around -4.11%. That supports the idea that the unsupervised common macro factor contains risk information.
The supervised models have mixed shapes. PLS and probability capture some high-stress behavior, but the sample is shorter and the relationship can become uneven across quintiles. The blend is especially interesting: the tightest quintile has high future volatility, but it also has strong future returns in this sample. That means a high blend signal may sometimes identify volatile upside environments, not only crash risk.
How to Read a Non-Monotonic Quintile Table
Macro validation rarely gives perfectly ordered quintiles. There are several reasons.
First, macro variables are slow. A high FCI can persist after the market has already sold off, so forward returns may rebound even though the FCI remains high. Second, policy reaction can offset stress. A tight or stressful macro state may trigger rate cuts, fiscal support, or liquidity provision. Third, financial markets price expectations before macro data confirms them. By the time a macro signal is extreme, some of the bad news may already be in prices.
So we shouldn’t demand that every column rises perfectly from Q1 to Q5. We look for distributional evidence: worse drawdowns, higher volatility, lower average future returns, and positive rank association with future stress. The model scoreboard does this by combining several diagnostics instead of relying on one table cell.
This is why fci_pca can win the full sample even with modest rank IC. It may not perfectly order every quintile, but it identifies the distributional deterioration around stress regimes and has long crisis coverage.
24) Model Scoreboard
The model scoring function turns the quintile validation into a compact ranking. For each FCI model, we compute several diagnostics.
The rank information coefficient measures whether higher FCI values are associated with higher future stress:
The drawdown spread compares future max drawdown in easy and tight regimes:
\[
\Delta DD=DD_{Q1}-DD_{Q5}
\]
Since drawdowns are negative, a positive spread means the tightest FCI quintile has a worse drawdown than the easiest quintile.
The volatility spread is:
\[
\Delta\sigma=\sigma_{Q5}-\sigma_{Q1}
\]
A positive value means future volatility is higher when conditions are tighter.
The monotonicity score checks whether future stress rises across quintiles in a roughly ordered way. The stability score penalizes overly noisy indexes:
The full-sample scoreboard selects fci_pca as the top model, with a final score around 0.80. It has the strongest drawdown spread and volatility spread in the full available sample, and it is long-history eligible because it starts in 1998.
The common-sample scoreboard changes the ranking. When all models are compared over the same later sample, fci_econ rises to the top with a final score around 0.81. This says the economic-weighted index holds up well when sample length is equalized.
The test-sample scoreboard, from February 2020 to January 2026, ranks fci_prob first with a final score around 0.89, followed by fci_econ around 0.79. The supervised probability model works well in the recent high-volatility period, but it has a shorter history.
For strategy design, the project selects fci_pca as the long-history strategy FCI. This is a conservative choice. It avoids choosing a supervised model mainly because it did well in the recent period, and it preserves coverage of the dot-com and 2008 regimes.
We compare the selected long-history FCI with the external NFCI benchmark. This is not a perfect apples-to-apples test because NFCI is built from financial variables, while our FCI is built mainly from macro blocks. Still, crisis agreement is useful.
The correlation between the selected FCI and NFCI is about 0.525. That is a meaningful positive relationship, especially because the inputs are different. It says the macro-built FCI captures a substantial part of broad financial stress.
The crisis agreement table is more important than the correlation alone:
During the financial crisis, selected FCI mean is about 1.81, NFCI mean is about 2.36, and sign agreement is 1.0.
During the COVID crash, selected FCI mean is about 0.57, NFCI mean is about 0.04, and sign agreement is 1.0.
During the inflation/rate shock, selected FCI mean is about -0.21, NFCI mean is about -0.28, and sign agreement is 0.9.
The model captures classic credit/liquidity stress well. The inflation/rate shock is different because macro pressure can be high while broad financial stress indexes are not necessarily crisis-high.
Show code
sector_assets = us_sector_assets[:]defensive_sector_assets = [x for x in ["xlp", "xlu", "xlv"] if x in returns.columns]cyclical_sector_assets = [x for x in ["xlf", "xli", "xly", "xlb"] if x in returns.columns]inflation_assets = [x for x in ["xle", "xlb", "dbc", "gld"] if x in returns.columns]rate_sensitive_assets = [x for x in ["xlk", "xly", "tlt"] if x in returns.columns]strategy_assets =sorted(set(sector_assets + defensive_assets + ["spy"]))required_strategy_assets = sector_assets + ["spy", "cash"]asset_first_month = monthly_close[sector_assets + ["spy"]].apply(pd.Series.first_valid_index)strategy_start =max(macro_start, asset_first_month.max() + pd.offsets.MonthEnd(6))score_strategy_dates = fci_models.index.intersection(returns.index)score_strategy_dates = score_strategy_dates[score_strategy_dates >= strategy_start]strategy_split_date = score_strategy_dates[int(len(score_strategy_dates) *0.65)] iflen(score_strategy_dates) else pd.NaTstrategy_test_dates = score_strategy_dates[score_strategy_dates >= strategy_split_date]strategy_sample_scoreboard = score_fci_models(fci_models.reindex(strategy_test_dates), target_table.reindex(strategy_test_dates))strategy_sample_scoreboard["strategy_test_sample"] =Trueprespecified_strategy_fci_name = long_history_fci_namestrategy_fci_name = prespecified_strategy_fci_name if prespecified_strategy_fci_name in fci_models.columns else"fci_econ"best_fci_name = strategy_fci_namebest_fci_series = fci_models[best_fci_name].rename("best_fci")best_fci_percentile = rolling_percentile(best_fci_series, 60).rename("best_fci_percentile")best_fci_3m_change = best_fci_series.diff(3).rename("best_fci_3m_change")tradable_macro = macro.shift(1)allocation_features = tradable_macro[["inflation_pressure_block", "policy_rate_pressure_block", "growth_recession_block", "macro_breadth_conflict_block", "goldilocks_support"]].copy()allocation_features["best_fci_percentile"] = best_fci_percentile.shift(1)allocation_features["best_fci_3m_change"] = best_fci_3m_change.shift(1)allocation_features["best_fci_value"] = best_fci_series.shift(1)dominant_block_values = tradable_macro[block_cols].abs()dominant_macro_block = pd.Series(np.nan, index=dominant_block_values.index, dtype=object)has_dominant_block = dominant_block_values.notna().any(axis=1)dominant_macro_block.loc[has_dominant_block] = dominant_block_values.loc[has_dominant_block].idxmax(axis=1)allocation_features["dominant_macro_block"] = dominant_macro_blockallocation_features = allocation_features.dropna(subset=["best_fci_percentile", "best_fci_3m_change"])model_choice_table = pd.DataFrame({"full_sample_score_winner": [full_sample_scoreboard.index[0]],"common_sample_score_winner": [common_model_scoreboard.index[0]],"test_sample_score_winner": [test_model_scoreboard.index[0]],"selected_strategy_fci": [strategy_fci_name],"selection_rule": ["Pre-specified long-history unsupervised FCI selected to avoid supervised model-selection lookahead and preserve crisis coverage."],})display(strategy_sample_scoreboard.round(4))display(model_choice_table)display(pd.DataFrame({"long_history_fci": [long_history_fci_name], "prespecified_strategy_fci": [strategy_fci_name], "strategy_start": [strategy_start], "strategy_test_start": [strategy_split_date]}))fig, ax = plt.subplots(figsize=(7, 3.5))strategy_sample_scoreboard["final_score"].sort_values().plot(kind="barh", ax=ax, color=colors[2])ax.set_title("strategy test sample score")ax.grid(True, axis="x", alpha=0.3)plt.tight_layout()plt.show()
observations
first_valid_date
last_valid_date
future_stress_rank_ic
drawdown_spread
monotonicity_score
volatility_spread
stability_score
future_stress_rank_ic_rank
drawdown_spread_rank
monotonicity_score_rank
volatility_spread_rank
stability_score_rank
final_score
strategy_test_sample
model
fci_econ
112
2016-10-31
2026-01-31
0.1919
0.0114
0.7
0.1000
0.7765
1.0
0.6
1.0
1.0
0.4
0.84
True
fci_pca
112
2016-10-31
2026-01-31
0.0918
0.0151
0.3
0.0957
0.7832
0.6
1.0
0.5
0.8
0.6
0.71
True
fci_prob
112
2016-10-31
2026-01-31
0.1395
0.0145
0.3
0.0303
0.7338
0.8
0.8
0.5
0.4
0.2
0.62
True
fci_blend
112
2016-10-31
2026-01-31
0.0549
0.0073
0.4
0.0871
0.8071
0.4
0.4
0.8
0.6
0.8
0.55
True
fci_pls
112
2016-10-31
2026-01-31
-0.1719
-0.0146
-0.8
-0.0323
0.8487
0.2
0.2
0.2
0.2
1.0
0.28
True
full_sample_score_winner
common_sample_score_winner
test_sample_score_winner
selected_strategy_fci
selection_rule
0
fci_pca
fci_econ
fci_prob
fci_pca
Pre-specified long-history unsupervised FCI se...
long_history_fci
prespecified_strategy_fci
strategy_start
strategy_test_start
0
fci_pca
fci_pca
1999-07-31
2016-10-31
26) Choosing the Strategy FCI
The allocation strategy doesn’t automatically use the model with the highest recent test score. It uses a pre-specified long-history unsupervised FCI, fci_pca, because the strategy needs a stable signal across multiple market cycles.
This is a key portfolio design point. A model can win the recent supervised score because it handled 2020–2025 well, but a trading strategy needs to survive many regimes. The selected strategy FCI starts early enough to include the dot-com cycle, the 2008 crisis, the 2020 crash, and the inflation shock.
The strategy sample starts in July 1999, and the test split begins around October 2016. That gives a long enough live-style history to compare sector rotation, macro gating, and benchmark behavior across several stress periods.
The selection table is transparent: fci_pca wins the full-sample long-history rule, fci_econ wins the common-sample score, fci_prob wins the test-sample score, and fci_pca is chosen for the actual strategy to avoid supervised model-selection lookahead and keep crisis coverage.
The return heatmap shows sector performance across easy-to-tight conditions. The volatility heatmap shows how risk changes across regimes.
The strongest pattern is in volatility. Almost every sector becomes more volatile in tighter FCI regimes. In the tightest quintile, XLF volatility is around 33%, XLE around 31%, XLY around 29%, and XLB/XLK around 26%. Defensive sectors like XLP and XLV have lower volatility, roughly 15% and 18% in the tightest quintile.
This is exactly the macro relation we care about: tight conditions may not always produce negative sector returns immediately, but they raise the risk distribution.
The return side is more nuanced. Some sectors still have positive average returns in high-FCI regimes, but the compensation comes with much higher volatility. Financials, energy, discretionary, and materials show strong regime sensitivity. Staples and health care are more stable.
This means FCI is more useful as a risk-gating variable than as a pure return forecast. It tells us when the return distribution becomes wider and drawdowns become more likely. That is why later strategy design reduces risky exposure when FCI percentile rises rather than trying to short all sectors whenever conditions are tight.
The heatmap also supports sector differentiation. During easier conditions, cyclical sectors can do well because growth and credit conditions support earnings. During tight conditions, volatility rises sharply, and defensive sectors become more attractive from a risk-adjusted perspective.
28) Defensive Minus Cyclical Spread and Inflation Assets
The defensive minus cyclical spread tracks a simple relative performance idea:
The plot shows that defensives have not simply dominated over the whole sample. The spread spikes in stress episodes and then can give back gains when risk appetite returns. This is why regime timing matters. Holding defensives permanently can underperform in bull markets, but switching to them during stress can reduce drawdown.
The high-inflation/policy table shows that inflation beneficiaries did well during high inflation-policy periods: XLE has average monthly return around 1.91%, XLB around 1.05%, DBC around 2.07%, and GLD around 1.27%. That supports the macro intuition that energy, materials, commodities, and gold can help when inflation is the dominant pressure.
The rate-sensitive table has missing values for XLK, XLY, and TLT under the high-policy condition in this specific output. That means the condition and available monthly data did not produce a valid sample for the selected assets in that filtered table. We shouldn’t force an interpretation from NaNs.
The inflation-asset result is still useful. It tells us that the defensive sleeve shouldn’t always mean only cash and bonds. During growth recessions with low inflation, Treasuries can help. During inflation shocks, long-duration bonds can be the wrong hedge, while commodities, gold, and short-duration cash-like assets may be more appropriate.
That is exactly why the later defensive-sleeve logic changes depending on whether stress comes from inflation-policy pressure or growth recession pressure.
Turning Macro Into Allocation Rules
A macro index becomes useful only when it changes a decision. In this project, the decision is not a single all-in/all-out switch. The FCI changes the risk budget and the macro blocks change sector preference.
where \(w_t^{risk}\) is the amount allocated to risky sector rotation, \(w_t^{sector}\) is the selected sector weight vector, and \(w_t^{defensive}\) is the defensive sleeve weight vector.
This structure is clean because it separates two questions:
How much equity-sector risk do we want?
Which sector exposures fit the current macro and momentum state?
A single score that mixes everything into one portfolio vector would be harder to interpret. The two-layer design lets us diagnose whether performance came from risk gating, sector selection, or defensive-asset selection.
The allocation strategy has two layers. First, it decides how much capital should remain in risky sectors. Second, it decides which sectors should receive that risky allocation.
The risky weight is based on the selected FCI percentile and the recent FCI change. Let \(p_t\) be the FCI percentile and \(\Delta_3F_t\) be the 3-month FCI change. The risky weight is:
Here \([x]_0^a\) means the value is clipped between \(0\) and \(a\), and \([\Delta_3F_t]^+\) means only positive FCI changes reduce risk.
This formula says:
If FCI percentile is below 60%, we keep full risky exposure.
If FCI percentile rises above 60%, risky exposure is gradually reduced.
If FCI is also rising quickly, risky exposure is reduced a bit more.
The strategy never goes below 50% risky exposure.
The latest strategy setup has 268 strategy months, from July 1999 to January 2026, and the minimum risky weight over the sample is 0.55. So the strategy is macro-gated, but it is not a full risk-off timing system.
30) Defensive Sleeve Logic
The defensive sleeve is chosen based on the type of macro stress.
If inflation and policy pressure are high:
\[
B_{I,t}+B_{P,t}>1
\]
we use inflation-aware defensive assets: gold, commodities, short Treasuries, and cash. This protects against the 2022-style regime where long duration can lose money at the same time as equities.
If growth recession pressure is high and inflation pressure is not high:
we use duration and classic safe-haven assets: intermediate Treasuries, short Treasuries, gold, and cash. This is more like a 2008 or pre-inflation recession hedge.
Otherwise, the defensive sleeve uses cash, short Treasuries, and intermediate Treasuries. This is the neutral defensive setup.
This conditional sleeve is one of the strongest ideas in the project. Defensive allocation depends on the source of macro stress. Bonds can be protection in growth shocks, but they can be a source of losses in inflation shocks.
Show code
def available_assets_at_date(date, candidates, ret):return [asset for asset in candidates if asset in ret.columns and date in ret.index and pd.notna(ret.loc[date, asset])]def defensive_sleeve(row): date = row.name inflation_policy = row.get("inflation_pressure_block", 0.0) + row.get("policy_rate_pressure_block", 0.0) growth = row.get("growth_recession_block", 0.0) inflation = row.get("inflation_pressure_block", 0.0)if inflation_policy >1.0: candidates = ["gld", "dbc", "shy", "cash"]elif growth >0.75and inflation <=0.75: candidates = ["ief", "shy", "gld", "cash"]else: candidates = ["cash", "shy", "ief"] picked = available_assets_at_date(date, [x for x in candidates if x in defensive_assets], returns_raw)ifnot picked: picked = ["cash"] out = pd.Series(0.0, index=defensive_assets) out.loc[picked] =1.0/len(picked)return outdef macro_fit_scores(features, assets): rows = []for date, row in features.iterrows(): score = pd.Series(0.0, index=assets)for asset in assets:if asset in defensive_sector_assets: score.loc[asset] +=0.70* row["best_fci_percentile"] +0.45* row["growth_recession_block"]if asset in cyclical_sector_assets: score.loc[asset] +=0.80* (1.0- row["best_fci_percentile"]) -0.40* row["growth_recession_block"] -0.30* row["macro_breadth_conflict_block"]if asset in inflation_assets: score.loc[asset] +=0.55* row["inflation_pressure_block"] -0.25*max(row["growth_recession_block"], 0.0)if asset in rate_sensitive_assets: score.loc[asset] -=0.45* row["policy_rate_pressure_block"]if asset =="xlf": score.loc[asset] -=0.35*max(row["policy_rate_pressure_block"], 0.0) *max(row["growth_recession_block"], 0.0) rows.append(score.rename(date))return cross_z(pd.concat(rows, axis=1).transpose())
The pure momentum rotation selects sectors based on recent sector strength. The macro version keeps momentum as the core, but modifies it with macro fit.
where \(R^{12-1}\) is medium-term momentum with a one-month skip, \(R^{6m}\) is six-month return, and \(\sigma^{6m}\) is recent annualized volatility. The second term rewards return per unit of recent risk.
Macro fit adds economic context. Defensive sectors receive support when FCI percentile and growth stress are high:
\[
\text{fit}_{def,t}=0.70p_t+0.45B_{G,t}
\]
Cyclicals receive support when conditions are easier and growth stress is low:
Rate-sensitive assets are penalized when policy pressure is high:
\[
\text{fit}_{rate,t}=-0.45B_{P,t}
\]
Financials receive an extra penalty when policy pressure and growth stress are both positive, because credit quality and funding conditions can deteriorate together.
Final Sector Score
The final sector score blends momentum, macro fit, macro support, and a risk penalty:
Momentum receives the largest weight because price action still matters. Macro fit receives a meaningful but smaller weight, enough to tilt the selection toward sectors that match the macro regime. Support adds broad goldilocks or inflation support. The risk penalty reduces sectors with high recent volatility or recent losses.
The strategy selects the top three sectors by \(S_{i,t}\), then allocates by inverse volatility with a 40% cap:
The selected sector sleeve is scaled by the risky weight \(w_t^{risk}\), and the rest goes into the defensive sleeve. This keeps the strategy from becoming a pure top-three momentum portfolio when macro stress is high.
The latest selected-sector table shows a clear risk-on tilt in late 2025 and January 2026. The selected sectors rotate among XLF, XLI, XLU, XLY, XLK, and XLP during the last year, with January 2026 selecting XLI, XLK, and XLY.
This matches the latest macro state. The selected FCI percentile is around 0.38, so conditions are not tight enough to force a large defensive allocation. The dominant macro block is labor cooling, but the actual labor stress is negative, which means labor is not the main risk driver. Inflation is positive, but broad stress is not confirmed.
The recent selection therefore leans toward cyclical and growth-sensitive sectors rather than cash, bonds, or gold. The strategy is responding to the combination of neutral FCI, strong or acceptable sector momentum, and the absence of broad recession stress.
The weight heatmap shows the last 60 months of the FCI momentum strategy. It is useful because it reveals whether the allocation is stable or constantly jumping.
The top panel shows sector and defensive weights. The darker cells correspond to higher weights, capped around 40%. The bottom panel shows the risky/defensive split.
The strategy spends most of the latest window heavily allocated to the risky sleeve, with only short defensive episodes. This is consistent with the selected FCI percentile being below the main stress threshold most of the time after 2023. The defensive sleeve becomes more relevant when FCI percentile rises or when the FCI is increasing quickly.
The heatmap also shows that the strategy is concentrated by design. It usually holds three selected sectors plus a defensive sleeve if risk is gated. This means the performance comparison later needs to consider turnover and concentration risk, not just CAGR.
Why We Compare Four Active Strategies Instead of One
The project compares multiple strategies because each one answers a different research question.
Equal-weight sectors answers: what happens if we keep sector exposure simple and diversified? Momentum rotation answers: how much value comes from price trend alone? FCI-gated basket answers: how much value comes from changing total risky exposure based on macro conditions? FCI momentum rotation answers: what happens when macro information changes both the risk budget and the sector selection?
This decomposition is important. If FCI momentum beats everything, macro plus momentum is strong. If pure momentum beats FCI momentum, the macro overlay may be too defensive or too slow. If FCI-gated beats equal-weight sectors, macro risk gating adds value even without complex sector picking.
The comparison also separates performance sources. CAGR tells us about compound growth. Volatility and Sortino tell us about risk. Max drawdown and worst 12-month return tell us about pain. Turnover tells us how expensive the strategy is to run. A macro strategy that improves drawdown but destroys CAGR is a hedge, not necessarily a better portfolio.
Show code
spy_weights = equal_sector_weights.copy() *0.0spy_weights["spy"] =1.0weights_by_strategy = {"equal_weight_sectors": equal_sector_weights, "momentum_rotation": momentum_weights, "fci_gated_basket": gated_weights, "fci_momentum_rotation": fci_momentum_weights, "spy_buy_hold": spy_weights}backtest_results = run_many_weights_backtests(weights_by_strategy, returns=strategy_returns, cost_bps=5.0, weight_timing="next_close")performance_table = build_metrics_table(backtest_results, rf_daily=rf_monthly, annualization=12.0)trade_table = build_trade_table(backtest_results)performance_table.columns = [str(col).lower().replace(" ", "_") for col in performance_table.columns]trade_table.columns = [str(col).lower().replace(" ", "_") for col in trade_table.columns]strategy_return_table = pd.concat({name: result.net_returns for name, result in backtest_results.items()}, axis=1).fillna(0.0)strategy_nav_table = pd.concat({name: result.net_values for name, result in backtest_results.items()}, axis=1)rolling_12m = (1.0+ strategy_return_table).rolling(12).apply(np.prod, raw=True) -1.0performance_table["hit_rate"] = (strategy_return_table >0).mean()performance_table["worst_12m_return"] = rolling_12m.min()performance_table["turnover"] = trade_table["avg_turnover"]performance_table["cost_drag"] = trade_table["cost_drag"]display(performance_table.round(4))full_defensive_first_month = monthly_close[optional_defensive_assets].apply(pd.Series.first_valid_index).max()full_defensive_start =max(strategy_start, full_defensive_first_month + pd.offsets.MonthEnd(6))full_defensive_dates = strategy_dates[strategy_dates >= full_defensive_start]full_defensive_returns = returns_raw.loc[full_defensive_dates.min():strategy_end, strategy_assets]display(full_defensive_returns.isna().mean().to_frame("missing_share_in_full_defensive_window"))full_defensive_weights = {name: weights.reindex(full_defensive_dates).dropna(how="all") for name, weights in weights_by_strategy.items()}full_defensive_results = run_many_weights_backtests(full_defensive_weights, returns=full_defensive_returns.fillna(0.0), cost_bps=5.0, weight_timing="next_close")full_defensive_performance_table = build_metrics_table(full_defensive_results, rf_daily=rf_monthly, annualization=12.0)full_defensive_performance_table.columns = [str(col).lower().replace(" ", "_") for col in full_defensive_performance_table.columns]display(pd.DataFrame({"full_defensive_start": [full_defensive_start], "full_defensive_months": [len(full_defensive_dates)]}))display(full_defensive_performance_table.round(4))
cagr
vol
sharpe
max_drawdown
calmar
sortino
hit_rate
worst_12m_return
turnover
cost_drag
Strategy
equal_weight_sectors
0.1070
0.1433
0.5069
-0.4913
0.2178
0.6570
0.6704
-0.4294
0.0136
0.0006
momentum_rotation
0.1099
0.1384
0.5379
-0.3766
0.2918
0.7544
0.6292
-0.3225
0.2632
0.0124
fci_gated_basket
0.1123
0.1312
0.5772
-0.3406
0.3298
0.8307
0.6704
-0.2728
0.0344
0.0015
fci_momentum_rotation
0.0978
0.1392
0.4546
-0.2532
0.3864
0.6532
0.6330
-0.2103
0.2698
0.0168
spy_buy_hold
0.1086
0.1449
0.5132
-0.5078
0.2139
0.6859
0.6704
-0.4342
0.0019
0.0000
missing_share_in_full_defensive_window
cash
0.0
dbc
0.0
gld
0.0
ief
0.0
shy
0.0
spy
0.0
tlt
0.0
xlb
0.0
xle
0.0
xlf
0.0
xli
0.0
xlk
0.0
xlp
0.0
xlu
0.0
xlv
0.0
xly
0.0
full_defensive_start
full_defensive_months
0
2006-08-31
234
cagr
vol
sharpe
max_drawdown
calmar
sortino
Strategy
equal_weight_sectors
0.1038
0.1508
0.4675
-0.4913
0.2112
0.6162
momentum_rotation
0.1065
0.1432
0.5066
-0.3766
0.2829
0.7158
fci_gated_basket
0.1101
0.1377
0.5392
-0.3406
0.3231
0.7884
fci_momentum_rotation
0.0881
0.1425
0.3947
-0.2532
0.3480
0.5653
spy_buy_hold
0.1102
0.1525
0.4985
-0.5078
0.2169
0.6770
33) Strategy Comparison
The strategy table compares five approaches:
Equal-weight sectors.
Pure momentum rotation.
FCI-gated sector basket.
FCI momentum rotation.
SPY buy and hold.
The key results are:
Strategy
CAGR
Vol
Sharpe
Max drawdown
Worst 12m
Turnover
Equal-weight sectors
10.70%
14.33%
0.507
-49.13%
-42.94%
1.36%
Momentum rotation
10.99%
13.84%
0.538
-37.66%
-32.25%
26.32%
FCI-gated basket
11.23%
13.12%
0.577
-34.06%
-27.28%
3.44%
FCI momentum rotation
9.78%
13.92%
0.455
-25.32%
-21.03%
26.98%
SPY buy and hold
10.86%
14.49%
0.513
-50.78%
-43.42%
0.19%
The FCI-gated basket gives the best overall Sharpe and the best CAGR among the US strategies in the full window. It reduces volatility, drawdown, and worst 12-month loss while slightly improving return.
The FCI momentum rotation has the lowest drawdown by far, around -25.3%, but its CAGR is lower. That means the macro-momentum version is more defensive and better at avoiding deep losses, but it gives up enough upside to reduce Sharpe.
The model comparison is especially useful because each strategy isolates a different idea.
Equal-weight sectors is the neutral sector benchmark. It has high drawdown because it stays fully invested in equities throughout every crisis. SPY buy-and-hold behaves similarly, with slightly higher drawdown.
Pure momentum rotation improves drawdown and return relative to equal-weight sectors, but it has high turnover. Momentum is useful because it can move away from persistent losers, but it can also rotate aggressively.
The FCI-gated basket keeps broad sector exposure but changes the risky/defensive split. Its strong result says macro risk gating adds value in this sample. It doesn’t need to pick perfect sectors; reducing risk exposure during tight conditions is enough to improve the return distribution.
The FCI momentum rotation is the most protective. It has the lowest max drawdown and worst 12-month loss. The cost is lower CAGR and higher turnover. This is a classic risk-control tradeoff: better crash behavior, weaker upside capture.
The full-defensive-window table starts in August 2006, once all defensive assets are available. In this shorter but cleaner sample, the main ranking remains economically consistent.
The FCI-gated basket has the best Sharpe around 0.539 and the best Sortino around 0.788. SPY has slightly higher CAGR, about 11.02%, but suffers a much larger max drawdown around -50.78%. The FCI-gated basket keeps max drawdown around -34.06%.
The FCI momentum rotation again gives strong drawdown control, around -25.32%, but lower CAGR around 8.81%. That confirms the same interpretation from the full strategy window: macro-momentum is defensive and robust in crashes, but it can become too cautious in long risk-on periods.
This is why the project shouldn’t declare one universal winner. For growth with moderate risk control, FCI-gated is strongest. For maximum drawdown control, FCI momentum is strongest. For simplicity, equal-weight or SPY is easier but riskier.
The equity-curve plot shows how the strategies compound through time. The drawdown plot shows where the macro strategies actually add value.
The main visual result is that macro-aware strategies separate most during crisis windows. During calm bull markets, the strategies often move together because they are all mostly invested in risky assets. During 2008, 2020, and 2022, the differences become much larger.
The rolling 12-month return plot shows that FCI momentum sacrifices some upside during strong recoveries. The rolling Sharpe plot shows periods where macro gating improves risk-adjusted performance, especially when volatility rises. The turnover plot shows the cost of rotation: pure momentum and FCI momentum rotate much more than equal-weight or SPY.
Turnover is not just an implementation detail. It is part of the strategy’s economic profile. A strategy with 27% monthly turnover needs enough alpha or risk reduction to justify trading costs. In this project, the 5 bps transaction cost assumption is mild. Higher costs would hurt momentum rotation and FCI momentum more than the FCI-gated basket.
Stress Windows as Economic Tests
The selected stress windows are not random. They represent different macro shock types.
The financial crisis is a credit, housing, labor, and liquidity crisis. A good macro FCI should detect broad stress and reduce risk. The COVID crash is a sudden exogenous growth shock with emergency policy easing. A slow macro signal may lag it, so price momentum can matter more. The inflation/rate shock is a nominal shock where bonds and equities can fall together. A useful strategy needs inflation-aware defensive assets or sector rotation toward inflation beneficiaries.
This matters because a strategy can look good on average while failing in the specific regime it is supposed to handle. The stress-window table checks whether the strategy works for the right economic reasons.
For example, the FCI momentum strategy does especially well in the financial crisis because broad macro stress was visible and persistent. It helps less in COVID because the shock was too sudden. In 2022, sector momentum and inflation beneficiaries play a major role.
The stress-window table is the clearest test of the macro idea.
During the financial crisis, SPY loses about -37.0%, equal-weight sectors lose about -37.1%, and momentum rotation loses about -29.0%. The FCI-gated basket loses -21.2%, and FCI momentum loses only -14.8%. The drawdown reduction is large. This is exactly where a macro stress index should help.
During the COVID crash, the shock was extremely fast. FCI-gated loses about -11.9%, similar to equal sectors, while momentum and FCI momentum do better, around -6.9% and -6.7%. The macro system had less time to react because COVID was a sudden event, so price momentum helped more than slow macro gating in that window.
During the 2022 inflation/rate shock, SPY loses about -17.7%, while momentum rotation gains about 1.5%. FCI momentum loses about -3.9%, and equal sectors/gated basket lose about -6.2%. This shows that sector selection and inflation-aware rotation were more important than just reducing risk.
Show code
latest_date = fci_momentum_weights.index.max()latest_features = allocation_features.loc[latest_date]decision_rows = []for asset in sector_assets + defensive_assets: momentum_value = sector_momentum_score.get(asset, pd.Series(0.0, index=strategy_dates)).reindex(strategy_dates).loc[latest_date] if asset in sector_assets else0.0 fit_value = macro_fit_score.get(asset, pd.Series(0.0, index=strategy_dates)).loc[latest_date] if asset in sector_assets else0.0 support_value = macro_support_score.get(asset, pd.Series(0.0, index=strategy_dates)).loc[latest_date] if asset in sector_assets else0.0 penalty_value = recent_risk_penalty.get(asset, pd.Series(0.0, index=strategy_dates)).loc[latest_date] if asset in sector_assets else0.0 final_value = final_sector_score.get(asset, pd.Series(0.0, index=strategy_dates)).loc[latest_date] if asset in sector_assets else0.0 weight_value = fci_momentum_weights.loc[latest_date, asset] if asset in fci_momentum_weights.columns else0.0if asset in inflation_assets and latest_features["inflation_pressure_block"] >0.75and weight_value >0: reason ="inflation hedge"elif asset in defensive_assets and latest_features["best_fci_percentile"] >0.65and weight_value >0: reason ="defensive stress protection"elif asset in rate_sensitive_assets and latest_features["policy_rate_pressure_block"] >0.75and fit_value <0: reason ="rate-pressure penalty"elif asset in cyclical_sector_assets and latest_features["best_fci_percentile"] <0.45and weight_value >0: reason ="risk-on cyclical"elif momentum_value <0and weight_value <=1e-8: reason ="weak momentum"else: reason ="macro stress exclusion"if weight_value <=1e-8else"risk-on cyclical" decision_rows.append({"date": latest_date, "selected fci model": strategy_fci_name, "selected fci value": latest_features["best_fci_value"], "selected fci percentile": latest_features["best_fci_percentile"], "selected fci 3m change": latest_features["best_fci_3m_change"], "dominant macro block": latest_features["dominant_macro_block"], "sector": asset, "momentum_score": momentum_value, "macro_fit_score": fit_value, "macro_support_score": support_value, "recent_risk_penalty": penalty_value, "final_score": final_value, "portfolio_weight": weight_value, "short reason category": reason})latest_decision_board = pd.DataFrame(decision_rows).sort_values("portfolio_weight", ascending=0)display(latest_decision_board.round(4))
date
selected fci model
selected fci value
selected fci percentile
selected fci 3m change
dominant macro block
sector
momentum_score
macro_fit_score
macro_support_score
recent_risk_penalty
final_score
portfolio_weight
short reason category
8
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xly
0.0403
1.1006
-0.5040
-1.7777
0.2848
0.4000
risk-on cyclical
3
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xli
0.7609
0.7068
-0.5040
-1.4127
0.6562
0.3667
risk-on cyclical
4
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xlk
1.4418
-1.1995
-0.5040
3.0926
0.4922
0.2333
risk-on cyclical
2
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xlf
-0.1387
0.7068
-0.5040
-1.4445
0.0730
0.0000
weak momentum
1
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xle
-0.0821
-0.7742
1.7638
1.1999
-0.0918
0.0000
weak momentum
5
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xlp
-1.4599
-0.6888
-0.5040
-0.2562
-1.1243
0.0000
weak momentum
0
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xlb
-0.7140
1.5258
1.7638
0.4703
-0.0060
0.0000
weak momentum
6
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xlu
0.0864
-0.6888
-0.5040
0.5112
-0.1575
0.0000
macro stress exclusion
7
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
xlv
0.0653
-0.6888
-0.5040
-0.3828
-0.1265
0.0000
macro stress exclusion
9
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
cash
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
macro stress exclusion
10
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
shy
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
macro stress exclusion
11
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
ief
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
macro stress exclusion
12
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
tlt
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
macro stress exclusion
13
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
gld
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
macro stress exclusion
14
2026-01-31
fci_pca
-0.3795
0.3823
-0.0183
labor_cooling_block
dbc
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
macro stress exclusion
36) Latest Decision Board
The latest decision board explains the January 2026 allocation. The selected FCI model is fci_pca, the selected FCI value is around -0.38, and the percentile is about 0.38. That means the FCI is not tight. The 3-month change is slightly negative, around -0.018, so conditions are not rapidly worsening.
The final portfolio weights are:
XLY at 40.0%,
XLI at about 36.7%,
XLK at about 23.3%,
all defensive assets at zero.
The decision board shows why. XLI has strong momentum and positive final score. XLK has very strong momentum but gets a negative macro-fit score and a high risk penalty, so it receives less than the cap. XLY gets the cap despite modest momentum because its macro fit is strong and recent risk penalty is favorable.
XLF, XLE, XLB, XLP, XLU, and XLV receive zero weights. Some have weak momentum. Some have macro-stress exclusion. The current signal is a concentrated risk-on sector allocation rather than a defensive macro allocation.
The latest-score plot confirms the same story visually. The final-score panel ranks the selected sectors above the rest. The portfolio-weight panel shows that the cap binds for XLY, while XLI and XLK receive the remaining risky allocation. The latest macro-block panel shows inflation pressure as the only major positive block, with growth, labor, housing, external trade, and breadth/conflict not showing broad stress.
This is a good example of how the model combines macro and price information. Inflation alone doesn’t force a defensive allocation because the selected FCI percentile is not high and the other blocks are not confirming a crisis. Momentum still matters, so the model owns the sectors with the strongest final sector scores.
The risk is concentration. A three-sector portfolio can underperform badly if the macro state changes quickly or if the selected sectors reverse. That concentration is intentional in the strategy design, but it means the strategy should be judged on drawdown, turnover, and stress behavior, not only on average return.
Reading the US Result Before Moving to Canada
The US run gives a clear hierarchy of ideas.
Macro gating improves the broad sector basket by reducing exposure during tight conditions. Pure momentum improves drawdown relative to equal sectors but rotates heavily. FCI momentum gives the strongest drawdown control but gives up too much return in some risk-on periods. The best overall US balance is the FCI-gated basket because it keeps broad equity participation while reducing exposure when macro stress rises.
The latest US allocation is risk-on because the chosen FCI percentile is not high. The model doesn’t move to cash or bonds just because inflation pressure is positive. It waits for broad stress or rising FCI pressure.
That logic is important for the secondary run. In Canada, the same framework may behave differently because commodities and external trade play a larger role. So the secondary application is not just a code-repeat. It tests whether the macro-FCI architecture can adapt to a different economy and sector structure.
37) Canada Secondary Application Through the Library
The secondary application repeats the macro-FCI workflow on Canada using the packaged library. This section is important because Canada has a different macro structure from the US.
Canada is more commodity-sensitive, more externally exposed, and more concentrated by sector. Financials, energy, materials, real estate, utilities, staples, and technology play different roles than US sectors. The Canadian economy is also sensitive to global commodity prices, housing, the Canadian dollar, and US demand.
The Canada workflow uses Canadian macro factors, Canadian sector ETFs, broad Canadian equity ETFs, Canadian bond and gold ETFs, and some global signal assets like FXC, UUP, HYG, LQD, and SPY. The data can be reproduced from the corresponding folders inside the repo’s data folder, especially the Canada macro, Canada sector ETF, core cross-asset, and international hedging ETF folders.
The important point is that the same macro language carries over: inflation pressure, policy pressure, growth recession, labor cooling, housing, external trade, breadth/conflict, FCI models, model scoring, and macro-gated allocation.
Canadian Asset Roles
The Canadian tradable universe has a very different economic composition:
ETF
Economic role
XIC.TO / XIU.TO
Broad Canadian equity exposure
XFN.TO
Canadian financials, heavily tied to banks, credit, housing, and yield-curve conditions
XEG.TO
Energy, tied to oil and global commodity cycles
XMA.TO
Materials, tied to metals, mining, and global industrial demand
XRE.TO
Real estate, sensitive to rates and Canadian housing conditions
XUT.TO
Utilities, defensive but rate-sensitive
XST.TO
Staples, defensive consumption
XIT.TO
Technology, growth-oriented but smaller and more concentrated than US tech
XSB.TO / XBB.TO / XLB.TO
short, aggregate, and long Canadian bonds
CGL-C.TO
Canadian gold exposure, useful for commodity/inflation stress
This market is a great secondary test because macro conditions should affect sectors differently than in the US. Energy and materials have much larger macro importance, while financials are deeply connected to domestic credit and housing.
The Canada latest block chart shows inflation pressure positive and most other blocks negative. External trade is the dominant macro block in the latest decision table. That makes sense for Canada because external demand and commodity-linked trade matter more than they do for the US.
The Canada block correlation matrix also differs from the US. External trade is more connected to labor cooling, and macro breadth/conflict relates to inflation, policy, and growth. This means a Canadian FCI can’t just be a copy of a US FCI with different tickers. The same methodology is reusable, but the economic interpretation has to be country-specific.
The Canada FCI plot shows very large spikes around the global financial crisis, 2020, and the post-2021 period. The selected FCI is the blend, which has enough history and strong full-sample score.
Why External Trade Matters More in Canada
External trade has a bigger role in the Canadian application because Canada is more exposed to commodities and foreign demand. Energy and materials are not small satellite sectors; they are core parts of the equity market and national income cycle.
A commodity upswing can improve trade income, support energy/materials earnings, strengthen the currency, and attract capital into Canadian equities. A commodity downturn can do the opposite. This means external trade pressure and commodity-sensitive sector returns are more tightly linked than in the US.
The Canada latest decision table shows external_trade_block as the dominant macro block. That doesn’t mean the economy is in trade stress; the selected FCI percentile is low. It means the largest absolute macro signal at the latest date comes from the external-trade side. In a Canadian allocation problem, that is exactly the kind of information we want to preserve.
This is also why the secondary run uses international hedging ETFs and global signal assets. Currency and global risk appetite matter more when the local equity market is tied closely to commodities and external demand.
38) Canada FCI Model Scores
The full-sample Canada scoreboard ranks fci_blend first with a final score around 0.713. It has a future-stress rank IC around 0.232, drawdown spread around 0.012, and volatility spread around 0.047. The probability FCI is second, with a higher rank IC around 0.251 but lower monotonicity and stability.
The test-sample scoreboard ranks fci_pls first with a very high score around 0.900, followed by fci_prob around 0.775. This tells us supervised models worked very well in the recent Canada sample. But the strategy selects fci_blend, because it is history-eligible and wins the full-sample rule.
That choice is sensible. Canada has fewer independent macro cycles in the tradable ETF sample, and the supervised models can become very sample-dependent after 2021. A blended FCI keeps the supervised information but anchors it with PCA and economic weighting.
The strategy starts in July 2012 and runs to January 2026. This window includes the commodity crash, COVID, the 2022 rate/inflation shock, and the 2023–2025 rebound.
Why the Canada Model Can Reward Cyclicals Under Easy FCI
When the selected Canada FCI percentile is low, the model does not need a large defensive sleeve. In that state, the main allocation problem becomes sector selection. Canada’s cyclical sectors are not interchangeable.
Financials, energy, and materials respond to different macro channels. Financials care about credit quality, housing, and rate margins. Energy cares about oil and global commodity demand. Materials care about metals, mining, global industrial demand, and sometimes China-sensitive growth expectations. When external trade is the dominant macro block and FCI is easy, the model can reasonably prefer XFN, XEG, and XMA at the same time.
The final sector score still keeps momentum at the center, so macro support alone cannot force a sector into the portfolio. That is why XEG can be selected despite weaker momentum: it receives enough macro fit and support. XMA has strong momentum and macro support, so it becomes a very natural holding. XFN has strong momentum and favorable risk penalty, so it gets the largest weight.
This interaction between macro fit and momentum is the main reason the Canada FCI momentum strategy performs better than a simple gated basket.
39) Canada Strategy Results
The Canada performance table is strong. The best risk-adjusted strategy is fci_momentum_rotation, with:
CAGR around 13.17%,
volatility around 11.61%,
Sharpe around 0.755,
max drawdown around -19.12%,
Sortino around 0.859.
Momentum rotation has the highest CAGR at about 13.40%, but it has slightly higher volatility and lower Sharpe. Equal-weight sectors and XIC buy-and-hold are also strong, but they have larger drawdowns, around -22% to -23%.
The FCI-gated basket is more defensive, with lower volatility but also lower CAGR. In Canada, combining FCI with momentum works better than gating alone because sector selection matters a lot. Energy, materials, financials, and technology can diverge strongly depending on commodity and external-demand regimes.
This is an important contrast with the US run. In the US, the FCI-gated basket was the cleanest overall Sharpe improvement. In Canada, FCI momentum is stronger because macro conditions map more directly into sector leadership.
The latest Canada decision board selects XFN.TO, XEG.TO, and XMA.TO, with small residual weights in XBB.TO and XSB.TO. The selected FCI model is fci_blend, with value around -0.625 and percentile around 0.335. Conditions are relatively easy, and the 3-month change is small positive.
The portfolio weights are:
XFN.TO about 39.6%,
XEG.TO about 32.7%,
XMA.TO about 26.8%,
small bond weights around 0.5% each for XBB and XSB.
This is a risk-on, commodity/cyclical Canadian allocation. XFN has strong momentum and favorable risk penalty. XMA has very strong momentum and strong macro fit. XEG has weaker momentum but strong macro support and macro fit because energy benefits from inflation/commodity sensitivity. XRE, XUT, XST, and XIT are excluded mainly because their momentum and final scores are weaker.
Canada Macro Interpretation
The Canada return-by-regime heatmap shows that sector leadership changes with the FCI. Energy and materials can perform well even when some stress measures are elevated, because commodity-linked sectors benefit from certain inflation and external-demand regimes. Financials can also lead when conditions are easy and credit remains stable.
The commodity-minus-defensive plot rises strongly in commodity-friendly periods, especially after 2020. That explains why a macro-aware Canada strategy needs commodity interpretation. A generic defensive/risk-on split is not enough. In Canada, energy and materials can act like macro assets, not just equity sectors.
The Canada strategy growth and drawdown plots show that fci_momentum_rotation controls drawdowns while keeping strong upside participation. The latest weights plot shows a clear move toward materials, financials, and energy in the recent period, with less reliance on defensive bonds or gold.
That secondary result strengthens the project. The same FCI architecture works in another market, but the economic content changes. In the US, the strategy mainly uses macro stress to gate sector risk. In Canada, the FCI also helps identify commodity-linked leadership.
Canada Compared With the US Allocation Problem
The US sector universe is broad and dominated by technology, health care, financials, discretionary, and other large diversified sectors. Canada is more concentrated. Financials, energy, and materials carry much more macro meaning. This changes how we interpret the same FCI machinery.
In the US, inflation pressure may support energy and materials, but those sectors are not always large enough to define the whole market. In Canada, energy and materials are central. External trade and commodity prices affect not only sector returns but also the currency, national income, and investor perception of the whole equity market.
This is why the Canada latest allocation toward XFN, XEG, and XMA is economically coherent. It says the macro state is not broadly stressed, external trade is the dominant block, and commodity/cyclical sectors have enough momentum or macro fit to lead.
The Canadian strategy result also shows that macro-aware momentum can work better when macro regimes map clearly into sector leadership. Energy and materials are not just cyclical sectors here; they are macro transmission assets.
Ending the Project at the Secondary Analysis
The Canada implementation closes the workflow by showing the full macro pipeline in a second economy: macro signals, blocks, FCI models, model scoring, regime interpretation, allocation, backtest, and latest decision diagnostics.
The central lesson is that macro modeling becomes useful when it stays interpretable. We can see whether stress comes from inflation, policy, growth, labor, housing, external trade, or breadth. We can compare different index-construction methods. We can test whether high FCI readings actually lead to worse future market conditions. Then we can translate the result into portfolio decisions with clear allocation rules.
For this project, the strongest parts are the block construction, the model comparison, and the sector/regime interpretation. The macro data doesn’t give a perfect forecast, but it gives a structured regime map that pure price momentum and static sector allocation don’t have.