A systematic global macro regime model for Tactical Asset Allocation, built on Fullerton Fund Management's four-factor Investment Environment framework. Classifies the global economy into five regimes using z-score deviations from trend across growth, inflation, liquidity, and risk appetite — and maps historical asset class performance to each regime.
Built in Python. Runs daily. Designed for discretionary portfolio managers who want a systematic signal to inform, not replace, human judgment.
Most TAA frameworks pick a window, pick a regime label, and present a single number as fact. This model doesn't do that.
Instead it:
- Computes every factor score across five lookback windows simultaneously (3yr, 5yr, 10yr, 20yr, full history)
- Outputs a probability vector across five regimes rather than a hard label
- Flags when windows disagree — because disagreement is itself a signal
- Tracks what it said and when via an append-only state log, so the live history is epistemically honest
- Treats data revisions as new information arriving today, not corrections to the past
The discretionary overlay does real work here. The model tells you where you are and how confident it is. The portfolio manager decides what to do about it.
Inspired by and modelled on Fullerton Fund Management's Investment Environment Model, as disclosed across their quarterly Investment Views (FIV) reports Q1 2023–Q2 2026.
Four factors, each expressed as z-score deviations from trend:
| Factor | What it measures |
|---|---|
| Growth | Leading (yield curve, OECD CLI, equity momentum) + Coincident (GDP trend deviation) |
| Inflation | CPI, PCE, 5yr5yr forward inflation expectations, TIPS real yield, commodity momentum |
| Liquidity | M2 growth, Fed balance sheet, real rate level, USD direction |
| Risk Appetite | VIX, HY credit spreads, EM/DM relative performance, equity trend |
Five regimes, output as probability distribution:
| Regime | Growth | Inflation | Character |
|---|---|---|---|
| Recovery / Early Cycle | ↑ | ↓ | Growth recovering, inflation contained |
| Goldilocks | + | − | Above-trend growth, below-trend inflation |
| Late Cycle | ↓ | ↑ | Decelerating growth, rising inflation |
| Danger Zone | − | + | Falling growth, elevated stress |
| Sentiment Driven | ~ | ~ | Macro ambiguous — risk appetite dominant |
Sentiment Driven is treated as a distinct positive regime (consistent with Fullerton's published framework), not merely an uncertainty flag. When macro factors are near trend and risk appetite is elevated, positioning and mood drive returns more than fundamentals.
Confidence scoring — three layers:
- Factor magnitude — how far are Growth and Inflation from zero
- Secondary confirmation — do Liquidity and Risk Appetite support the primary call
- Window consensus — what fraction of the five window variants agree
Window weighting. Each window gets a configurable weight in the ensemble probability average. Shorter windows (3yr, 5yr) reflect the current cycle; longer windows (20yr, full history) provide structural context. Default: shorter windows weighted equally at 1.0, longer windows downweighted to 0.5. Adjustable in the Config Editor without touching code.
src/
├── config.py # All parameters — tickers, windows, weights, thresholds, regime ideals
├── config_overrides.py # Override layer — reads config_overrides.json, applied at runtime
├── runner.py # Daily runner — fetches data, runs pipeline, writes state log
├── data/
│ ├── fetcher.py # Pull from FRED + Yahoo Finance, cache locally, incremental refresh
│ ├── vintage_manager.py # Lag offsets, staleness flagging, revision signals
│ ├── state_log.py # Append-only record of every model run
│ └── synthetic.py # Regime-aware synthetic data for testing/showcase
├── factors/
│ └── engine.py # Four-factor computation across all z-score windows + consensus
├── regimes/
│ └── classifier.py # Five-regime probability vector, weighted ensemble, confidence scoring
├── analysis/
│ └── regime_map.py # Historical asset class performance by regime
├── dashboard/
│ └── app.py # Streamlit dashboard — six-page live presentation layer
└── tests/
├── check_module1.py # Data fetcher + vintage manager
├── check_module2.py # Factor engine
├── check_module3.py # Regime classifier
├── check_module4.py # Historical regime map
└── diagnose_live.py # Live data diagnostic — run when data issues arise
Config-driven. Every meaningful number lives in config.py — z-score windows, window weights, publication lag offsets, regime ideal points, softmax temperature, momentum windows, staleness grace periods. Nothing is hardcoded in logic modules. Calibrating the model means touching only config.py or using the dashboard Config Editor.
Override system. config_overrides.json sits alongside config.py and layers parameter overrides on top without modifying the base config. Saved from the dashboard Config Editor. Delete the file to reset to defaults. Use "Promote to config.py" when a calibrated set is ready to become the new default.
Data vintaging. The model only uses data that was publicly available at the time of each run. Publication lags are encoded per series in config. Staleness is flagged visibly, never silently filled. Revisions are treated as new information arriving today, not corrections to the past.
Window parameterisation. Every factor is computed across five lookback windows simultaneously. A 3-year window asks "how does today compare to recent history." A 20-year window asks "how does today compare to a full cycle." These are different questions and will sometimes give different answers — that disagreement is surfaced, not hidden. Window weights control how much each lookback influences the primary regime call.
Probabilistic output. Rather than a single hard regime label, the classifier outputs a probability vector across all five regimes at every date. The primary label is the highest-probability regime, but the full distribution is always visible — consistent with Fullerton's disclosed Investment Environment Indicator.
State log. Every daily run appends one row recording what the model said and what data it used. This is the live epistemic record. The historical bootstrap (before day one of live running) uses revised public data and is clearly labelled as illustrative. BOOTSTRAP and LIVE periods are tagged separately throughout. The state log is the difference between a track record and a backtest.
Daily cadence with incremental refresh. The runner checks what's changed since the last run and pulls only updated series. Most days nothing changes. On data release days, the model updates and logs what changed and why.
Discretionary overlay. The model produces a signal. A human decides what to do with it. The systematic layer compensates for cognitive bias and data overload; the discretionary layer compensates for model blind spots and regime transitions.
| Source | Series | Tier |
|---|---|---|
| FRED | GDP, CPI, PCE, M2, Fed balance sheet, yield curve, TIPS, forward inflation, OECD CLI | REVISED* |
| Yahoo Finance | VIX, S&P 500, global equity ETFs, credit ETFs, commodities, gold | REALTIME |
*REVISED = standard FRED data — retrospectively clean but not what existed in real time. Flagged in dashboard. ALFRED vintage data or Bloomberg DDIS would close this gap.
Known data gaps:
- OECD CLI runs ~2.5 years behind on FRED — growth factor uses yield curve, equity momentum, and GDP as compensating sub-components
- Put/call ratio not available via Yahoo Finance — VIX carries this weight in the risk appetite factor
- Liquidity factor is Fed-centric — ECB, PBoC, BOJ balance sheet data requires Bloomberg or CEIC
| File | Status | Notes |
|---|---|---|
config.py |
✅ Live | Five-regime taxonomy, window weights, all calibration parameters |
config_overrides.py |
✅ Live | Override layer with dashboard editor integration |
runner.py |
✅ Live | Daily runner, state log writer, change detection |
data/fetcher.py |
✅ Live | FRED + Yahoo, incremental refresh, local cache |
data/vintage_manager.py |
✅ Live | Lag offsets, quarterly resampling, staleness flagging |
data/state_log.py |
✅ Live | Append-only, auto-created on first run |
data/synthetic.py |
✅ Live | Regime-aware synthetic data, exact schema match |
factors/engine.py |
✅ Live | All four factors, five windows, consensus scoring |
regimes/classifier.py |
✅ Live | Five-regime probability vector, weighted ensemble |
analysis/regime_map.py |
✅ Live | Regime periods, asset return stats, BOOTSTRAP/LIVE tagging |
dashboard/app.py |
✅ Live | Six-page Streamlit dashboard — Current State, Factor History, Regime History, Asset Performance, Data Quality, Config Editor |
tests/ |
✅ Live | Independent test + diagnostic scripts |
# Install dependencies
pip install -r requirements.txt
# Run all module checks (synthetic data, no internet required)
python src/tests/check_module1.py
python src/tests/check_module2.py
python src/tests/check_module3.py
python src/tests/check_module4.py
# Run daily model and write first state log entry (requires internet)
python src/runner.py
# Diagnose live data issues
python src/tests/diagnose_live.py
# Launch dashboard (keep terminal open while using)
streamlit run src/dashboard/app.pyTo switch from synthetic to live data: toggle "Live data" in the dashboard sidebar.
The runner is designed to be scheduled via Windows Task Scheduler:
Program: python
Arguments: C:\dev\taa\src\runner.py
Start in: C:\dev\taa
Trigger: Daily at 08:00
Each run:
- Fetches latest data from FRED and Yahoo Finance
- Detects what changed since yesterday
- Runs the full pipeline (vintage → factors → classifier)
- Appends one row to
src/logs/state_log.csv - Logs factor readings and regime call to
src/logs/runner.log - Exits (~30 seconds total)
The dashboard reads from the cache and state log independently — it does not need to be running for the daily run to work.
State log location: src/logs/state_log.csv — created automatically on first run. Do not edit manually.
All calibration is done through the Config Editor page in the dashboard. Changes are saved to config_overrides.json and take effect immediately without restarting. Reset to defaults by clicking "Reset to defaults" or deleting the JSON file.
Key parameters to calibrate once 3–6 months of live data has accumulated:
SOFTMAX_TEMPERATURE — sharpness of regime probability distribution. Reduce toward 1.0 if Sentiment Driven dominates.
ZSCORE_WINDOW_WEIGHTS — relative influence of each lookback window. Downweight 240m and expanding to emphasise cycle-aware signal over long-run structural anchor.
REGIME_IDEALS — where each regime sits in (growth, inflation) z-score space. Most impactful calibration lever. Review against actual factor readings from live history.
SENTIMENT_RA_THRESHOLD / SENTIMENT_BOOST_WEIGHT — sensitivity of Sentiment Driven regime to risk appetite signal.
When satisfied with a calibrated set, use "Promote to config.py" in the Config Editor to make it the new default, then delete config_overrides.json.
| Option | Effort | Cost | Use case |
|---|---|---|---|
| Local + Task Scheduler | Done | Free | Daily operation, personal use |
| Streamlit Community Cloud | Low | Free | Showcase, demo — no persistent runner |
| Cloud VM (AWS/Azure) | Medium | ~$15/month | Always-on, shared access, proper production |
For always-on deployment without keeping a terminal open: run from a regular Command Prompt window (not VS Code terminal) and minimise rather than close. The server stays live as long as that window exists.
- Four-factor engine — growth, inflation, liquidity, risk appetite
- Five-regime probability vector with confidence scoring
- Multi-window z-score with configurable ensemble weighting
- Historical regime map — asset class performance by regime
- Six-page Streamlit dashboard with live/synthetic toggle
- Config Editor with override system and one-click reset
- Daily runner with state log and change detection
- Live data pipeline — FRED + Yahoo Finance
- Bloomberg integration — global CB balance sheets, full liquidity factor
- ALFRED vintage data — epistemically honest bootstrap history
- Regional expansion — US, Europe, Asia roll-up to global composite
- Calibration script — score parameter sets against historical anchor points
- Portfolio construction layer — regime-conditional position sizing
- Cloud deployment — always-on with authentication
Fullerton Fund Management, Fullerton Investment Views (FIV), Q1 2023–Q2 2026. All factor construction and regime taxonomy is inferred from public disclosures. Fullerton's internal weighting methodology and aggregation rules are proprietary and not replicated here.
This model is a systematic decision-support tool, not investment advice. Past regime-conditional returns are descriptive, not predictive.