Vol. I · No. 07— · — · —--:--:-- EST
Tanmay DabhadeThe Journal

Build LogJune 3, 2026·8 min read·No. 01

From Zero to Macro Regime Filters: My First 3 Days Building Trading Algorithms in Python

Three days into a 90-day quant challenge: from unlearning prediction to shipping a macro-reactive regime filter that made money through the COVID crash.

I recently started a 90-Day Challenge to learn quantitative research and algorithmic trading from scratch. My goal wasn't to find a magic formula to "beat the market," but to learn how to build robust, mathematical systems that manage risk and react to macroeconomic environments.

I decided to use QuantConnect (and their LEAN engine) as my Python testbench. Over my first 72 hours, I went from not knowing how an event-driven system worked to building a Tactical Asset Allocation model that successfully navigated the 2020 COVID crash.

Here is exactly how the first three days unfolded, the traps I fell into, and the code I used to fix them.

Day 1: Unlearning "Prediction" and Understanding "Research"

The biggest trap new algorithmic traders fall into is treating the market like a puzzle to be solved.

My first lesson was a harsh reality check: Quantitative research is experimentation, not prediction. You aren't trying to guess what a stock will do tomorrow; you are looking for an edge with a positive expected value over thousands of occurrences.

Before writing a single line of code, I had to understand why most backtests lie. If a backtest looks like a 45-degree line straight up, it's usually broken due to:

  • Survivorship Bias: Testing on today's S&P 500 ignores all the companies that went bankrupt over the last decade.
  • Lookahead Bias: Accidentally letting your algorithm peek at tomorrow's closing price to make today's trading decision.
  • Curve Fitting (Overfitting): Adding so many hyper-specific parameters that the algorithm perfectly memorizes the past but blows up in live trading.

With the mindset shifted, I learned the core strategy archetypes — specifically Trend Following (riding macro waves) and Mean Reversion (betting that extreme panic or greed will eventually snap back to average).

Day 2: The "Hello World" Strategy and the 33% Drawdown

On Day 2, it was time to code. QuantConnect doesn't use static CSV files; it uses an Event-Driven State Machine. Time moves forward tick-by-tick, and your OnData function must react to that specific slice of time.

I built the classic "Hello World" of quant finance: a 50-day / 200-day Simple Moving Average (SMA) Crossover on the S&P 500 (SPY).

The Result: It made a 17.5% return in 2020. The Reality: It completely failed as a quantitative strategy.

Because moving averages are lagging indicators, my algorithm didn't sell when the March 2020 COVID crash hit. It just froze and rode the market down, suffering a massive 33.7% drawdown.

The Fix: Hard Stops and Strategy Handoffs

To fix this, I had to introduce a structural memory to the algorithm and build a Hybrid Strategy.

  1. The Stop-Loss: I added a hard portfolio-level rule: if a position drops 10%, liquidate immediately.
  2. The Regime Shift: Once stopped out, the algorithm switched from "Trend Following" to "Mean Reversion." It waited for the Relative Strength Index (RSI) to drop below 30 (oversold conditions) to buy the absolute bottom of the crash.
  3. The Handoff: Once the market recovered and the SMAs crossed back over, the algorithm seamlessly handed the state back to "Trend Following" to ride the rest of the bull market.

My drawdown shrank to under 10%, and my Sharpe Ratio nearly doubled.

Day 3: Escaping the "Diversification Tax"

I wanted to make the portfolio even safer, so I diversified. I allocated 50% of my capital to Equities (SPY) and 50% to Long-Term Treasury Bonds (TLT), which traditionally spike during periods of recession or financial panic.

It worked perfectly — my drawdown dropped to 5%.

But when I looked at the returns, I realized I was paying a massive "Diversification Tax." By rigidly forcing half of my capital into slow-moving bonds during the massive 2020 V-shaped recovery, I artificially anchored my upside. I was paying a heavy tax for safety.

The Ultimate Solution: Tactical Asset Allocation (Risk-On / Risk-Off)

Instead of statically holding both assets, I realized institutional systems use macroeconomic indicators as a "traffic light" to dynamically shift capital.

I scrapped the static 50/50 split and built a Tactical Regime Filter using the 200-day SMA of the S&P 500 as my macro indicator.

  • 🟢 Risk-On (Greed): If the market is above its 200-day trend, the economy is healthy. Allocate 100% to Equities.
  • 🔴 Risk-Off (Panic): If the market drops below its 200-day trend, something is systemically broken. Dump all equities and hide 100% of capital in Long-Term Bonds.

Here is the full algorithm I ran in QuantConnect. Each symbol carries its own indicators and regime state, so SPY and TLT are managed independently through the same trend → stop-out → recovery → handoff loop:

from AlgorithmImports import *

class SymbolData:
    def __init__(self, algo, symbol):
        self.symbol = symbol
        self.fast_sma = algo.SMA(symbol, 50, Resolution.HOUR)
        self.slow_sma = algo.SMA(symbol, 200, Resolution.HOUR)
        self.rsi = algo.RSI(symbol, 14, MovingAverageType.SIMPLE, Resolution.HOUR)

        self.stopped_out = False
        self.current_trade_label = None

class MultiAssetAllocationAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2020, 1, 1)
        self.set_end_date(2021, 1, 1)
        self.set_cash(100000)

        tickers = ["SPY", "TLT"]
        self.symbol_data = {}
        for ticker in tickers:
            ticker_symbol = self.add_equity(ticker, Resolution.HOUR).symbol
            self.symbol_data[ticker_symbol] = SymbolData(self, ticker_symbol)

        self.set_warm_up(200)
        self.target_weight = 1.0 / len(tickers)
        self.debug("Multi Asset Handoff Strategy Compiled and Running")

    def on_data(self, data: Slice):
        if self.is_warming_up:
            return

        for symbol, sd in self.symbol_data.items():
            if not data.contains_key(symbol) or data[symbol] is None:
                continue

            fast_value = sd.fast_sma.Current.Value
            slow_value = sd.slow_sma.Current.Value
            rsi_value = sd.rsi.Current.Value

            # Hard stop: a 10% loss liquidates and flips this symbol into crash mode.
            if self.portfolio[symbol].invested:
                pnl = self.portfolio[symbol].unrealized_profit_percent
                if pnl < -0.10:
                    self.liquidate(symbol)
                    sd.stopped_out = True
                    sd.current_trade_label = None
                    self.debug(f"SL hit on {symbol.value}! Switching to crash regime")
                    continue

            if not self.portfolio[symbol].invested:
                if sd.stopped_out:
                    # Mean reversion: wait for extreme panic before re-entering.
                    if rsi_value < 30:
                        self.set_holdings(symbol, self.target_weight)
                        sd.current_trade_label = "RECOVERY"
                        sd.stopped_out = False
                        self.debug(f"V-Shape Setup! Buying {symbol.value} via RSI.")
                else:
                    # Trend following: buy the golden cross.
                    if fast_value > slow_value:
                        self.set_holdings(symbol, self.target_weight)
                        sd.current_trade_label = "TREND"
                        self.debug(f"Trend setup! Buying {symbol.value} via SMA.")
            else:
                if sd.current_trade_label == "TREND":
                    if fast_value < slow_value:
                        self.liquidate(symbol)
                        sd.current_trade_label = None
                        self.debug(f"Trend broken, exiting {symbol.value} via SMA.")
                elif sd.current_trade_label == "RECOVERY":
                    # Handoff: once the trend re-asserts, hand the position back to trend following.
                    if fast_value > slow_value:
                        sd.current_trade_label = "TREND"
                        self.debug(f"Handoff successful for {symbol.value}: switching to trend following.")

When I ran this backtest over 2020, the results held up.

BacktestJan 2020 – Jan 2021
$116,573+16.57%
StrategySPY buy & hold
Net Profit
16.573%
CAGR
16.540%
Sharpe Ratio
1.067
Max Drawdown
10.700%
Win Rate
50%
Profit-Loss Ratio
3.04
Alpha
0.114
Total Orders
17

As the COVID panic began in late February, the algorithm instantly sensed the regime shift, dumped all equities, and bought TLT. It actually made money during the generational crash, and then cleanly shifted back to 100% SPY months later to ride the massive bull market to the finish line.

What's Next?

In just three days, I moved from lagging indicators to a dynamic, macro-reactive state machine. But everything I've built so far still relies on the market going up eventually.

For Day 4, I am stepping outside of directional trading entirely to explore Market Neutrality and build a Statistical Arbitrage (Pairs Trading) system.

If you are on your own quant journey, I'd love to hear how you handle regime switching in your own architecture.

— ◆ —
TD
Tanmay Dabhade

Systems engineer and CS student at Michigan State ('27). Building backend systems, data pipelines, and full-stack tools — and writing about the messy middle.