A trend-following system with a mean-reversion escape hatch, built on StratOS.
Write-up: Stepping aside when the trend gets ahead of itself
Hold a trending instrument. When price stretches unusually far above its moving average, step aside: that stretch is the part that gives back. Then scale back in on the way down, in chunks, rather than guessing the bottom in one trade.
| Instrument | TQQQ (3× leveraged NASDAQ-100) |
| Period | 2020-01-02 → 2026-09-11 · 6.7 years |
| Bars | Daily |
| Starting capital | $100,000 |
| Data | Yahoo Finance, split/dividend adjusted |
| MA Divergence | Buy and hold TQQQ | |
|---|---|---|
| Total return | +5,974.4% | +555.9% |
| CAGR | 84.7% | 32.5% |
| Sharpe ratio | 2.22 | 0.73 |
| Max drawdown | −33.5% | −81.7% |
| End value | $6,220,203 | $671,690 |
| Trades | 36 | 1 |
The flat stretches in that curve are the point of the whole exercise. They are the periods the strategy spent in cash, out of the market, while a 3× leveraged ETF did whatever it was going to do.
Both lines start with the same cash, trade the same bars, and pay the same execution rules. For the first two years they are nearly the same line: in a straight uptrend, a strategy whose baseline is "hold the instrument" has nothing to add. The separation is 2022: buy-and-hold falls 81.7% peak to trough and spends three years recovering; the strategy sat out most of it and compounded from a base it never gave back.
That is the whole thesis in one picture. The edge is not in the entries. It is in the quarters spent holding cash.
Beating an 81.7% drawdown with a 33.5% one is a more meaningful result than beating 32.5% CAGR with 84.7%, because the drawdown is what determines whether a real person is still holding the position when the recovery arrives.
The worst peak-to-trough loss was 33.5%, in early 2026. Over the same period holding TQQQ outright drew down 81.7%. A one-third drawdown is still a bad year by any normal standard. It is only respectable relative to the instrument.
| Year | Return | Start | End |
|---|---|---|---|
| 2020 | +108.15% | $102,401 | $213,151 |
| 2021 | +179.62% | $213,151 | $596,007 |
| 2022 | −13.33% | $596,007 | $516,543 |
| 2023 | +138.22% | $510,416 | $1,215,899 |
| 2024 | +101.32% | $1,154,265 | $2,323,787 |
| 2025 | +73.32% | $2,309,104 | $4,002,156 |
| 2026¹ | +56.52% | $3,974,067 | $6,220,203 |
¹ Partial year, through 11 September 2026.
Six up years and one down year is a flattering record, and it is also a six-and-a-half-year sample containing exactly one bear market. Treat the 2022 line (the only year the system had to defend rather than compound) as the most informative row in the table, not the worst one.
36 fills across 16 completed round trips.
| Win rate | 81.2% (13W / 3L) | Profit factor | 21.94 |
| Longest win streak | 5 | Longest losing streak | 1 |
| Median holding period | 47.5 days | Longest hold | 401 days |
| Average | Median | Worst | Best | |
|---|---|---|---|---|
| Return per round trip | +43.09% | +39.96% | −18.63% | +194.20% |
| Winners | +55.70% | +43.75% | +8.87% | +194.20% |
| Losers | −11.54% | −11.43% | −4.56% | −18.63% |
A profit factor near 22 is not a sign of a miraculous edge; it is what happens when a strategy takes small, capped losses and occasionally rides a leveraged instrument through a long trend. The shape matters more than the magnitude: losses cluster tightly between −4.6% and −18.6%, while the winners are open-ended.
| Period | Count | Worst | Median | Average | Best | % positive |
|---|---|---|---|---|---|---|
| Daily | 1,052 | −20.31% | +0.48% | +0.44% | +13.52% | 58% |
| Weekly | 350 | −12.85% | 0.00% | +1.02% | +28.86% | 40% |
| Monthly | 81 | −16.01% | +3.15% | +6.04% | +46.98% | 58% |
| Yearly | 7 | −13.33% | +101.32% | +91.97% | +179.62% | 86% |
The weekly row is the honest one: only 40% of weeks are positive. The median week is exactly zero, because the strategy is often in cash. Returns arrive in concentrated bursts, which is uncomfortable to hold in real time regardless of what the annual figures say.
The engine calls step() once per bar with candles through the last closed
bar plus the current bar's open, and fills whatever orders come back at that
open. Every rule below is therefore evaluated on information that was genuinely
available. Lookahead is impossible by construction, not by care.
Baseline. Hold the instrument while price is above its moving average and the average itself is rising.
Stretch exit. Leave when price closes far enough above the moving average. A leveraged ETF that has run a long way above trend is carrying a premium that historically comes back; the exit is a bet on that reversion, not on a top. "Far enough" is either a multiple of the average or a number of standard deviations above it: two profiles of the same idea.
Cross exit, confirmed. Leave when price closes below the average. A single close through a flat average is mostly noise, so the exit requires several consecutive closes beyond it and the next bar to open beyond it as well.
Slope filter. Ignore average-crossings while the average itself is flat (the regime where crossings mean least), and hold the current position instead. Band exits still fire: a violent stretch happens in a steep trend, and that is exactly when you want out.
Scale-in ladder. After an exit, buy back in steps as price falls, measured either from the average or from the running peak. Each rung deploys an equal dollar amount of the remaining cash, so a deeper fall buys more shares. Anything still uninvested when price breaks back above the average goes in at once.
Re-entry. Back in when price falls to the entry band, or, optionally, once the stretch has given back enough from its peak, whichever comes first.
The calibration is not published. The mechanism above is complete and the implementation is in
src/ma_divergence/strategy.py, but the thresholds, windows and ladder that produced the results on this page are not in this repository.params.example.tomlships illustrative values so the code runs; it will not reproduce the numbers above.
git clone https://github.com/gocmenco/ma-divergence.git
cd ma-divergence
make init # venv + stratos from git
python main.py # writes output/*.html
python main.py --show # ...and opens itYahoo Finance data, no API key, a couple of minutes. The HTML report is far richer than this page: interactive equity and drawdown, monthly tables, the return distribution, round-trip statistics, the full configuration, and a journal of every trade.
python main.py --profile stddev # standard-deviation bands
python main.py --ticker QQQ --start 2015-01-01
python main.py --capital 25000 --no-benchmark
python main.py --export-csv # series for tools/make_charts.pymain.py is the whole program, top to bottom: parameters in, strategy built,
backtested, reported, and compared against simply holding the same ticker.
Start there.
All the numbers live in params.toml, which is git-ignored. A fresh clone falls
back to params.example.toml and says so on startup. Copy it and put your own
calibration in:
cp params.example.toml params.tomlstrategies/ holds two runnable configurations (one per band type) found by
the StratOS CLI:
stratos list
stratos run tqqq_ma_divergence --dry-runstratos run finds them because StratOS searches ./strategies under the
working directory. That is the entire integration: no registration, no plugin
manifest. On cron:
15 18 * * 1-5 cd /path/to/ma-divergence && .venv/bin/stratos run tqqq_ma_divergence
The runner decides and prints. It does not place orders and does not notify
anyone. If you want it to act, wire stratos.trader into run(); if you want
the signal delivered somewhere, pipe the output or add your own notifier.
from ma_divergence import MaDivergence, MaDivergenceConfig
from ma_divergence.params import load_params
params = load_params("fixed")
strategy = MaDivergence(ma_divergence_config=MaDivergenceConfig(...), ...)
strategy.backtest()Everything above is a backtest. Backtests are the most flattering way to look at a trading system, and this one has every advantage:
- The parameters were chosen by looking at backtests. That is what makes them worth exactly what they do out of sample, and no more.
- One backtest is a single draw from a jagged surface. Adjacent parameter values on a system like this can differ by tens of CAGR points. That spread is noise, not a peak, and picking the top of it is how you fool yourself.
- Six and a half years is one regime and a bit. 2020–2026 was, with one interruption, an extraordinary run for leveraged NASDAQ exposure. A strategy whose baseline is "hold TQQQ" inherits that. The interesting question is not whether it beat cash; it is what happens in a decade that looks like 2000–2010.
- The comparison that matters is buy-and-hold, not zero. It is above, and
main.pyprints both every run for exactly this reason. - Costs are modelled but light. Whole-share fills at the next open, no commission, no borrowing cost, no slippage assumption beyond the open price.
- Leveraged ETFs decay. TQQQ's daily reset means its long-run behaviour depends on the path, not just the destination. A backtest captures this only as far as the price history does.
Before believing any number here, run the split: a parameter worth keeping sits
near the top of both halves of the period, not at the full-period maximum.
stratos.optimizer has the tools (grid sweeps, walk-forward efficiency,
Deflated Sharpe, CSCV/PBO, a robustness gate), and its CLAUDE.md in the
StratOS repo is the protocol.
Nothing here is investment advice, a recommendation, or a claim about the future. It is a worked example of building a strategy on StratOS.
main.py backtest + report: start reading here
params.example.toml every knob, with illustrative values
src/ma_divergence/ the strategy class and the parameter loader
strategies/ runnable configurations, found by `stratos run`
tools/make_charts.py regenerates the charts on this page
tests/ offline smoke tests
docs/ the charts on this page
output/ generated reports (git-ignored)
make test # pytest
make format # isort + autoflake + black
make install-local-stratos # develop against a local ../stratos checkoutRegenerating the charts above:
python main.py --export-csv
python tools/make_charts.pyMIT. See LICENSE.



