An allocation process that can be replayed, argued with, and shown to be wrong

MacroSense: a regime and sentiment allocator that tests whether one person can build and run the repeatable allocation process an investment committee is supposed to run, on data anybody can look up. I built it alone. Each trading day it sorts the market into one of four regimes (market environments such as calm growth or rate stress) and sets weights across fifteen funds.

Sponsor
Self-directed
Status
In progress · not yet validated
Governing constraint
Architecture, Not Performance
Reading time
10 minutes

In progress MacroSense runs end to end. It has not been validated. The backtest (replaying the strategy over past data as if it were live) is built, but the history it can run over is fifty-six trading days. That cannot support a return, a ratio or a probability of beating a benchmark, so no performance figure appears on this page. Extending the history is the current work.

What this Engagement produced

  • 13 modules

    Each separable and runnable alone; the seven carrying the loop are named below with their inputs and outputs.
  • 4 macro regimes

    Calm Growth, Turbulent Growth, Rate Stress and Risk Off, each with its own allocation constraints.
  • 56 trading days of history

    All the history the backtest can run over so far.

The Situation

Allocation decisions made by hand are usually made once, defended in a meeting, and never recorded in a form anybody can re-run. Six months later there is no process to question, only a sequence of opinions.

A system that runs on a schedule from stated inputs, and writes down what it did and why, can be checked later. So I set out to build the whole loop alone, with no step that works only because somebody is watching.

MacroSense has two layers. The macro layer reads market data, classifies the regime, and sets weights across fifteen sector and credit exchange-traded funds. The company layer scores what companies say in their filings and earnings calls, and passes a sector score back to the macro layer.

One constraint was fixed at the start and never relaxed: every input is free and public. Treasury yields and credit spreads come from the Federal Reserve’s published series. Volatility and fund prices come from public market data. Company commentary comes from 10-K and 10-Q filings and earnings-call transcripts.

Why every input is free and public

Cost is part of it, but not the main part. A system built on a subscription cannot be reproduced by a reader or checked by somebody who doubts it. A process whose whole claim is that it repeats should not have a paywall in the middle of it. The price is that free sources go missing: identifiers get retired, endpoints change shape, requests come back empty. So each input has a list of backup sources, tried in order.

What I Did

I built every part myself. MacroSense has thirteen modules, each runnable on its own, and the seven below carry the daily loop. Each takes a stated input and returns a stated output.

Exhibit 1 The seven components that carry the loop

Select a component to see what goes in and what comes out.

View the data
Each component of the loop, with what it takes in and what it returns in the words of the paragraphs that state them, and the components its output goes to
ComponentInOutHands its output to
Data fetcherPublic macro series and public market prices — Treasury yields at two and ten years and the spread between them, high-yield and investment-grade credit spreads (the extra yield corporate bonds pay over Treasuries), the volatility index and its term structure (how expected volatility differs between the next month and the next three), and one- and three-month momentum (recent price return) for each of fifteen sector and credit exchange-traded funds.One dated row, fifty-eight columns wide.Regime classifier, Portfolio optimizer
Regime classifierA rolling window of at least twenty of those rows, plus sentiment signals for the energy and healthcare sectors.One of four regimes — Calm Growth, Turbulent Growth, Rate Stress, Risk Off — with a probability for each.Portfolio optimizer, Briefing, Dashboard
Portfolio optimizerThe regime and the current signal row.Weights across the same fifteen funds, summing to one.Briefing, Dashboard
Sentiment scorerEarnings-call transcripts and 10-K and 10-Q filings from the SEC’s public archive, collected by a companion updater that waits between requests to stay under the SEC’s rate limit.A score for each company on four dimensions — revenue tone, cost and margin direction, confident versus hedged language, newly disclosed risk — a weighted composite of the four, and a sector roll-up that goes back to the classifier as one of its features (the inputs a model learns from).Regime classifier, Briefing, Dashboard
SchedulerThe clock and the exchange calendar.A completed cycle, logged.No other component
BriefingThe regime name, the probability vector, the previous weights, the new weights, the sentiment signals.A few sentences of plain English saying what changed and why.Dashboard
DashboardThe regime and its four probabilities, the weights, the sentiment scores, and every past rebalance, which is saved to a local database.One page showing where the market stands, what the portfolio holds and why, and how both have changed over time.No other component
Source: published; each component’s input and output as the paragraphs below state them. The moving packets show which way data travels, not how much or when.

The data fetcher. In: public macro series and public market prices — Treasury yields at two and ten years and the spread between them, high-yield and investment-grade credit spreads (the extra yield corporate bonds pay over Treasuries), the volatility index and its term structure (how expected volatility differs between the next month and the next three), and one- and three-month momentum (recent price return) for each of fifteen sector and credit exchange-traded funds. Out: one dated row, fifty-eight columns wide.

The regime classifier. In: a rolling window of at least twenty of those rows, plus sentiment signals for the energy and healthcare sectors. Out: one of four regimes — Calm Growth, Turbulent Growth, Rate Stress, Risk Off — with a probability for each. The model is a four-state Gaussian hidden Markov model. It treats the market’s regime as something that can’t be seen directly, and works out which of the four states most likely produced the data. The probabilities matter more than the label: 0.41 against a runner-up at 0.39 says something different from 0.90. So the briefing gets all four probabilities, not just the top one.

The portfolio optimizer. In: the regime and the current signal row. Out: weights across the same fifteen funds, summing to one. Each regime changes the constraints (rules the weights must obey), not the model. In the two growth regimes the portfolio is long only, with a cap per position. In the two stress regimes short positions are allowed within tighter bounds, defensive holdings are favored, and rate-sensitive ones are penalized. A convex solver (a method that finds the single best set of weights under those rules) does the work. If the solver fails, the optimizer falls back to equal weights and logs the reason as an error.

Exhibit 2 Why the briefing gets all four probabilities, not just the top one

Pick the leading regime, then drag from a close call to a decisive one.

View the data
The worked example’s two calls (0.41 against 0.39, and 0.90), filled out to all four regimes so each sums to one, with Calm Growth leading and Turbulent Growth the runner-up; picking another leading regime gives it the first row’s values, the next row round the runner-up’s, and the rest the remaining values in this order. Each regime’s constraint set, as this page describes it
RegimeConstraint setClose callDecisive
Calm GrowthLong only, with a cap per position0.410.90
Turbulent GrowthLong only, with a cap per position0.390.06
Rate StressShort positions allowed within tighter bounds; defensive holdings favored, rate-sensitive ones penalized0.120.02
Risk OffShort positions allowed within tighter bounds; defensive holdings favored, rate-sensitive ones penalized0.080.02
Source: illustrative; the Case Study’s own worked example (0.41 against 0.39, and 0.90), not model output. The other probabilities are filled in so each call sums to one.

The sentiment scorer. In: earnings-call transcripts and 10-K and 10-Q filings from the SEC’s public archive, collected by a companion updater that waits between requests to stay under the SEC’s rate limit. Out: a score for each company on four dimensions — revenue tone, cost and margin direction, confident versus hedged language, newly disclosed risk — a weighted composite of the four, and a sector roll-up that goes back to the classifier as one of its features (the inputs a model learns from). This is the company layer.

The scheduler. In: the clock and the exchange calendar. Out: a completed cycle, logged. Three jobs — a rebalance after each trading day’s close, a model retrain on the first of the month, a filings pull on Sunday. Each step can fail without stopping the rest, because the scheduler has to keep running for months with nobody watching it.

Exhibit 3 The scheduler’s week: three jobs, each on its own calendar

Pick the day the first of the month falls on, and watch the retrain move while the other jobs stay put.

View the data
The scheduler’s jobs and when each runs, in the words of the paragraph above
JobRuns
RebalanceAfter each trading day’s close
Model retrainOn the first of the month
Filings pullOn Sunday
Source: published; the three jobs as the paragraph above states them, drawn from those rules, not from the scheduler’s log. Every weekday is drawn as a trading day, with no exchange holiday.

The briefing. In: the regime name, the probability vector, the previous weights, the new weights, the sentiment signals. Out: a few sentences of plain English saying what changed and why. A language model writes them. With no API key available, it falls back to a fixed template, so the record has no gaps on days an outside service was unreachable.

The dashboard. In: the regime and its four probabilities, the weights, the sentiment scores, and every past rebalance, which is saved to a local database. Out: one page showing where the market stands, what the portfolio holds and why, and how both have changed over time.

The rule that kept the components separable

I wrote down a short list of interfaces (what each module takes in and hands back) and treated it as fixed. It covered what the classifier is called with and what it returns, that the optimizer returns weights keyed by ticker summing to one, and the five arguments the briefing takes. Everything behind those interfaces was rewritten repeatedly, the fetcher most of all, and none of those rewrites reached another module. Without fixed interfaces, every change is a whole-system change. Then you stop making improvements, because you can’t tell what each one will break.

What Changed

The system works end to end. On schedule it fetches, classifies, allocates, explains itself and logs the result. It has kept doing that, not just in a one-time demo. That is what I set out to build, and it is done.

Running it also turned up two defects. The optimizer’s equal-weight fallback is meant for a solver that fails, and it logs that as an error. But when the optimizer’s import failed, it took the same fallback and kept producing weights that looked normal. Separately, an alert channel was built but never configured, so every alert returned quietly and reached nobody.

The backtest is built too. It reuses the live classifier and the live optimizer unchanged, because a backtest that rewrites the strategy ends up testing the rewrite. It works in three steps. First, it walks the history forward one rebalance at a time, classifying on data up to that date and no further. Second, it applies the real daily fund returns that followed and charges a cost for turnover (how much of the portfolio is traded). Third, it compares the result with buying and holding the broad market. It computes what a reader would ask for: total return, a compound growth rate, a Sharpe ratio (return per unit of risk), volatility, maximum drawdown (the largest fall from a peak), results by regime, and a distribution of outcomes.

Pending Result Whether MacroSense allocates well is not answered here. The continuous history it has accumulated is fifty-six trading days, from 7 January to 27 March 2026. Replayed at the backtest’s default weekly rebalance that is roughly eleven decisions; monthly, two. Every ratio above would be annualized (scaled up to a yearly rate) from a single market environment three months long. A figure produced that way describes the window, not the system. The current work is backfilling the snapshot series so the model can run across several years and several regimes. The numbers go on this page when the window can carry them.

Exhibit 4 The whole window holds a handful of decisions

Pick a rebalance frequency to count the decisions the window holds.

View the data
The window, replayed at three rebalance frequencies, and the decisions each would make in it, as the paragraph above counts them; the daily count is one decision per trading day
RebalanceDecisions in the window
Every trading day56
Weekly, the backtest’s defaultroughly 11
Monthly2
Source: published; this page’s own figures for the window, 7 January to 27 March 2026. Marks are spaced evenly, not set on real dates, and the multi-year axis has no stated scale. Every return the backtest computes is withheld, because a window this short cannot support one.

The backtest also has no sentiment data. Those columns are missing from the history, so the classifier fills them with zeros. Only the macro layer has been tested; the company layer has not.

What I Learned

Fifty-six trading days is one market environment, not a sample. The first time the backtest drew an equity curve (the portfolio’s value over time) next to its benchmark, the shape felt like a result. It was not one. The ratios a backtest prints are annualized, which makes three months look like a year, and nothing in the output says so. Sample size decides whether a number means anything at all.

The harder lesson is that a system which runs is very satisfying, and the satisfaction is misleading. Every day the scheduler fires, a cycle completes, a briefing is written and a row lands in the database. None of that is evidence about whether the allocations are any good.

What it would take for a number to go on this page

A history long enough to contain all four regimes, with enough time in each to test that regime’s constraints against real returns. Scored sentiment across that history instead of zeros. And the result reported with its window, its costs, and the periods where it went the other way.

Where Else This Applies

Ask what would make the output wrong. Any automated process handed to a client, such as a nightly reporting refresh, a scoring model or a self-updating forecast, offers a clean run as its best evidence. A clean run says nothing about whether the output is right. The useful question is what would have to be true for the output to be wrong, and whether anybody has checked.

State the window in the same breath as the figure. Think of a six-week pilot, a two-week test, or a model checked against one quarter. Each gives an estimate uncertain enough to contain the opposite conclusion, and each is often reported as a finding. Report the window with the number, and never annualize a window too short to support it.

Make the degraded path announce itself. Both defects in What Changed kept producing output after a component had stopped working. That is worse than stopping, because nothing downstream can tell. Any monitoring or integration work meets this in some form, and a fallback that stays silent costs the most.

All six Engagements Ask me about this one