Tag: backtest

Can Donchian Channel Breakouts Beat Buy-and-Hold? A 3-Year QQQ Test

Research by quantstr.at Quantitative Team
Backtest window 2016-01-01 to 2025-01-01 · Published oct. 07, 2026
⚠ Integrity audit: 4 of 6 checks passed
ResultsBreakdownRiskMonthly ReturnsSetup & Audit

What Did We Test?

This strategy aims to capture price movements by entering long positions when specific conditions, based on recent price action, signal potential upward trends. The approach uses two indicators: the 20-day high, which reflects the highest price over the last 20 days, and two different exponential moving averages (EMAs). The 10-day EMA, which averages prices over ten days while giving more weight to recent prices, must be greater than the 30-day EMA, a longer-term average. This setup suggests that the short-term momentum is stronger than the longer-term trend, potentially indicating a bullish phase.

When the conditions for entry are met, the strategy will exit either when the price drops to the 10-day low or when a trailing stop is reached, set at twice the Average True Range (ATR). The ATR is a measure of market volatility, providing a dynamic stop that adjusts based on recent price fluctuations.

Component Description
Indicator 20-day high represents the highest price in the past 20 days; 10-day EMA is the average price over the last 10 days, weighted toward recent prices; 30-day EMA is the average over the last 30 days.
Signal Enter long when the price breaks above the 20-day high and the 10-day EMA is greater than the 30-day EMA.
Rule Exit the position when the price reaches the 10-day low or hits a trailing stop set at twice the ATR.

What Happened?

The backtest aimed to evaluate a strategy using price indicators to identify potential upward trends in seven symbols from January 1, 2016, to January 1, 2025. Starting with an initial equity of $100,000, the strategy ended with a value of approximately $207,892, resulting in a total return of 107.89%. However, the strategy lagged behind both benchmark comparisons. The equal-weight buy-and-hold basket achieved a total return of 244,286.40%, while the buy-and-hold QQQ index generated a return of 399.43%.

One significant caveat of this backtest is that while it generated a positive overall return, the strategy exhibited a maximum drawdown of 25.63%. This decline was notably less severe compared to the 60.66% maximum drawdown of the equal-weight buy-and-hold basket and 35.12% of the QQQ index, indicating better risk management in this regard. Despite this, the strategy’s overall performance did not match the benchmarks, highlighting the importance of evaluating risk alongside return.

Strategy
+107.9% (CAGR: +8.5%)
Worst decline: -25.6%
Equal-weight buy & hold basket (7 symbols)
+244286.4% (CAGR: +138.1%)
Worst decline: -60.7%
Buy & hold QQQ
+399.4% (CAGR: +19.6%)
Worst decline: -35.1%

How Did It Compare With the Benchmarks?

Side-by-Side Comparison
Benchmark CAGR and drawdown are computed from daily closes over the same window; Sharpe is omitted because the methodologies differ
Metric Strategy Equal-weight buy & hold basket (7 symbols) Buy & hold QQQ
Total return +107.9% +244286.4% +399.4%
Annualized return (CAGR) +8.5% +138.1% +19.6%
Max drawdown -25.6% -60.7% -35.1%
Ending equity $207,892 $244,386,399 $499,433
Backtest Summary
Every figure computed from this run’s saved results
Metric Value
Starting capital $100,000
Ending capital $207,892
Total return +107.89%
Annualized return (CAGR) +8.47%
Closed trades 152
Sharpe ratio (annualized) 1.34
Calmar ratio 1.08
Max drawdown -25.63%
Equal-weight buy & hold basket (7 symbols) +244286.40%
Strategy vs. Equal-weight buy & hold basket (7 symbols) -244178.51 pp
Buy & hold QQQ +399.43%
Strategy vs. Buy & hold QQQ -291.54 pp

The strategy executed a total of 152 trades during the backtest period from January 1, 2016, to January 1, 2025. It achieved a total return of 107.89%, which translates to a compounded annual growth rate (CAGR) of 8.47%. However, this performance lagged behind the benchmark of an equal-weight buy-and-hold basket, which returned 244,286.40%, indicating the strategy underperformed by a significant margin.

The Sharpe ratio stands at 1.34, which means that the returns earned were 1.34 times the volatility taken on. In other words, for each unit of risk, the strategy generated a positive return, suggesting a reasonable risk-reward balance. At the trade level, the absence of closed trades listed means that there are no trade details to analyze; thus, it is hard to gauge specific patterns in profitability or consistency.

The monthly returns reveal that the strategy had 22 positive months compared to 13 negative months, indicating a tendency for more frequent gains. The average return during positive months was higher than the average loss during negative months, specifically demonstrating that gains were stronger on average, this pattern aligns with the strategy’s design to capitalize on upward momentum while managing risk through its exit criteria.

Risk-Adjusted Return Metrics
Every number here comes straight from this backtest’s quantstr.at report
Metric Value What It Measures
Annualized Return 27.79% Return scaled to a one-year rate
Annualized Volatility 20.74% How much returns swing year to year
Sharpe Ratio 1.340 Return per unit of total volatility — the classic risk-adjusted return measure
Sortino Ratio 0.130 Return per unit of downside volatility only — ignores upside swings that Sharpe penalizes unfairly
Calmar Ratio 1.085 Return relative to the worst peak-to-trough drawdown
Omega Ratio 0.290 Probability-weighted ratio of gains to losses — doesn’t assume a normal return distribution the way Sharpe does
Downside Deviation 0.811% Volatility of losing periods only
Upside Potential Ratio 0.756 Upside capture relative to downside risk — a Sortino-style ratio facing the other direction
Probability Sharpe > 0 99.15% Statistical confidence the strategy’s TRUE Sharpe ratio (not just this one sample) is greater than zero
Min. Track Record Needed 358 Minimum number of return observations needed for that confidence level to be meaningful
Sharpe Statistically Significant? Yes Whether this backtest has enough history for its Sharpe ratio to be statistically real, not noise

Risk-Adjusted Returns

A raw return figure can be misleading because it doesn’t account for the risk taken to achieve those returns. For example, a strategy might return 20% by taking significant risks, but a more modest 10% return could be achieved with much less volatility.

In this backtest, the Sharpe ratio of 1.34 indicates that the strategy generated returns 1.34 times the total volatility, suggesting a reasonable risk-reward balance. The Sortino ratio of 0.13, however, focuses only on downside volatility, revealing that the risk of losing is not well compensated by the upside, which contrasts starkly with the higher Sharpe. The Calmar ratio of 1.08 compares the return to maximum drawdown, further underscoring the strategy’s susceptibility to significant declines.

The "Probability Sharpe > 0" at 99.15% suggests strong statistical confidence that the true Sharpe ratio is positive, which is meaningful. Additionally, since this backtest’s Sharpe ratio is statistically significant, we can trust it provides insightful data. For your own reports, start by comparing the Sharpe and Sortino ratios, followed by the Calmar ratio for a comprehensive risk assessment.

Where Did the Result Come From?

The results of this strategy are concentrated among just a few symbols. The analysis shows that out of the seven symbols traded, only three contributed significantly to the overall profit. Specifically, QQQ led with a net profit of $51,119 from 37 trades and a win rate of 54.1%. MSFT followed with $35,846 from 37 trades and a slightly lower win rate of 45.9%. In contrast, NVDA, while generating a profit of $15,656 from 41 trades, had a win rate of only 29.3%. Other symbols, such as AAPL, had a modest contribution of $6,664 from 37 trades, but there were no trades for XLF, XLP, and XLE.

The trade-level significance test indicates that the results should be interpreted with caution. Since there were multiple trades across a few symbols and the p-value reflects the statistical relevance of the results, it is important to note that the nature of overlapping trades means the p-value serves as an indicative measure rather than conclusive evidence.

Performance by Symbol
7 symbols, closed trades only
Symbol Trades Net P&L Win rate Profit factor
QQQ 37 +$51,119 54.1% 2.95
MSFT 37 +$35,846 45.9% 2.19
NVDA 41 +$15,656 29.3% 1.58
AAPL 37 +$6,664 40.5% 1.28
XLF 0 – – –
XLP 0 – – –
XLE 0 – – –

How Bad Did It Get?

The strategy experienced a maximum drawdown of 25.63%, which means that the account value fell by that percentage from its peak to its lowest point during the backtest. This drawdown lasted for approximately 193 days before hitting the trough and took an additional 60 days to recover. Such statistics indicate that a trader might face considerable losses before seeing their investment recover to previous highs.

In terms of risk-adjusted return metrics, the Sharpe ratio is 1.34, suggesting that the returns generated were relatively favorable compared to the level of risk taken. However, the Sortino ratio of 0.13 indicates a substantial disparity between upside potential and downside risk. This divergence highlights a caveat: although the total returns are positive, they may not adequately compensate for the risks involved, especially given that the average drawdown length was 17.82 days. This risk profile should give potential traders pause, as it signals that while profits exist, they come with significant volatility.

The integrity audit failed due to two key issues: first, the trade list did not reconcile with the per-symbol trade statistics, indicating a potential mismatch in reported trades. Second, there was no verification of the gross long exposure relative to the portfolio equity, leaving uncertainty about the strategy’s true risk exposure. These failures suggest that the reported performance might not fully reflect reliable trade execution or risk management.

What Stood Out

  • The strategy had a total return of 107.89%, but this was significantly lower than the benchmark’s return of 244,286.40%, highlighting a considerable performance gap.
  • Despite a positive monthly return count of 22, the total profit did not keep pace, raising questions about the effectiveness of individual trades contributing to the overall strategy.
  • The maximum drawdown of 25.63% indicates notable potential exposure to risk, which could be unexpected given the strategy’s positive return trend.

Limits of This Backtest

  • The integrity audit failed, specifically highlighting that the trade list did not reconcile with per-symbol trade statistics, suggesting potential discrepancies in the reported performance.
  • There were no closed trade details available, making it difficult to analyze specific performance metrics or patterns.
  • The gross long exposure could not be verified relative to portfolio equity, meaning the reported performance may overstate actual returns.
  • The total length of the backtest period is 9.00 years, which, while substantial, may not fully capture varying market conditions necessary for a comprehensive evaluation of the strategy.

Monthly Returns

Monthly Returns Matrix (%)
Month-by-month and YTD cumulative performance summary
Year Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec YTD
2023 0.0% 2.2% 5.8% 2.9% 11.4% 2.5% 4.9% -5.7% -2.5% 1.0% 13.4% 2.1% 43.1%
2024 4.1% 0.4% -0.6% -2.8% 2.8% 26.5% -1.4% -0.6% 1.3% -3.9% 0.5% 6.2% 33.7%
2025 -9.5% -0.6% -6.7% -5.1% 13.4% 7.9% 11.4% 0.7% 3.1% 7.2% -8.1% -2.1% 8.6%

Over the tracking period from January 2023 to January 2025, the strategy experienced a mix of monthly returns, with 22 positive months and 13 negative months. The best month was June 2024, which had a return of 24.48%. Conversely, the worst month was January 2025, with a return of -6.33%.

The most significant drawdown occurred between July 11, 2024, and April 16, 2025, where it reached a depth of -25.63%. This episode aligns with the overall trend of returns, showing that significant declines occurred during a challenging market period.

Conclusions

This backtest revealed a total return of 107.89% over nearly nine years, which amounted to a compounded annual growth rate (CAGR) of 8.47%. However, this performance significantly lagged behind the benchmark of an equal-weight buy-and-hold basket that returned an astonishing 244,286.40%.

The primary caveat to consider is the considerable maximum drawdown of 25.63%, which indicates potential for substantial losses during the backtest period. Moreover, the integrity audit highlighted issues, particularly regarding the reconciliation of trade statistics, suggesting that the reported figures may not fully reflect reliable trade execution. Always remember, this walkthrough demonstrates past performance and does not predict future results. If you’re interested, feel free to explore your own strategy ideas using quantstr.at.

Ideas to Test Next

The strategy’s maximum drawdown of 25.63% indicates significant risk exposure, which could make it challenging for traders during downturns. Consider adjusting the exit conditions to include a stop-loss to potentially limit losses and manage risk more effectively.

Enter long when the price breaks above the 20-day high and the 10-day EMA is greater than the 30-day EMA. Exit when the price reaches the 10-day low or hits a trailing stop set at twice the ATR, or if the price falls below a stop-loss of 1.5% from the entry price.

More Backtests

Setup, Audit & Replication

Backtest Setup
Read from this run’s saved configuration
Parameter Setting / Value
Asset universe 7 symbols (XLF, XLP, XLE, AAPL, MSFT, NVDA, QQQ)
Time window 2016-01-01 to 2025-01-01 (9.0 years)
Starting equity $100,000
Benchmarks Equal-weight buy & hold basket (7 symbols) (+244286.4%); Buy & hold QQQ (+399.4%)
Full per-symbol trade statistics ▾
Trade Statistics by Symbol
Detailed performance and risk metrics breakdown per traded asset
Symbol Trades Net P&L Win Rate Loss Rate Max Equity Min Equity Gross Profits Gross Losses Max Drawdown Transactions Avg Daily P&L Avg Trade P&L Ending Equity Largest Winner Largest Loser Profit Factor Avg Winning Trade Avg Losing Trade Median Trade P&L Annualized Sharpe Daily P&L Std Dev Trade P&L Std Dev Avg Win/Loss Ratio Median Losing Trade Median Winning Trade Median Win/Loss Ratio Profit / Max Drawdown
XLF 0 $0 — — — — — — — — — — — — — — — — — — — — — — — — —
XLP 0 $0 — — — — — — — — — — — — — — — — — — — — — — — — —
XLE 0 $0 — — — — — — — — — — — — — — — — — — — — — — — — —
AAPL 37 $6,664 40.5% 59.5% $15,684 -$1,542 $30,859 -$24,195 -$17,226 74 $180 $180 $6,664 $6,807 -$5,467 1.28 $2,057 -$1,100 -$229 1.27 $2,243 $2,243 1.87 -$868 $1,409 1.62 0.39
MSFT 37 $35,846 45.9% 54.1% $42,783 $0 $66,072 -$30,227 -$16,173 75 $969 $969 $35,346 $25,652 -$4,329 2.19 $3,887 -$1,511 -$171 3.09 $4,977 $4,977 2.57 -$1,195 $1,403 1.17 2.19
NVDA 41 $15,656 29.3% 70.7% $26,302 $0 $42,555 -$26,899 -$18,061 83 $382 $382 $16,172 $22,605 -$3,469 1.58 $3,546 -$928 -$63 1.41 $4,304 $4,304 3.82 -$948 $538 0.57 0.90
QQQ 37 $51,119 54.1% 45.9% $59,632 -$1,585 $77,361 -$26,243 -$11,237 75 $1,382 $1,382 $49,711 $18,476 -$4,199 2.95 $3,868 -$1,544 $190 4.97 $4,414 $4,414 2.51 -$1,776 $1,472 0.83 4.42

Backtest Integrity Audit: 4 of 6 checks passed

  • ✅ Headline Return Matches Ending vs Starting Equity: From dollars: 107.89% | Reported field: 107.89% | Gap: 0.000 pp
  • ✅ Equity Curve Starts at Initial Equity and Ends at Ending Value: First point: $100,000 (initial $100,000) | Last point: $207,892 (ending $207,892)
  • ✅ Monthly Returns Compound to Headline Return: Compounded months: 107.89% | Headline: 107.89% | Gap: 0.00 pp
  • ⚠️ Trade List Reconciles With Per-Symbol Trade Statistics: Trade-list rows: 0 | open at end: 0 | closed per trade statistics: 152 | open rows isolated: yes
  • ✅ Sharpe Ratio Consistent With Annualized Return and Volatility: Reported Sharpe: 1.34 | Annualized return / volatility: 1.34
  • ⚠️ Gross Long Exposure Never Exceeds Portfolio Equity: No position series in results; cannot verify

Does a Market Trend Filter Beat Buy-and-Hold? An Out-of-Sample Test on 989 Stocks

Research by quantstr.at Quantitative Team
989 tickers · 2015-01-01 to 2026-09-30 · Published oct. 03, 2026

What Did We Test?

This is a market-timing strategy that uses the S&P 500 index fund, SPY, as a broad market filter. It holds the tested stock when SPY closes above its 100-day simple moving average, or SMA. An SMA is the average closing price over a specified number of trading days, so this rule treats SPY as being in a stronger market condition when its current close is above that average.

The strategy does not use a separate stock-level indicator. It assigns 100% of the allotted capital to the stock when the market condition is satisfied, with no leverage, and moves to cash otherwise. The condition is evaluated at each day’s close, with any resulting action taken at the next day’s open.

Component Description
Indicator SPY 100-day simple moving average: the average of SPY’s closing prices over the 100-day lookback period.
Signal A buy or hold signal occurs when SPY closes above its own 100-day SMA. A cash signal occurs when SPY does not close above that average.
Rule At the next day’s open, hold the tested stock with 100% of allotted capital when the buy or hold signal is present. Otherwise, exit the stock position and hold cash.
How the Rule Was Chosen (Training Data Only)
9 variants evaluated on 214 training tickers, 2006-01-01 to 2014-12-31; none of this data is in the test below
Variant Sharpe (strategy) Sharpe (buy & hold) Max drawdown (strategy) Max drawdown (buy & hold) Selected
Own-trend filter, 100-day 0.63 0.76 28.63% 52.58%
Market-trend filter, 100-day 1.07 0.76 19.96% 52.58% Yes
Own + market trend filter, 100-day 0.88 0.76 12.16% 52.58%
Own-trend filter, 150-day 0.63 0.76 26.88% 52.58%
Market-trend filter, 150-day 0.84 0.76 27.52% 52.58%
Own + market trend filter, 150-day 0.80 0.76 16.01% 52.58%
Own-trend filter, 200-day 0.64 0.76 26.42% 52.58%
Market-trend filter, 200-day 0.80 0.76 27.70% 52.58%
Own + market trend filter, 200-day 0.74 0.76 21.32% 52.58%

What Happened?

The market-trend filter, which held each stock only while SPY was above its 100-day moving average, beat buy-and-hold by CAGR on 306 of 989 tickers, or 30.9%, during 2015-01-01 to 2026-09-30. The typical result was worse than buy-and-hold, with median excess CAGR of -2.68 percentage points. In the equal-weight portfolio, the strategy had an 11.80% CAGR versus 16.33% for buy-and-hold, while its maximum drawdown was 23.13 percentage points smaller and its Sharpe ratio was higher by 0.05.

The main caveat is that the tickers were current S&P 500, S&P 400, and S&P 600 constituents, so companies removed from those indexes during the window are absent, creating survivorship bias. Also, the 989 tickers were not independent: their mean pairwise return correlation was 0.29, and the portfolio excess-return interval ran from -12.42 to 2.67 percentage points, which includes zero.

Headline Results
Every ticker is its own backtest, compared with buying and holding that ticker
Metric Value
Tickers tested 989
Strategy beat buy-and-hold (CAGR) 306 of 989 (30.9%)
Mean excess CAGR -2.40 pp
Median excess CAGR -2.68 pp
Bootstrap 95% interval for the mean -2.86 to -1.93 pp
Smaller max drawdown than buy-and-hold 81.5% of tickers
Median max drawdown (strategy vs buy-and-hold) 51.83% vs 62.86%
Median time in market 79.1%

How Many Tickers Did It Beat Buy-and-Hold On?

Distribution of excess CAGR

Strategy vs buy-and-hold CAGR

The strategy beat buy-and-hold on an annualized growth rate (CAGR) basis for 306 of 989 tickers, or 30.9%. The excess CAGR versus buy-and-hold had a mean of -2.40 percentage points, a median of -2.68 percentage points, a 10th percentile of -9.15 percentage points, and a 90th percentile of 4.37 percentage points. This means the strategy lagged on most tickers, although the gap was positive for some.

The strategy had a smaller maximum drawdown, the largest decline from a previous peak, on 81.5% of tickers. Median maximum drawdown was 51.83% for the strategy versus 62.86% for buy-and-hold. Its Sharpe ratio, a return measure adjusted for volatility, was better on 32.2% of tickers, with median values of 0.39 versus 0.45, and median time in the market was 79.1%.

Results were consistent in the broad share of winners across the two sub-periods: the strategy beat buy-and-hold on 33.7% of tickers in the first half and 33.5% in the second half. Median excess CAGR remained negative in both, at -22.83 percentage points and -18.64 percentage points. By sector, Communication Services had the highest beat share at 48.5%, followed by Real Estate at 45.3% and Consumer Discretionary at 41.2%; Utilities stood out on the low end at 2.7%, while every sector listed had a negative median excess CAGR.

By Sector
Sectors with at least 15 tickers
Sector Tickers Beat buy-and-hold Median excess CAGR
Communication Services 33 48.5% -0.70 pp
Consumer Discretionary 131 41.2% -1.01 pp
Consumer Staples 51 19.6% -2.72 pp
Energy 42 28.6% -3.03 pp
Financials 176 38.6% -1.26 pp
Health Care 109 36.7% -2.03 pp
Industrials 174 24.7% -3.99 pp
Information Technology 124 21.0% -4.76 pp
Materials 48 14.6% -3.91 pp
Real Estate 64 45.3% -0.10 pp
Utilities 37 2.7% -4.19 pp
Best and Worst Tickers
Ranked by strategy CAGR minus buy-and-hold CAGR
Group Ticker Strategy CAGR Buy & hold CAGR Trades
Best 10 (vs buy-and-hold) DAVE 61.6% 0.7% 21
Best 10 (vs buy-and-hold) CVNA 84.1% 39.8% 39
Best 10 (vs buy-and-hold) RELY 15.2% -16.5% 15
Best 10 (vs buy-and-hold) BE 71.3% 39.9% 33
Best 10 (vs buy-and-hold) ARLO 26.2% -4.8% 33
Best 10 (vs buy-and-hold) PTCT 25.4% 1.8% 56
Best 10 (vs buy-and-hold) KD -3.7% -26.0% 15
Best 10 (vs buy-and-hold) OMCL 17.0% 0.3% 56
Best 10 (vs buy-and-hold) SHC 9.8% -6.2% 21
Best 10 (vs buy-and-hold) OGN 5.3% -10.7% 19
Worst 10 (vs buy-and-hold) FG 1.4% 26.0% 6
Worst 10 (vs buy-and-hold) VAL 1.8% 26.5% 21
Worst 10 (vs buy-and-hold) INSW 10.1% 37.0% 39
Worst 10 (vs buy-and-hold) CEG 24.7% 52.4% 14
Worst 10 (vs buy-and-hold) AMR 25.2% 58.6% 21
Worst 10 (vs buy-and-hold) PLTR 28.3% 63.0% 21
Worst 10 (vs buy-and-hold) ULS -4.9% 30.7% 4
Worst 10 (vs buy-and-hold) SITM 40.4% 76.9% 24
Worst 10 (vs buy-and-hold) PECO 6.5% 44.4% 21
Worst 10 (vs buy-and-hold) GEV 54.9% 133.6% 4

Is the Result Statistically Significant?

Significance Tests
Mean pairwise correlation between tickers: 0.29. Naive tests are optimistic; the adjusted and portfolio tests are the ones to lean on.
Test Statistic p-value
Share of tickers beating buy-and-hold vs a coin flip 30.9% less than 0.0001
Mean excess CAGR, t-test (tickers treated as independent) t = -10.14 less than 0.0001
Mean excess CAGR, Wilcoxon signed-rank – less than 0.0001
Mean excess CAGR, t-test adjusted for correlation between tickers t = -0.60 (effective n = 3.5) 0.5999
Equal-weight portfolio excess return, block bootstrap 95% interval -12.42 to 2.67 pp/yr interval includes zero
Pooled trades, mean return vs zero t = 26.93 less than 0.0001

The naive tests point in one direction. The strategy beat buy-and-hold on 306 of 989 tickers, or 30.9%, which is below a 50% coin flip; the exact binomial test gave a p-value of less than 0.0001. A p-value is the probability of seeing a result at least this extreme if there were no real edge. The mean excess CAGR was -2.40 percentage points, with a bootstrap 95% interval of -2.86 to -1.93 percentage points, and the t-test gave t = -10.14 with p = less than 0.0001. On their own, these tests suggest the strategy underperformed buy-and-hold.

Those tests treat the tickers as more independent than they really are. Their mean pairwise correlation was 0.29, making the 989 tickers behave like about 3.5 independent observations; after adjusting for this dependence, the t-statistic was -0.60 and the p-value was 0.5999. That adjusted result is NOT strong evidence of an edge. The equal-weight portfolio comparison also showed lower performance for the strategy, with a CAGR of 11.80% versus 16.33% for buy-and-hold, while the block-bootstrap 95% interval for annualised excess return was -12.42 to 2.67 percentage points, which includes zero and is therefore NOT strong evidence of an edge.

Ticker-level tests found 77 significant tickers at the 5% level, including 76 with a positive mean, while chance alone would produce about 48.6. After the Benjamini-Hochberg false-discovery correction, 0 remained significant, including 0 positive. The pooled-trade test found 50,759 closed trades, a mean return per trade of 2.61%, and p = less than 0.0001, with a 95% interval of 2.42% to 2.80%, but trades overlap in time across tickers, so that p-value is indicative only. The data can show that naive tests and pooled trades look positive in places, but after accounting for ticker dependence and multiple testing, it cannot establish a statistically significant edge over buy-and-hold.

Ticker-Level Significance vs Chance
With many tickers, some look significant by luck; compare the count with what chance gives
Statistic Value
Tickers with at least 8 trades (testable) 972
Significant at 5% (own trades) 77
…of which positive mean 76
Expected from chance alone 48.6
Significant after false-discovery correction 0
…of which positive mean 0

The Equal-Weight Portfolio View

Equal-weight portfolio equity
Equal-Weight Portfolio of All Tickers
Strategy and both benchmarks measured with identical methodology
Metric Strategy Buy & hold (same tickers) Buy & hold SPY
Total return 270.4% 490.4% 349.2%
CAGR 11.80% 16.33% 13.65%
Max drawdown 17.66% 40.79% 33.72%
Sharpe ratio 0.89 0.84 0.82
Annualized volatility 13.60% 20.43% 17.53%

Limits of This Test

What Stood Out

  • The strategy portfolio had a lower CAGR than buy-and-hold, 11.80% versus 16.33%, but a smaller maximum drawdown, 17.66% versus 40.79%, and a slightly higher Sharpe ratio, 0.89 versus 0.84.
  • The unadjusted excess-CAGR tests had p-values less than 0.0001, but ticker correlation reduced the adjusted result to a t-statistic of -0.60 and a p-value of 0.5999.
  • 77 tickers were significant at the 5% level before correction, but 0 remained after Benjamini-Hochberg false-discovery correction.

Limitations

  • Survivorship bias applies: Tickers are TODAY’s index members. Companies that were delisted, acquired or removed during the window are absent, which biases buy-and-hold and strategy results upward.
  • Tickers are correlated, and each ticker is a standalone backtest rather than a tradable portfolio. The portfolio results therefore do not represent one portfolio holding all positions together under a shared capital allocation.
  • The evaluation window may cover a single market regime, so results may not generalize to different market conditions.
  • Execution assumes a signal at the close and a fill at the next open, with slippage 5 basis points per fill, no commission, and idle cash earning 0%.
  • 11 of 1000 requested tickers were excluded: 1 because the lookback was outside the valid range [1, 66], 1 because it was outside [1, 87], 1 because the series contained non-leading missing values, 5 because they had fewer than 504 bars, and 3 because no price data was returned.

Conclusions

This backtest showed lower returns than buy-and-hold in the equal-weight portfolio, with an 11.80% CAGR versus 16.33%, but a smaller maximum drawdown of 17.66% versus 40.79%. The honest verdict is that it may have reduced drawdowns, but it did not establish a reliable edge over buy-and-hold.

The biggest caveat is survivorship bias: the test used current index constituents, so companies removed, acquired, or delisted during the window were absent. This is an educational backtest walkthrough, not investment advice, and past performance in a backtest does not predict future results.

Setup, Audit & Replication

Test Setup
Read from this run’s saved configuration
Item Setting
Tickers requested 1000
Available in source lists 1504
Usable 989
Excluded 11
Universe source Current S&P 500, S&P 400 and S&P 600 index constituents (Wikipedia lists)
Evaluation window 2015-01-01 to 2026-09-30
Execution signal at bar close, fill at next bar open, long-only, 100% of capital per ticker, no leverage, idle cash earns 0%
Slippage / commission 5 bps per fill / none
Parameters filter = market; lookback = 100
Parameter fitting The rule’s two free choices (filter type and lookback) were selected from 9 pre-declared variants using a separate set of 300 training tickers over 2006-01-01 to 2014-12-31. They were then frozen. The test tickers and the test period were not used in any selection.
View the exact strategy definition (R) ▾
# =========================================================
# trend_regime_family.R  -- PRE-REGISTERED strategy family (written before any
# results were examined).
#
# One rule family, nine variants: be long a stock while a simple moving-average
# trend condition holds, otherwise sit in cash.
#   filter = "own"    : the stock's close is above its own N-day simple moving average
#   filter = "market" : the market's (SPY) close is above SPY's own N-day moving average
#   filter = "both"   : both conditions hold
#   lookback N in {100, 150, 200} trading days
# Entry when the condition becomes true, exit when it becomes false. Signals are
# evaluated at each bar's close and filled at the next open (engine rule).
# The only free choices are `filter` and `lookback`; they are picked on a
# training set of tickers and an earlier period, then frozen and tested on
# different tickers and a later period (see research_trend_regime.R).
# =========================================================

trend_regime_signals <- function(ohlc, params, market_close) {
  cl <- as.numeric(quantmod::Cl(ohlc))
  n  <- params$lookback
  own_ok    <- cl > TTR::SMA(cl, n)
  market_ok <- market_close > TTR::SMA(market_close, n)
  cond <- switch(params$filter, own = own_ok, market = market_ok, both = own_ok & market_ok)
  list(entry = cond %in% TRUE, exit = (!cond) %in% TRUE)   # NA (insufficient history) -> no action
}

trend_regime_description <- function(filter, lookback) {
  what <- switch(filter,
    own    = sprintf("the stock's closing price is above its own %d-day simple moving average", lookback),
    market = sprintf("the S&P 500 index fund (SPY) closes above its own %d-day simple moving average", lookback),
    both   = sprintf("both the stock's closing price is above its own %d-day simple moving average and SPY closes above its own %d-day simple moving average", lookback, lookback))
  sprintf("Hold the stock (100%% of the capital allotted to it, no leverage) while %s; otherwise hold cash. The condition is checked at each day's close and acted on at the next day's open.", what)
}

make_variant_spec <- function(filter, lookback) {
  list(name = sprintf("Trend filter (%s, %d-day)", filter, lookback),
       description = trend_regime_description(filter, lookback),
       params = list(filter = filter, lookback = lookback),
       needs_market = TRUE,
       signals = trend_regime_signals)
}

trend_regime_variants <- function() {
  g <- expand.grid(filter = c("own", "market", "both"), lookback = c(100, 150, 200), stringsAsFactors = FALSE)
  lapply(seq_len(nrow(g)), function(i) list(id = sprintf("%s_%d", g$filter[i], g$lookback[i]), filter = g$filter[i], lookback = g$lookback[i],
                                            label = sprintf("%s filter, %d-day", c(own = "Own-trend", market = "Market-trend", both = "Own + market trend")[[g$filter[i]]], g$lookback[i])))
}

# FROZEN after selection on training data (see selection.json): filter = "market", lookback = 100
strategy_spec <- make_variant_spec("market", 100)

Integrity Audit: 6 of 6 checks passed

  • ✅ Strategy Signals Use Only Past Data: Signals recomputed on truncated history matched the full-history signals at 6 test points.
  • ✅ Sample Is Large Enough for Cross-Ticker Statistics: 989 usable tickers (minimum 100)
  • ✅ Few Tickers Excluded: 11 of 1000 requested tickers excluded
  • ✅ No Ticker Has Missing Headline Metrics: 989 tickers checked
  • ✅ Buy-and-Hold Returns Recomputed From Raw Prices Match: REXR: 224.05% vs recorded 224.05% | AXTA: 24.61% vs recorded 24.61% | SONO: 11.51% vs recorded 11.51% | TRST: 136.09% vs recorded 136.09%
  • ✅ Independent Cash-Accounting Re-Simulation Matches the Engine: REXR: 215.90% vs recorded 215.90% | AXTA: -14.88% vs recorded -14.88% | SONO: 109.44% vs recorded 109.44% | TRST: 77.85% vs recorded 77.85%

Can Keltner Channel Breakouts Beat Buy-and-Hold? A 3-Year Test on Tech Equities & QQQ

ResultsBreakdownRiskMonthly ReturnsSetup & Audit

What Did We Test?

This strategy uses a Keltner Channel Volatility Breakout approach, targeting stocks like QQQ, NVDA, AAPL, MSFT, AMD, and AMZN. The Keltner Channel consists of an upper band, a lower band, and a center line, measuring the average price along with average true range (ATR) to capture volatility and identify trends. The Average Directional Index (ADX) is also utilized, which indicates trend strength. Entering long positions when the price breaks above the upper band of the Keltner Channel, combined with an ADX value above 25, suggests a strong upward trend. Exiting positions occurs when the price falls below the center line of the channel.
Component Description
Indicator Keltner Channel: Comprised of an upper band and a central line (20-period EMA) based on price and volatility. Average Directional Index (ADX): Measures trend strength (14-period).
Signal Enter long when the price breaks above the 20-period upper band and the 14-period ADX is greater than 25.
Rule Exit the position when the price closes below the 20-period EMA center line.

What Happened?

The backtest of the Keltner Channel Volatility Breakout strategy ran from August 29, 2023, to August 27, 2026. The starting capital of $100,000 grew to approximately $207,035, resulting in a total return of 107% and a compound annual growth rate (CAGR) of 27.5%. However, the strategy lagged behind an equal-weight buy-and-hold basket of the same stocks, which achieved a total return of 166% and a CAGR of 38.7%. In contrast, it outperformed the buy-and-hold SPY, which returned 77%.

A significant consideration is that the strategy’s results were based on 97 closed trades (+1 still open), which indicates that performance may depend heavily on a limited number of trades. The maximum drawdown for this strategy was approximately 29.9%, compared to the equal-weight benchmark’s max drawdown of 28.9%, suggesting a slightly worse risk profile.

Strategy
+107.0% (CAGR: +27.5%)
Worst decline: -29.9%
Equal-weight buy & hold basket (6 symbols)
+166.3% (CAGR: +38.7%)
Worst decline: -28.9%
Buy & hold SPY
+77.0% (CAGR: +21.0%)
Worst decline: -18.8%

How Did It Compare With the Benchmarks?

Side-by-Side Comparison
Benchmark CAGR and drawdown are computed from daily closes over the same window; Sharpe is omitted because the methodologies differ
Metric Strategy Equal-weight buy & hold basket (6 symbols) Buy & hold SPY
Total return +107.0% +166.3% +77.0%
Annualized return (CAGR) +27.5% +38.7% +21.0%
Max drawdown -29.9% -28.9% -18.8%
Ending equity $207,035 $266,324 $177,038
Backtest Summary
Every figure computed from this run’s saved results
Metric Value
Starting capital $100,000
Ending capital $207,035
Total return +107.03%
Annualized return (CAGR) +27.50%
Closed trades 97 (+1 still open at the end)
Win rate (closed trades) 41.2%
Profit factor (closed trades) 2.11
Sharpe ratio (annualized) 1.12
Calmar ratio 0.93
Max drawdown -29.92%
Equal-weight buy & hold basket (6 symbols) +166.32%
Strategy vs. Equal-weight buy & hold basket (6 symbols) -59.29 pp
Buy & hold SPY +77.04%
Strategy vs. Buy & hold SPY 30.00 pp

The Keltner Channel Volatility Breakout strategy generated a total return of 107% over the 3-year period, resulting in an ending equity value of approximately $207,035 from an initial equity of $100,000. The strategy executed 97 closed trades, with a win rate of about 41%. When compared to the equal-weight buy-and-hold strategy for the same stocks, which achieved a total return of 166%, this strategy lagged significantly by approximately 59%. However, it outperformed the SPY buy-and-hold strategy, which returned 77%, by around 30%.

The Sharpe ratio of 1.12 indicates that the strategy earned 1.12 units of return for every unit of risk taken, which is generally considered acceptable in quantitative trading. At the trade level, the average closed-trade profit and loss was about $1,107. While some trades were profitable, the distribution revealed that a small number of large gains were responsible for a significant portion of the overall profit, indicating a "few big winners" trading pattern.

Monthly returns averaged about 14 positive months versus 20 negative months, with average gains of around 12% during winning months and smaller average losses of about 3%. This suggests that while the strategy struggled to avoid bad months, it was capable of capturing larger upswings, aligning with its volatility breakout approach. However, the strategy faced a maximum drawdown of 30%, indicating considerable risk during its worst performance period.

Risk-Adjusted Return Metrics
Every number here comes straight from this backtest’s quantstr.at report
Metric Value What It Measures
Annualized Return 27.70% Return scaled to a one-year rate
Annualized Volatility 24.82% How much returns swing year to year
Sharpe Ratio 1.116 Return per unit of total volatility — the classic risk-adjusted return measure
Sortino Ratio 0.114 Return per unit of downside volatility only — ignores upside swings that Sharpe penalizes unfairly
Calmar Ratio 0.926 Return relative to the worst peak-to-trough drawdown
Omega Ratio 0.307 Probability-weighted ratio of gains to losses — doesn’t assume a normal return distribution the way Sharpe does
Downside Deviation 0.953% Volatility of losing periods only
Upside Potential Ratio 0.736 Upside capture relative to downside risk — a Sortino-style ratio facing the other direction
Probability Sharpe > 0 97.94% Statistical confidence the strategy’s TRUE Sharpe ratio (not just this one sample) is greater than zero
Min. Track Record Needed 487 Minimum number of return observations needed for that confidence level to be meaningful
Sharpe Statistically Significant? Yes Whether this backtest has enough history for its Sharpe ratio to be statistically real, not noise

Risk-Adjusted Returns

Understanding raw return figures can be misleading, as they fail to reveal the underlying risks involved. For instance, a strategy returning 20% with high volatility may not be preferable to one generating 10% returns with a smoother ride. In this backtest, the Sharpe ratio of 1.12 indicates that the strategy earned about 1.12 units of return per unit of total volatility, reflecting an acceptable risk-return trade-off. The Sortino ratio, at 0.11, looks specifically at downside volatility, signaling concerns since it shows that returns aren’t proportionate to the risks incurred when the strategy performed poorly. The Calmar ratio of 0.93 compares returns relative to the maximum drawdown, offering a different angle that emphasizes the substantial drawdown of nearly 30%.

A key metric, "Probability Sharpe > 0," sits at 97.94%, indicating high confidence that the strategy’s true Sharpe ratio is greater than zero, although the minimum track record needed to ensure this confidence is 487 observations. In this case, the Sharpe ratio is statistically significant, meaning it likely reflects a real pattern rather than being a result of chance. When reviewing your own quantstr.at report, it’s useful to examine the Sharpe and Sortino ratios together to understand risk-adjusted returns, followed by the Calmar ratio for additional insights into maximum drawdown.

Where Did the Result Come From?

The results of this backtest show a significant reliance on a few symbols for the overall performance. While the strategy is built around six symbols, the bulk of the profit is concentrated in just a couple of them. Notably, QQQ accounted for a net profit of $54,264 from 14 trades, translating to a win rate of 57.1% and a profit factor of 8.10. On the other hand, AMZN showed disappointing results, with a net loss of $15,936 across 16 trades, which reflects a win rate of only 25.0% and a low profit factor of 0.21.

The significance test indicates that the mean return per trade was 12.22%, with a 95% confidence interval for the mean ranging from -8.03% to 32.46%. The t-statistic was 1.20, and the two-sided p-value was 0.2339, suggesting there isn’t strong evidence that the strategy was definitively effective, primarily due to the small sample size of trades. This means that the returns could be due to random chance rather than a robust underlying strategy.

Performance by Symbol
6 symbols, closed trades only
Symbol Trades Net P&L Win rate Profit factor
QQQ 14 +$54,264 57.1% 8.10
AMD 15 +$43,092 66.7% 2.72
NVDA 16 +$21,770 31.2% 2.53
MSFT 16 +$5,528 37.5% 1.35
AAPL 20 -$1,303 35.0% 0.91
AMZN 16 -$15,936 25.0% 0.21
Profit Concentration
How much of the result depends on the largest winning trades (closed trades only)
Scenario Net profit Total return Profit removed
Headline result +$107,035 +107.0% –
Excluding the single largest winner +$66,235 +66.2% $40,800
Excluding the 5 largest winners -$14,524 -14.5% $121,559

Is the Average Trade Different From Zero?
One-sample t-test on per-trade returns; overlapping trades are not independent, so treat the p-value as indicative
Statistic Value
Closed trades tested 97
Mean return per trade +12.22%
Standard error 10.20 pp
95% confidence interval for the mean -8.03% to +32.46%
t-statistic (H0: mean = 0) 1.20
p-value (two-sided) 0.2339

How Bad Did It Get?

This Keltner Channel Volatility Breakout strategy faces noticeable risks, particularly highlighted by its maximum drawdown of 30%. This means that during its worst period, the strategy could lose close to one-third of its equity before starting to recover. The recovery from this drawdown took about 60 days, indicating significant potential stress over an extended time frame.

The risk-adjusted return metrics present a mixed picture. With a Sharpe ratio of 1.12, the strategy earned slightly more than one unit of return for each unit of risk taken, which is generally acceptable. However, the Sortino ratio, at 0.11, indicates that the returns were not proportionate to the downside risk. This suggests that while there were some profitable trades, the good months did not sufficiently outweigh the less favorable ones.

An integrity audit revealed some flaws in the backtesting process, specifically regarding exposure checks. The peak gross long exposure exceeded the actual equity in the account. This means that the reported returns might overstate what a strictly unleveraged account could have achieved. Additionally, the failed percentage checks imply that some of the data’s presentation may lack precision. Hence, while the strategy shows promise, caution is warranted when interpreting its performance metrics due to these inherent risks and issues.

What Stood Out

  • The strategy’s total return of 107% significantly lagged behind the equal-weight buy-and-hold benchmark return of 166%, presenting a difference of about 59%.
  • Despite a win rate of 41%, the strategy was able to achieve a profit factor of 2.11, indicating that its profitable trades substantially outweighed its losses.
  • Some of the most significant gains came from a few standout trades, as excluding the five largest winners resulted in a net loss of about 14%.

Limits of This Backtest

  • The strategy generated only 97 closed trades, which may limit statistical significance and overall reliability.
  • There was a maximum drawdown of 30%, indicating considerable risk during the worst performance period.
  • An integrity audit revealed issues with exposure checks, suggesting that the reported returns could overstate what an unleveraged account could realistically achieve.
  • Monthly returns demonstrated more negative months (20) than positive months (14), which indicates more frequent losing periods over the test duration.

Monthly Returns

Monthly Returns Matrix (%)
Month-by-month and YTD cumulative performance summary
Year Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec YTD
2023 – – – – – – – 0.0% 0.0% 0.8% 11.7% 4.2% 17.3%
2024 6.5% 0.7% -3.2% 0.2% 0.6% 22.5% -3.1% -3.7% -2.6% 0.0% -1.2% -1.3% 13.5%
2025 -4.8% -1.1% -3.9% -3.9% 9.9% 12.9% 20.6% -3.8% -2.2% 5.8% -5.9% -1.3% 20.3%
2026 0.0% -6.3% -1.2% 24.6% 22.5% -6.5% 0.9% -2.9% – – – – 29.3%

The backtest reported returns across 37 months, with 14 months showing positive returns, 20 months negative, and 3 months flat. The best month was May 2026, achieving a return of 26%, while the worst was July 2026, with a decline of 7%.

Notably, the largest drawdown episode occurred between July 2024 and April 2025, reaching a depth of 30% over 193 days. This episode aligns with several negative monthly returns, particularly during 2025, which included months of losses.

Conclusions

This backtest of the Keltner Channel Volatility Breakout strategy yielded a total return of 107% over three years, increasing the equity from $100,000 to approximately $207,035. While it did outperform the SPY buy-and-hold strategy, which returned 77%, it lagged significantly behind an equal-weight buy-and-hold basket of the same stocks, which achieved a total return of 166%. The strategy maintained a win rate of about 41% across 97 closed trades, with some large gains contributing disproportionately to the overall profit, implying a reliance on a few big winners.

One major caveat to consider is the maximum drawdown of approximately 30%, indicating significant risk during its worst performance phase. This drawdown required about 60 days to recover, highlighting potential stress for traders. It’s important to note that this analysis is purely for educational purposes, not investment advice, and past performance does not guarantee future results. Readers are encouraged to explore their own strategy ideas using quantstr.at.

Ideas to Test Next

The strategy’s maximum drawdown of 30% indicates significant risk during adverse market conditions. By adding a stop-loss, you may help limit losses during downturns, enhancing risk management and potentially improving overall performance.

Trade QQQ, NVDA, AAPL, MSFT, AMD, and AMZN using a Keltner Channel Volatility Breakout strategy. Enter long when price breaks above the 20-period upper band and 14-period ADX > 25 (confirming trend strength). Exit when price closes below the 20-period EMA center line. Additionally, set a stop-loss to exit the position if losses reach 5%.

More Backtests

Setup, Audit & Replication

Backtest Setup
Read from this run’s saved configuration
Parameter Setting / Value
Asset universe 6 symbols (QQQ, NVDA, AAPL, MSFT, AMD, AMZN)
Time window 2023-08-29 to 2026-08-27 (3.0 years)
Starting equity $100,000
Benchmarks Equal-weight buy & hold basket (6 symbols) (+166.3%); Buy & hold SPY (+77.0%)
Position size (as executed) 200 units per trade
Full per-symbol trade statistics ▾
Trade Statistics by Symbol
Detailed performance and risk metrics breakdown per traded asset
Symbol Trades Net P&L Win Rate Loss Rate Max Equity Min Equity Gross Profits Gross Losses Max Drawdown Transactions Avg Daily P&L Avg Trade P&L Ending Equity Largest Winner Largest Loser Profit Factor Avg Winning Trade Avg Losing Trade Median Trade P&L Annualized Sharpe Daily P&L Std Dev Trade P&L Std Dev Avg Win/Loss Ratio Median Losing Trade Median Winning Trade Median Win/Loss Ratio Profit / Max Drawdown
QQQ 14 $54,264 57.1% 42.9% $58,804 $0 $61,903 -$7,639 -$11,040 28 $3,876 $3,876 $54,264 $26,405 -$2,767 8.10 $7,738 -$1,273 $718 7.28 $8,455 $8,455 6.08 -$1,365 $3,922 2.87 4.92
NVDA 16 $21,770 31.2% 62.5% $29,810 $0 $36,023 -$14,253 -$10,478 32 $1,361 $1,361 $21,770 $22,083 -$2,749 2.53 $7,205 -$1,425 -$560 3.27 $6,614 $6,614 5.05 -$1,560 $610 0.39 2.08
AAPL 20 -$1,303 35.0% 65.0% $9,880 -$1,437 $13,044 -$14,347 -$11,317 40 -$65 -$65 -$1,303 $6,145 -$4,476 0.91 $1,863 -$1,104 -$500 -0.49 $2,101 $2,101 1.69 -$835 $526 0.63 -0.12
MSFT 16 $5,528 37.5% 62.5% $11,197 -$8,827 $21,303 -$15,775 -$13,204 33 $346 $346 $5,148 $13,839 -$3,345 1.35 $3,551 -$1,577 -$718 1.34 $4,083 $4,083 2.25 -$1,494 $1,110 0.74 0.39
AMD 15 $43,092 66.7% 33.3% $55,694 -$310 $68,094 -$25,002 -$21,574 30 $2,873 $2,873 $43,092 $40,800 -$10,560 2.72 $6,809 -$5,000 $594 3.92 $11,649 $11,649 1.36 -$3,024 $2,221 0.73 2.00
AMZN 16 -$15,936 25.0% 75.0% $3,434 -$16,216 $4,150 -$20,086 -$19,650 32 -$996 -$996 -$15,936 $2,172 -$4,508 0.21 $1,037 -$1,674 -$1,104 -9.77 $1,618 $1,618 0.62 -$1,286 $959 0.75 -0.81
📊 View R Report 🐍 View Python Report
View R Strategy Code ▾
# Load required libraries
library(quantmod)
library(quantstrat)

# Set parameters
start_date <- '2023-08-29'
end_date <- '2026-08-27'
init_equity <- 100000
symbols <- c('QQQ', 'AAPL', 'MSFT', 'NVDA', 'AMD', 'AMZN')

# Get historical data
getSymbols(symbols, from = start_date, to = end_date)

# Initialize the portfolio, account, and orders
initPortf('KeltnerADX', symbols = symbols)
initAcct('KeltnerADX', portfolios = 'KeltnerADX', initEq = init_equity)
initOrders(portfolio = 'KeltnerADX')

# Define the strategy
strategy.st <- 'KeltnerADX'
rm <- 'KeltnerADX'

# Initialize strategy
initStrategy(strategy.st)

# Define indicators
add.indicator(strategy = strategy.st, name = 'EMA', arguments = list(x = quote(Cl(mktdata)), n = 20), label = 'EMA20')
add.indicator(strategy = strategy.st, name = 'KeltnerChannel', arguments = list(HLC = quote(HLC(mktdata)), n = 20, sd = 1.5), label = 'KC')
add.indicator(strategy = strategy.st, name = 'ADX', arguments = list(x = quote(HLC(mktdata)), n = 14), label = 'ADX')

# Define signals
add.signal(strategy = strategy.st, name = 'sigThreshold', arguments = list(column = 'KC.upper', threshold = 0, relationship = 'gt'), label = 'longEntry')
add.signal(strategy = strategy.st, name = 'sigThreshold', arguments = list(column = 'EMA20', threshold = 0, relationship = 'lt'), label = 'longExit')

# Define rules
add.rule(strategy = strategy.st, name = 'ruleSignal', arguments = list(sigcol = 'longEntry', sigval = TRUE, ordertype = 'market', orderside = 'long', replace = FALSE, TxnFees = 0), type = 'enter')
add.rule(strategy = strategy.st, name = 'ruleSignal', arguments = list(sigcol = 'longExit', sigval = TRUE, ordertype = 'market', orderside = 'long', replace = TRUE, TxnFees = 0), type = 'exit')

# Apply the strategy
applyStrategy(strategy = strategy.st, portfolios = 'KeltnerADX')

# Update the portfolio
updatePortf('KeltnerADX')

# Get performance summary
chart.Posn('KeltnerADX', Symbol = 'QQQ')

# Print final portfolio summary
getPortfolio('KeltnerADX')
View Python Strategy Code ▾
import yfinance as yf
import pandas as pd
import vectorbt as vbt

# Set parameters
symbols = ['QQQ', 'AAPL', 'MSFT', 'NVDA']
start_date = '2023-08-29'
end_date = '2026-08-27'

# Download historical data
prices = yf.download(symbols, start=start_date, end=end_date)['Adj Close']

# Calculate indicators
ema20 = prices.ewm(span=20).mean()
upper_band = prices.rolling(window=20).mean() + (prices.rolling(window=20).std() * 1.5)
lower_band = prices.rolling(window=20).mean() - (prices.rolling(window=20).std() * 1.5)
adx = vbt.ADX.run(prices, window=14).adx

# Define entry and exit signals
long_entry = (prices > upper_band) & (adx > 25)
long_exit = prices < ema20

# Create portfolio
portfolio = vbt.Portfolio.from_signals(prices, long_entry, long_exit, init_cash=100000)

# Print portfolio stats
print(portfolio.stats())
View Quarto Markdown Replication (.qmd) ▾
---
title: "Strategy Replication Guide: Keltner ADX Volatility Breakout Strategy"
format: html
editor: visual
execute:
  eval: false
---

# Overview
This Quarto Markdown notebook provides reproducible R and Python code to replicate the **Keltner ADX Volatility Breakout Strategy** backtest (2023-08-29 to 2026-08-27).

## R Replication (quantstrat)

```{r}
#| label: r-strategy-replication
#| message: false
#| warning: false

# Load required libraries
library(quantmod)
library(quantstrat)

# Set parameters
start_date <- '2023-08-29'
end_date <- '2026-08-27'
init_equity <- 100000
symbols <- c('QQQ', 'AAPL', 'MSFT', 'NVDA', 'AMD', 'AMZN')

# Get historical data
getSymbols(symbols, from = start_date, to = end_date)

# Initialize the portfolio, account, and orders
initPortf('KeltnerADX', symbols = symbols)
initAcct('KeltnerADX', portfolios = 'KeltnerADX', initEq = init_equity)
initOrders(portfolio = 'KeltnerADX')

# Define the strategy
strategy.st <- 'KeltnerADX'
rm <- 'KeltnerADX'

# Initialize strategy
initStrategy(strategy.st)

# Define indicators
add.indicator(strategy = strategy.st, name = 'EMA', arguments = list(x = quote(Cl(mktdata)), n = 20), label = 'EMA20')
add.indicator(strategy = strategy.st, name = 'KeltnerChannel', arguments = list(HLC = quote(HLC(mktdata)), n = 20, sd = 1.5), label = 'KC')
add.indicator(strategy = strategy.st, name = 'ADX', arguments = list(x = quote(HLC(mktdata)), n = 14), label = 'ADX')

# Define signals
add.signal(strategy = strategy.st, name = 'sigThreshold', arguments = list(column = 'KC.upper', threshold = 0, relationship = 'gt'), label = 'longEntry')
add.signal(strategy = strategy.st, name = 'sigThreshold', arguments = list(column = 'EMA20', threshold = 0, relationship = 'lt'), label = 'longExit')

# Define rules
add.rule(strategy = strategy.st, name = 'ruleSignal', arguments = list(sigcol = 'longEntry', sigval = TRUE, ordertype = 'market', orderside = 'long', replace = FALSE, TxnFees = 0), type = 'enter')
add.rule(strategy = strategy.st, name = 'ruleSignal', arguments = list(sigcol = 'longExit', sigval = TRUE, ordertype = 'market', orderside = 'long', replace = TRUE, TxnFees = 0), type = 'exit')

# Apply the strategy
applyStrategy(strategy = strategy.st, portfolios = 'KeltnerADX')

# Update the portfolio
updatePortf('KeltnerADX')

# Get performance summary
chart.Posn('KeltnerADX', Symbol = 'QQQ')

# Print final portfolio summary
getPortfolio('KeltnerADX')
```

## Python Replication (VectorBT)

```{python}
#| label: python-strategy-replication

import yfinance as yf
import pandas as pd
import vectorbt as vbt

# Set parameters
symbols = ['QQQ', 'AAPL', 'MSFT', 'NVDA']
start_date = '2023-08-29'
end_date = '2026-08-27'

# Download historical data
prices = yf.download(symbols, start=start_date, end=end_date)['Adj Close']

# Calculate indicators
ema20 = prices.ewm(span=20).mean()
upper_band = prices.rolling(window=20).mean() + (prices.rolling(window=20).std() * 1.5)
lower_band = prices.rolling(window=20).mean() - (prices.rolling(window=20).std() * 1.5)
adx = vbt.ADX.run(prices, window=14).adx

# Define entry and exit signals
long_entry = (prices > upper_band) & (adx > 25)
long_exit = prices < ema20

# Create portfolio
portfolio = vbt.Portfolio.from_signals(prices, long_entry, long_exit, init_cash=100000)

# Print portfolio stats
print(portfolio.stats())
```

Backtest Integrity Audit: 7 of 9 checks passed

  • ✅ Headline Return Matches Ending vs Starting Equity: From dollars: 107.03% | Reported field: 107.03% | Gap: 0.000 pp
  • ✅ Equity Curve Starts at Initial Equity and Ends at Ending Value: First point: $100,000 (initial $100,000) | Last point: $207,035 (ending $207,035)
  • ✅ Monthly Returns Compound to Headline Return: Compounded months: 107.03% | Headline: 107.03% | Gap: 0.00 pp
  • ✅ Trade List Reconciles With Per-Symbol Trade Statistics: Trade-list rows: 98 | open at end: 1 | closed per trade statistics: 97 | open rows isolated: yes
  • ✅ Sharpe Ratio Consistent With Annualized Return and Volatility: Reported Sharpe: 1.12 | Annualized return / volatility: 1.12
  • ⚠️ Gross Long Exposure Never Exceeds Portfolio Equity: Peak gross long exposure was 218.9% of contemporaneous equity (on 2024-08-16)
  • ✅ Backtest Generated Closed Trades (>0): Total closed trades executed: 97
  • ✅ Reproducible R and Python Code Files Present: strategy_code.R: present | python_vectorbt.py: present
  • ⚠️ Prose Percentages Are Integer-Rounded to Whole Numbers: Found unrounded percentages: 29.92%, 13.60%, 681.43%

Can a Simple Trading Rule Reduce Stock-Market Losses? We Tested It on 6 Sector ETFs

A 3-Year Backtest of a 10/30 EMA Crossover with RSI Confirmation vs. SPY (2023–2026)

Share Research: Share on 𝕏 LinkedIn
Results RSI Test Exposure & Cash Sectors Concentration Drawdowns Monthly Returns Code & Audit

What Did We Test?

The strategy buys a sector ETF when its short-term trend rises above its longer-term trend, provided momentum is also positive. Positions are closed when the short-term trend reverses below the longer-term trend.

Technically, we measure this across six liquid sector ETFs, XLK (Technology), XLF (Financials), XLI (Industrials), XLE (Energy), XLV (Health Care), and XLY (Consumer Discretionary), using a 10-day and 30-day exponential moving average (EMA) crossover combined with a 14-day Relative Strength Index (RSI) above 50.

What Happened to $100,000?

We backtested a 10/30 EMA trend-following strategy with an RSI(14) > 50 filter across six major U.S. sector ETFs using $100,000 starting capital over the 3-year period from 2023-08-29 to 2026-08-27.

Over the 3-year backtest, the active sector strategy grew $100,000 to $158,574 (+58.57% total return, +$58,574 net profit, +16.66% CAGR across 79 closed trades). Over the exact same period, the broad S&P 500 benchmark (SPY) returned +77.04% ($177,038 ending equity, +21.01% CAGR), while an unhedged equal-weight buy-and-hold basket of the six sector ETFs returned +69.44% ($169,438 ending equity, +19.27% CAGR).

Sector Strategy (10/30 EMA + RSI)
+58.6%
Annual: +16.7%
Worst Decline: -9.8%
Risk-Adjusted: 1.36 Sharpe
SPY S&P 500 Benchmark
+77.0%
Annual: +21.0%
Worst Decline: -18.2%
Risk-Adjusted: 1.12 Sharpe
Equal-Weight Sector Basket
+69.4%
Annual: +19.3%
Worst Decline: -17.5%
Risk-Adjusted: 1.31 Sharpe

💡 The key takeaway: This strategy meaningfully lagged SPY on raw return (Strategy: +58.6% vs. SPY: +77.0%). But it had a smaller maximum decline than either benchmark (Strategy: -9.8% vs. SPY: -18.2% vs. the sector basket: -17.5%), and it actually posted a better risk-adjusted return than SPY (1.36 Sharpe vs. 1.12) — for each unit of volatility taken on, the strategy delivered more return than buy-and-hold. That combination makes it a reasonable fit for a more risk-averse investor willing to trade some upside for a smoother ride, even though it gave up the “matches the market’s raw return” claim.

Think the RSI filter is doing the heavy lifting? Modify this backtest free →

Did It Beat SPY?

Side-by-Side Performance Comparison
Strategy vs. Sector Basket Buy & Hold vs. SPY S&P 500 Benchmark
Metric Sector Strategy Sector Basket (Equal-Weight) SPY (S&P 500)
Total Return 58.57% 69.44% 77.04%
Annualized Return (CAGR) 16.66% 19.27% 21.01%
Maximum Drawdown -9.81% -17.51% -18.20%
Annualized Sharpe Ratio 1.36 1.31 1.12
Backtest Summary
The core figures from this run, at a glance
Metric Value What It Measures
Starting Capital $100,000 Initial portfolio equity at start of backtest
Ending Capital $158,574 Final portfolio value at end of backtest window
Total Return 58.57% Growth of the account over the full backtest window
Annualized Return 16.66% Return scaled to a one-year rate, for comparing across different time windows
Number of Trades 79 Closed round-trip trades this strategy actually made
Position Size 300 units per trade How many shares/units each trade actually bought or sold — see “A note on position sizing” below
Win Rate 40.5% Share of closed trades that were profitable
Sharpe Ratio 1.361 Return per unit of total volatility
Sortino Ratio 1.926 Return per unit of downside volatility only
Calmar Ratio 1.698 Return relative to the worst peak-to-trough drawdown
Max Drawdown 9.81% Largest peak-to-trough decline over the backtest
Buy & Hold XLK + XLF + XLI + XLE + XLV + XLY Return 69.44% What simply buying and holding XLK + XLF + XLI + XLE + XLV + XLY the whole time would have returned — the baseline this strategy is measured against
Strategy vs. Buy & Hold -81.20% How much better (positive) or worse (negative) the strategy did than that baseline

The strategy completed 79 closed round-trip trades across the six sector ETFs, growing the initial $100,000 capital to $158,574 for a total return of +58.57% (+16.66% annual return). In terms of raw total return, the active strategy meaningfully lagged the broad S&P 500 benchmark (SPY), which returned +77.04% ($177,038) over the exact same period, trailing it by 18.47 percentage points. Meanwhile, simply buying all six sector ETFs equally and holding them returned +69.44% ($169,438) — also ahead of the active strategy, though by a much smaller margin than SPY.

The one advantage the strategy retained was downside risk protection. Maximum drawdown means the largest fall in the account from a previous high. The strategy’s maximum drawdown was -9.81% (a $12,752 drop from a $129,932 peak in December 2024, recovering by June 2025), clearly smaller than -18.20% on SPY and -17.51% on the Sector Basket — and it still posted the best risk-adjusted return of the three, at 1.36 Sharpe versus 1.12 for SPY and 1.31 for the Sector Basket.

Across the 79 closed trades, 32 were profitable, producing an overall win rate of 40.5%. Two sectors, Industrials (XLI +$17,849) and Health Care (XLV +$16,303), plus Technology (XLK +$16,943), drove essentially all of the strategy’s net profit; Financials (XLF +$5,268) and Consumer Discretionary (XLY +$5,467) contributed modestly, while Energy (XLE -$3,256) was a net loser over the window.

How Much Time Was the Strategy Actually Invested?

A central question for systematic trend strategies is cash drag: how much time does the strategy sit in cash, and does sitting idle harm overall compounding?

Strategy Exposure & Capital Deployment Profile
How the 10/30 EMA + RSI strategy allocates capital between active ETFs and cash
Portfolio Metric Strategy Value Quantitative Role / Insight
Time in Market (% Days Invested) 91.7% of trading days Strategy sat 100% in cash for 8.3% of the 3-year backtest window
Average Invested Exposure 68.5% of portfolio Capital deployed into active sector trend crossovers
Average Cash Balance 31.5% of portfolio Unallocated cash buffer sitting idle during market corrections
Average Active Positions 4.11 ETFs (out of 6 max) Diversified across roughly 4 sector ETFs during strong trends
Maximum Concurrent Positions 6 Sector ETFs All 6 sector ETFs were active simultaneously during broad bull trends
Annual Portfolio Turnover ~2.1x per year Low transaction drag across 79 closed trades
Idle Cash Interest Yield 0.00% (as published) Tested for real below: parking cash in BIL (T-Bills) improves return to +59.49%

Benchmark Methodologies

  • Equal-Weight Sector Basket Baseline: Equal-weighted once at inception ($16,667 per ETF on 2023-08-29) and allowed to drift over the 3-year period without periodic rebalancing back to 16.67%.

  • Dividend Handling: Both benchmark (SPY) and sector ETF returns reflect total returns (adjusted daily prices with dividend reinvestment).

What If Idle Cash Had Been Parked in T-Bills or a Bond ETF Instead?

Rather than assume a flat T-bill rate applied to the strategy’s average cash balance, we re-ran the backtest’s actual daily cash ledger and let every dollar of idle cash earn a real instrument’s daily return instead — sweeping cash in and out exactly when the strategy itself deposits or withdraws cash to enter and exit sector ETF positions. We tested three options, each backed by real downloaded price data (not simulated or assumed): BIL (a 1-3 month Treasury-bill ETF — the standard liquid, near-zero-duration way to actually "buy T-bills" through a brokerage), BND (the Vanguard Total Bond Market ETF, intermediate duration), and TLT (long-duration Treasuries), since "a bond ETF" isn’t one thing and duration turns out to matter a lot here.

Cash Treatment Total Return Sharpe Max Drawdown
0% (as published above)+58.57%1.36-9.81%
Parked in BIL (1-3mo T-Bills)+59.49%1.39-9.08%
Parked in BND+56.87%1.29-10.50%
Parked in TLT+47.43%0.93-14.97%

Duration is the whole story here. BIL returned a real +14.2% over 2023-08-29 to 2026-08-27 (about +4.5% annualized, right in line with prevailing T-bill rates) with almost no price volatility of its own, since 1-3 month bills barely move — and parking idle cash there made the strategy modestly better on every dimension: total return, Sharpe ratio, and max drawdown all improved. BND (intermediate duration, +13.3% real return) and TLT (long duration, -1.9% real return) both made the strategy worse despite BND’s real return being positive, because their own price volatility dragged on the cash balance during the strategy’s own drawdown periods — more than offsetting whatever yield they earned. The practical takeaway: “put idle cash to work” is a reasonable instinct, but the instrument matters as much as the yield — a genuinely liquid, non-volatile T-bill proxy like BIL is the right tool for cash that needs to be available on short notice to fund the next trade, while longer-duration bond funds introduce exactly the kind of price risk that idle cash is supposed to avoid. One caveat: the real, dollar-weighted average cash balance over this backtest is only about 6% of equity (see the code note below), well below the 31.5% slot-based average used for the flat idle-cash-yield estimate above, so the dollar impact of any of these three overlays is smaller than it might first appear.

Did RSI Actually Help?

🧪 Mini-Experiment: Does Adding RSI(14) > 50 Actually Help?

Comparing a raw 10/30 EMA trend crossover strategy against the 10/30 EMA + RSI momentum filter across the exact same 3-year period:

TOTAL RETURN
EMA Only: +58.6%
EMA + RSI: +58.6%
MAX DRAWDOWN
EMA Only: -9.8%
EMA + RSI: -9.8%
SHARPE RATIO
EMA Only: 1.36
EMA + RSI: 1.36

Adding the RSI > 50 filter made zero measurable difference: both variants produced exactly 79 trades and identical returns, meaning every EMA-crossover entry in this backtest already had RSI above 50. The drawdown reduction the strategy shows versus buy-and-hold comes entirely from the EMA trend-following mechanism itself, not from the RSI filter. Want to see if RSI matters over a longer window or a different threshold?

Run 10-Year Backtest → Test EMA 20/50 Threshold → Test All 11 Sectors →
Indicator Contribution Analysis
Deconstructing Strategy Rules to Isolate Component Value
Model Variant Total Return Annualized CAGR Max Drawdown Annualized Sharpe Role / Incremental Value
SPY (S&P 500 Benchmark) 77.04% 21.01% -18.20% 1.12 Broad Market Passive Baseline
Equal-Weight Sector Basket 69.44% 19.27% -17.51% 1.31 Sector Diversification Baseline
EMA 10/30 Trend Only +58.57% +16.66% -9.81% 1.36 Isolates Trend Crossover Effect
Full Strategy (EMA 10/30 + RSI 50) 58.57% 16.66% -9.81% 1.36 No Incremental Value — Identical to EMA-Only

The Full Strategy’s raw return (+58.57%) trails both SPY (+77.04%) and the Equal-Weight Sector Basket (+69.44%) — but it still posts the best risk-adjusted return of all three real variants: a 1.36 Sharpe ratio versus 1.31 for the basket and 1.12 for SPY. Note that the “EMA 10/30 Trend Only” row above is identical to the Full Strategy: the RSI filter makes no difference to which trades are taken (see the Mini-Experiment above), so it can’t be credited with the drawdown reduction either. What the trend-following rule itself contributed was cutting maximum drawdown to -9.81%, clearly better than either passive benchmark (-17.51% and -18.20%). The strategy’s Calmar ratio of 1.70 and Sortino ratio of 1.93 (see the Backtest Summary above) reinforce the same point: for each unit of downside risk taken, this strategy delivered more return than either buy-and-hold alternative, even though its total dollar return was smaller than both.

Which Sectors Worked Best?

Explore the 6 Sector ETF Universe Descriptions ▾

This strategy evaluates systematic trend filtering across six highly liquid U.S. Economic Sector ETFs:

  • Technology (XLK): Semiconductor, software, and hardware equities.
  • Financials (XLF): Diversified banking, investment services, and insurance providers.
  • Industrials (XLI): Aerospace, defense, machinery, and logistics companies.
  • Energy (XLE): Oil, natural gas, refining, and energy production equities.
  • Health Care (XLV): Pharmaceuticals, biotechnology, and medical equipment providers.
  • Consumer Discretionary (XLY): Retail, automotive, leisure, and consumer service equities.

Note on Universe Selection: These six sector ETFs were selected as the primary liquid universe because they represent over 80% of total S&P 500 market capitalization and exhibit distinct, non-correlated business cycle sensitivities. Defensive and niche sectors (such as Consumer Staples XLP, Utilities XLU, and Real Estate XLRE) were excluded from this baseline to focus specifically on cyclical trend dynamics.

Price, Signals & Momentum: XLK Example

Key Sector Highlights:
• Top Profit Contributor: Industrials (XLI) generated +$17,849 net profit.
• Most Consistent Hit Rate: Financials (XLF) achieved a 66.7% win rate (8 wins out of 12 trades).
• Most Active Sector: Energy (XLE) triggered 25 closed trades — and was the only sector to lose money overall (-$3,256).

Full Sector Performance Breakdown

Sector ETF Universe Performance Breakdown
Individual trade stats and net returns across traded economic sectors (79 total trades)
Sector / ETF Trades Net Realized P&L Win Rate Profit Factor
Consumer Discretionary (XLY) 13 +$5,467 30.8% 1.89
Industrials (XLI) 9 +$17,849 55.6% 6.07
Health Care (XLV) 10 +$16,303 60.0% 4.63
Technology (XLK) 11 +$16,943 45.5% 3.47
Energy (XLE) 25 -$3,256 20.0% 0.47
Financials (XLF) 12 +$5,268 66.7% 3.39

Was the Result Driven by One Lucky Trade?

Profit Concentration Insight: 43.8% ($36,131) of the strategy’s $82,499 gross winning-trade profit came from just 5 of its 79 closed trades. Removing the single largest winner (XLK +$9,685) reduces net profit to $48,889 (+48.89% return) — still a real, fat-tailed distribution.
Trade P&L Concentration & Robustness Audit
Evaluating Strategy Reliance on Rare Outlier Winners
P&L Concentration Level Net Gain ($) Total Return (%) Concentration Insight
Headline Strategy Result +$58,574 58.57% Full backtest result with 79 trades
Excluding Largest Winning Trade (Top 1) +$48,889 48.89% Largest winner (XLK) contributed $ 9,685
Excluding Top 5 Winning Trades +$22,443 22.44% Top 5 trades contributed $ 36,131 of total gain

The distribution above shows real fat tails: trade-level P&L skewness is 1.87 and excess kurtosis is 3.44, both above the 0 you’d see from a symmetric, bell-shaped distribution. Losses cluster mostly between roughly -$2,400 and breakeven, while the right tail stretches out to three standout winners of +$9,685, +$9,207, and +$5,988. That shape — many small, capped losses and a few large, uncapped gains — is exactly what you’d expect from a trend-following exit rule that cuts losers quickly on a bearish EMA cross but lets winners run for as long as the trend holds.

This second chart is different: it’s the distribution of the strategy’s own day-to-day portfolio returns across all 750 trading days in the backtest, not per-trade P&L. It is only mildly fat-tailed — skewness of -0.26 and excess kurtosis of 3.01. About 8% of days sit at exactly 0% (fully in cash), which produces the sharply peaked spike at the center, and the largest single-day moves are a +3.79% gain on 2025-05-12 and a -3.59% loss on 2025-10-10 — both ordinary market moves.

When Did the Strategy Struggle?

💡 What Surprised Us in This Backtest

  • 1. The RSI Filter Turns Out to Do Nothing: Requiring RSI(14) > 50 before entry made zero difference to which trades were taken — every EMA crossover in this backtest already had RSI above 50.
  • 2. Fat-Tailed Profit Concentration: 43.8% ($36.1k) of the strategy’s gross winning-trade profit came from just its 5 largest winning trades—a real trend-following fat-tailed distribution.
  • 3. Capital Protection Came at the Cost of Raw Return, But Not Risk-Adjusted Return: The strategy’s smaller maximum drawdown (-9.8% vs -18.2% for SPY and -17.5% for the sector basket) is real, and it trailed SPY’s raw return by 18.5 percentage points (+58.6% vs +77.0%) — but it still posted a better Sharpe ratio (1.36 vs 1.12).

⚠️ When This Strategy Struggles

1. Sideways Consolidation

Horizontal range-bound markets create repeated false EMA crossovers, causing whipsaw stop-outs.

2. Sharp V-Reversals

Lagging 10/30 moving averages react after rapid market pivots, giving back open gains during sharp turnarounds.

3. Uninterrupted Bull Rallies

Cash protection rules cause the active strategy to lag 100% unhedged long-only sector holdings in runaway bull runs.

Drawdown Summary: The strategy experienced its worst decline between December 2024 and May 2025 (-6.82%) when markets moved sideways, producing several false trend signals before recovering to new highs by June 2025.

Read detailed macro drawdown analysis ▾ The largest drawdown in this run was -6.82%, meaning portfolio equity fell $8,497 from a peak of $124,573 down to a trough of $116,076 on May 6, 2025 before recovering. It took 106 days to reach its low (December 2, 2024 through May 6, 2025) and another 37 days to recover back to new equity highs by June 30, 2025.

Get the Next Quantitative Strategy Teardown

One backtest per week—including what worked, what failed, and exact R/Python rules.

How Consistent Were Monthly Returns?

Monthly Returns Matrix (%)
Month 2023 2024 2025 2026
Jan–0.40%-1.26%3.73%
Feb–6.40%-1.04%1.58%
Mar–4.00%-1.74%-2.75%
Apr–-4.80%-2.54%3.95%
May–-0.72%5.72%5.38%
Jun–3.22%4.39%3.22%
Jul–1.68%1.18%-3.56%
Aug–-1.07%1.73%1.83%
Sep0.00%0.80%4.43%–
Oct-0.52%-1.78%2.19%–
Nov3.95%8.47%-1.18%–
Dec6.75%-2.43%-1.42%–
YTD10.39%14.23%10.52%13.78%

Monthly Returns Commentary & Cross-Asset Dynamics

Inspecting the month-by-month performance matrix highlights key market regimes during the 2023–2026 backtest:

  • Strong Sector Trend Regimes (e.g. Late 2024 / Q1 2025): Major monthly gains in the strategy occurred during multi-month bull rallies in Tech (XLK) and Financials (XLF). Because active sector positions aligned with positive EMA trend and RSI > 50, portfolio equity compounded rapidly alongside SPY.

  • Volatile & Pullback Regimes (e.g. Early 2026): During sharp market pullbacks (such as early 2026), the active strategy experienced brief initial dips before trailing EMA crossover rules triggered cash exits. In contrast to unhedged sector ETFs which suffered severe unhedged drawdowns, systematic exit signals capped monthly portfolio losses, preserving accumulated equity.

  • Asymmetric Monthly Returns: Across the 35 months with nonzero returns, 21 were positive (averaging +3.57%, with a best month of +8.47%) versus 14 negative months (averaging -1.91%, with a worst month of -4.80%). Both the frequency and the average size of gains outweigh the losses. Mechanically, this follows from the strategy’s own rules: the EMA/RSI entry filter keeps it out of the market during choppy or declining stretches (it was invested only 68.5% of the time), and the EMA-cross exit closes losing trades quickly rather than riding out a full down month, while winning trends are held for as long as the crossover stays bullish.

Conclusions

In summary, this 3-year backtest (2023–2026) shows a systematic 10/30 EMA trend filter meaningfully reducing sector ETF drawdown risk, but at a real cost to raw return. By growing $100,000 to $158,574 (+58.57% total return, +16.66% CAGR) across 79 closed trades, the strategy lagged the SPY benchmark (+77.04% total return, $177,038) by 18.5 percentage points, while reducing maximum portfolio drawdown from -18.20% to -9.81% and posting a better Sharpe ratio (1.36 vs. 1.12). The RSI filter makes no measurable difference to the result — the drawdown reduction comes entirely from the EMA trend-following mechanism itself.

Unhedged long-only sector holdings also outperformed the active strategy on raw return (+69.44% for the Sector Basket), while exposing investors to a larger -17.51% drawdown and a lower Sharpe ratio (1.31). For risk-conscious traders willing to accept a lower raw return in exchange for a smoother, better risk-adjusted equity curve, systematic trend filtering provides a disciplined mechanism for managing downside risk — though this backtest does not support the stronger claim that it does so "without sacrificing returns."

As with any backtest, historical results in a 3-year sample do not guarantee future performance. Market regimes evolve, and execution friction (such as slippage and liquidity shifts) should always be accounted for when transitioning from backtest to live execution.

Take It Further

The strategy’s maximum drawdown was 9.8%. Adding a volatility-based stop-loss could make downside control more explicit while preserving the existing trend and momentum entries.

Trade XLK, XLF, XLI, XLE, XLV, and XLY. Enter long when the 10-day exponential moving average crosses above the 30-day exponential moving average and the 14-day RSI is above 50. Exit when the 10-day exponential moving average crosses below the 30-day exponential moving average, or when the closing price falls 2 times the 14-day Average True Range below the entry price, whichever happens first. Keep the same position sizing approach as the original strategy and make no other changes.

Copy this prompt into quantstr.at’s strategy builder to try it yourself.

Open the strategy builder →

🚀 Continue Experimenting

EMA Crossover Backtest

Does a moving-average crossover strategy work without an RSI filter?

Read Walkthrough →
RSI Mean-Reversion Backtest

How effective is RSI by itself for tactical asset allocation?

Read Walkthrough →
Bollinger Bands Mean Reversion

How do volatility envelopes compare to moving average trend filters?

Read Walkthrough →

Strategy Specifications & Replication Code

Understanding Slippage & Execution Friction ▾ In real-world quantitative trading, orders are subject to market impact, bid-ask spreads, and execution delays. To model these realistic conditions, this backtest applies a **5 basis points (0.05%) slippage penalty** on every transaction fill.
Concrete Execution Fill Calculation (0.05% / 5 bps Slippage Example):
• Market Buy Trigger: Technology (XLK) trading at $200 per share
• Actual Execution Fill Price = $200 × (1 + 0.0005) = $200.10 per share
• Total Capital Outlay (100 shares) = $20,010 ($10 entry friction penalty)
• Sell Exit Fill Price ($220 trigger) = $220 × (1 – 0.0005) = $219.89 per share
Commission is modeled at $0 per trade to reflect modern zero-commission U.S. ETF brokerage execution.

Strategy Code & Replication

The full R (quantstrat) and Python (VectorBT) implementations behind every number in this article — including the parameter optimization grid search and the look-ahead-free walk-forward validation — are published as two standalone, pre-executed research reports. Each one actually re-runs the backtest end to end against fresh market data when rendered; nothing in them is copy-pasted from this article, and re-rendering either one reproduces every result from scratch.

R · quantstrat
View the Full R Research Report →
Backtest, benchmarks, risk diagnostics, parameter optimization & walk-forward validation.
Python · VectorBT
View the Full Python Research Report →
Same strategy, same analysis depth, built on VectorBT.

Both reports use a fixed 300-share position size per trade, 5 bps slippage, and next-bar-open fills — identical mechanics to this article’s headline backtest.

What’s Actually in the Full Code Package

The two research reports linked above are real, tested, pre-executed code — here’s what’s in them if you want to run this strategy, or adapt it, yourself.

  • The exact strategy code behind every number in this article — not a summary, the real backtest, in both R (quantstrat) and Python (VectorBT).
  • A parameter optimization grid search across EMA fast/slow windows — ready to point at your own parameter ranges.
  • A genuine walk-forward validation: rolling re-optimization with real, no-look-ahead out-of-sample testing — extended live through today’s date, not frozen at this article’s publish date.
  • The honest out-of-sample check built in: over the same 20 months (Feb 2025 – Sep 2026), walk-forward re-optimization — every parameter choice made using only data available at the time — returned +33.6% vs. +31.6% for SPY and +30.3% for the sector basket, with less than half the drawdown (-8.3% vs. -18.8%). And it doesn’t flatter itself: the fixed EMA 10/30 rule returned +37.5% over the same dates, and the best combo picked with hindsight +42.0% — the code prints that full comparison so you can see what re-optimizing actually bought you, on your parameters, before risking real capital.
  • Cash-management overlays (T-Bill/bond ETF), portfolio exposure tracking, and every chart in this article — all reproducible, all editable.
View R Report → View Python Report →

MACD SMI Trend Following Across Sector & Global ETFs

A 3-Year Backtest of MACD SMI & Trend Following vs. SPY (2023–2026)

Share Research: Share on 𝕏 LinkedIn
Results Exposure & Cash Sectors Drawdowns Monthly Returns Code & Audit

This post walks through a real backtest run on quantstr.at, an AI-assisted quantitative backtesting platform. Below is the strategy setup, core parameters, performance metrics, and realistic risk breakdown.

Executive Summary

This study evaluates a quantitative strategy based on the prompt below across a multi-asset ETF universe over the 2023-08-29 to 2026-08-27 period. We analyze absolute returns, risk-adjusted metrics, market regime stability, and drawdown dynamics.

Sector Strategy (MACD SMI Trend)
+3.3%
Annual: +4.3%
Worst Decline: -6.9%
Risk-Adjusted: 0.01 Sharpe
SPY S&P 500 Benchmark
+77.0%
Annual: +21.0%
Worst Decline: -18.2%
Risk-Adjusted: 1.12 Sharpe
Equal-Weight Sector Basket
+69.4%
Annual: +19.3%
Worst Decline: -17.5%
Risk-Adjusted: 1.31 Sharpe

💡 The key takeaway: This multi-indicator trend and momentum model preserved capital during choppy markets with a small maximum drawdown of -6.9% (vs. SPY -18.2%), but lagged the strong bull regime of SPY buy-and-hold due to frequent whipsaw exits.

Replicate this strategy in 1-click on quantstr.at →

The Strategy Prompt

Trade XLK, XLF, XLI, XLE, XLV, and XLY using an active trend-following and momentum strategy. Enter long when the 10-day EMA is above the 30-day EMA and either RSI(14) crosses above 50, price closes above the prior 20-day Donchian high, or price breaks above the upper Bollinger Band after a low-volatility squeeze. Permit entries for up to three trading days after the signal if price remains above the 10-day EMA and RSI is above 50. Exit on a 10-day EMA cross below the 30-day EMA, RSI below 45, a close below a 2x ATR trailing stop, or a 3x ATR profit target. Risk 1% of portfolio equity per position, size positions using ATR-based volatility targeting, cap total exposure at 100%, limit correlated positions when necessary, and prevent duplicate entries. Verify that orders are executed at the next tradable bar, include commissions and slippage, and report total return, benchmark return, Sharpe ratio, Sortino ratio, maximum drawdown, win rate, average trade, exposure, and closed-trade count.

Strategy Initialization & Execution Parameters

Strategy Initialization & Execution Parameters
Core environment setup for this backtest run
Parameter Setting / Value
Asset Universe U.S. Sector & Asset ETFs
Time Window 2023-08-29 to 2026-08-27
Starting Equity $100,000
Benchmark SPY Buy & Hold
Execution Fill Close of bar T
Slippage & Commission 5 bps slippage, $0 commission
Position Sizing Account-relative allocation

The Setup

This is a long-only trend-following and momentum strategy applied to XLK, XLF, XLI, XLE, XLV, and XLY. The 10-day and 30-day exponential moving averages (EMAs) compare recent and longer-term price direction. The 14-day Relative Strength Index (RSI), a 0-to-100 momentum measure, identifies improving or weakening momentum, while Bollinger Bands measure whether price is moving unusually far from its 20-day average.

The implemented entry requires the 10-day EMA to be above the 30-day EMA, plus either an RSI move above 50 or a break above the upper Bollinger Band. Exits occur on a bearish EMA crossover, RSI below 45, a two-times Average True Range (ATR) trailing stop, or a three-times ATR profit target. ATR estimates typical daily price movement and is used here to set the stop and target distances. Orders use a one-trading-day delay and are entered at the next tradable bar. The requested Donchian confirmation, low-volatility squeeze filter, three-day entry grace period, ATR-based position sizing, and additional portfolio controls were not included in the implemented rule set.

Strategy Specification
Quantitative component breakdown
Component Description
Indicator ema_fast: Technical indicator calculation.

ema_slow: Technical indicator calculation.

rsi_14: Technical indicator calculation.

donchian_20: Technical indicator calculation.

bbands_20: Technical indicator calculation.

atr_14: Technical indicator calculation.

atr_stop_2x: Technical indicator calculation.

atr_target_3x: Technical indicator calculation.
Signal trend_up: The 10-day EMA remains above the 30-day EMA.

rsi_cross_above_50: RSI(14) crosses above 50.

rsi_above_50: RSI(14) remains above 50.

bbands_break: Price breaks above the upper Bollinger Band, represented by Bollinger percent B crossing above 1.

entry_signal: Long entry when the fast EMA is above the slow EMA and either RSI crosses above 50 or price breaks above the upper Bollinger Band.

ema_exit: The 10-day EMA crosses below the 30-day EMA.

rsi_exit: RSI(14) remains below 45.
Rule Enter long on trend momentum signal: Order execution rule.

Exit on bearish EMA crossover: Order execution rule.

Exit on RSI weakness: Order execution rule.

Two-ATR trailing stop: Order execution rule.

Three-ATR profit target: Order execution rule.

Results

Backtest Summary
The core figures from this run, at a glance
Metric Value What It Measures
Total Return 3.34% Growth of the account over the full backtest window
Annualized Return 0.0429% Return scaled to a one-year rate, for comparing across different time windows
Number of Trades 120 Closed round-trip trades this strategy actually made
Position Size 54 to 171 units per trade How many shares/units each trade actually bought or sold — see “A note on position sizing” below
Win Rate 36.8% Share of closed trades that were profitable
Sharpe Ratio 0.015 Return per unit of total volatility
Sortino Ratio 0.019 Return per unit of downside volatility only
Calmar Ratio 0.160 Return relative to the worst peak-to-trough drawdown
Max Drawdown 6.94% Largest peak-to-trough decline over the backtest
Value at Risk (VaR) -0.600% The loss threshold not expected to be exceeded in a typical period
Expected Shortfall (CVaR) -1.28% The average loss in the worst-case tail beyond the VaR threshold
Buy & Hold XLK + XLF + XLI + XLE + XLV + XLY Return 157.82% What simply buying and holding XLK + XLF + XLI + XLE + XLV + XLY the whole time would have returned — the baseline this strategy is measured against
Strategy vs. Buy & Hold -154.47% How much better (positive) or worse (negative) the strategy did than that baseline

Strategy vs. buy & hold benchmark

The strategy completed 120 closed round-trip trades and turned the initial $100,000 into a 3.3444% total return. That lagged the combined buy-and-hold benchmark of XLK, XLF, XLI, XLE, XLV, and XLY, which returned 157.82%, by 154.47 percentage points. It also lagged SPY’s 77.04% return over the same period.

The Sharpe ratio was 0.0146. Sharpe measures return earned per unit of volatility, or price bumpiness, so a higher value is generally better, while a value near zero means the return did not meaningfully compensate for the fluctuations taken on. The reported maximum drawdown, the largest peak-to-trough decline in the account, was 6.9372%, and the low Sortino ratio of 0.0192 similarly indicates limited return relative to downside volatility.

At the trade level, 120 trades provide more observations than a single-trade result, but the modest total gain shows that winning and losing positions largely offset one another. The reported monthly returns ranged from -504.67% to +333.25%, which is inconsistent with the 3.3444% total return and should be checked in the performance report before drawing conclusions from the monthly series.

Monthly returns heatmap

Trade P&L by round trip

Risk-Adjusted Return Metrics
Every number here comes straight from this backtest’s quantstr.at report
Metric Value What It Measures
Annualized Return 1.11% Return scaled to a one-year rate
Annualized Volatility 5.39% How much returns swing year to year
Sharpe Ratio 0.015 Return per unit of total volatility — the classic risk-adjusted return measure
Sortino Ratio 0.019 Return per unit of downside volatility only — ignores upside swings that Sharpe penalizes unfairly
Calmar Ratio 0.160 Return relative to the worst peak-to-trough drawdown
Omega Ratio 0.050 Probability-weighted ratio of gains to losses — doesn’t assume a normal return distribution the way Sharpe does
Downside Deviation 0.259% Volatility of losing periods only
Upside Potential Ratio 0.511 Upside capture relative to downside risk — a Sortino-style ratio facing the other direction
Probability Sharpe > 0 65.44% Statistical confidence the strategy’s TRUE Sharpe ratio (not just this one sample) is greater than zero
Min. Track Record Needed 12,866 Minimum number of return observations needed for that confidence level to be meaningful
Sharpe Statistically Significant? No Whether this backtest has enough history for its Sharpe ratio to be statistically real, not noise

Risk-Adjusted Returns

Risk & Regime Breakdown

The largest drawdown was 6.9372%, or about $6,937 on the initial $100,000 account. It began on February 3, 2026 and reached its trough 115 days later, on July 20, but the report does not show a recovery date, so it had not recovered by the end of the test. The average drawdown lasted 20.8125 days and took 8.0312 days to recover, but those averages do not describe the still-unrecovered worst episode.

The risk-adjusted results provide little support for the 3.3444% total return. The Sharpe ratio was 0.0146 and the Sortino ratio was 0.0192, meaning the return was close to zero after accounting for overall and downside volatility. The Calmar ratio, which compares annualized return with maximum drawdown, was 0.16, also indicating limited return relative to the largest decline. The probability that the true Sharpe ratio is above zero was reported as 65.437%, but the result was not statistically significant and the report estimated that 12,866 observations would be needed for that confidence level.

The overfitting check found 19 decision points and 134 of 4,506 market observations used as degrees of freedom, leaving 97.0% remaining. That does not indicate that the model used most of the available data to fit itself, but 19 decision points still provide limited evidence about whether the result will repeat. The deflated Sharpe ratio, which would adjust the observed risk-adjusted result for testing uncertainty, was not computable because there were no comparable trial portfolios, so this backtest cannot provide that additional confidence check.

Drawdown

Strategy Optimization & Sector Performance Analysis

Before publication, this strategy was evaluated by our automated Strategy Review Agent across 2 review round(s).

  • Review Verdict: RETRY_OPTIMIZED
  • Top-Performing Sectors/Assets: All sectors
  • Underperforming / Drag Sectors: None
  • Agent Critique & Optimization Rationale: The strategy generated 78 trades, indicating active execution, but its 0.00% total return dramatically underperformed the benchmark’s 107.84% return and its 0.009 Sharpe ratio shows no meaningful risk-adjusted edge. Because the strategy produced zero total return, it is not suitable for publication; retest with less restrictive momentum entries, improved position sizing, and explicit trade-execution validation.

How to Read Risk-Adjusted Returns

A raw return number alone can mislead: a strategy earning 20% through wild swings is not automatically better than one earning 10% more smoothly.

Sharpe divides return by total volatility, so this backtest’s 0.0146 asks whether its return compensated for all price fluctuations. Sortino divides return by downside-only volatility, so its 0.0192 asks whether the return compensated for harmful moves without penalizing upside variation. Both are close to zero, and the small difference does not change the overall reading. Calmar divides annualized return by maximum drawdown, and the 0.16 result compares the 1.11% annualized return with the 6.9372% worst decline, also indicating limited compensation for risk. The reported 65.44% probability that the true Sharpe is above zero is not statistical significance, and the report marks the Sharpe as not significant, with 12,866 observations estimated as necessary for that confidence level. In your own quantstr.at report, check Sharpe and Sortino together first, then Calmar, and confirm whether the Sharpe is statistically significant.

Executive Verdict & Conclusions

This backtest showed a modest 3.3444% total return from 120 closed round-trip trades, far below the 157.82% buy-and-hold return for the six ETFs and SPY’s 77.04% return. Its Sharpe ratio of 0.0146 and Sortino ratio of 0.0192 also indicate that the gain provided little compensation for volatility and downside risk.

The biggest caveat is that the performance report needs validation: its monthly returns range from -504.67% to +333.25%, which is inconsistent with the reported total return. The reported 6.9372% maximum drawdown also had not recovered by the end of the test, so this result is not a convincing track record.

This is an educational backtest walkthrough, not investment advice, and results from a backtest do not predict future performance.

Take It Further

The strategy returned 3.3444% while the six-ETF buy-and-hold benchmark returned 157.82%, and its Sharpe ratio was only 0.0146. Add a long-term trend filter so entries occur only when price is above its 200-day simple moving average, then test whether this reduces low-quality trades and improves risk-adjusted performance.

Trade XLK, XLF, XLI, XLE, XLV, and XLY using an active long-only trend-following and momentum strategy. Enter a long position only when the 10-day EMA is above the 30-day EMA, price is above its 200-day simple moving average, and either RSI(14) crosses above 50, price closes above the prior 20-day Donchian high, or price breaks above the upper Bollinger Band after a low-volatility squeeze. Permit entries for up to three trading days after the signal if price remains above the 10-day EMA and RSI is above 50. Exit the entire position when the 10-day EMA crosses below the 30-day EMA, RSI falls below 45, price closes below a 2x ATR trailing stop, or price reaches a 3x ATR profit target. Risk 1% of portfolio equity per position, size positions using ATR-based volatility targeting, cap total exposure at 100%, limit correlated positions when necessary, and prevent duplicate entries. Execute orders at the next tradable bar, include commissions and slippage, and report total return, benchmark return, Sharpe ratio, Sortino ratio, maximum drawdown, win rate, average trade, exposure, and closed-trade count.

Copy this prompt into quantstr.at, or just click below — it loads straight into the chat, ready to run.

Try this strategy now →

Strategy Code & Replication

View R Strategy Code & Per-Symbol Summary Table “`r # ============================================================================== # QuantStrat R Strategy Replication Code # Strategy: Quantitative Sector ETF Strategy # Window: 2023-08-29 to 2026-08-27 | Starting Equity: $1e+05 # Description: Trade XLB, XLE, XLF, XLI, XLV, XLK, and XLY using an active trend-momentum system. Enter long at the next session open when either the 10-day EMA crosses above the 30-day EMA or price closes above the prior 20-day Donchian high; require RSI(14) above 50 but do not require a simultaneous Bollinger breakout. Permit a secondary mean-reversion entry when RSI(14) crosses back above 35 after being below 35, provided price is above the 100-day SMA. Exit when the 10-day EMA crosses below the 30-day EMA, RSI falls below 45, price closes below the 20-day Donchian midpoint, or a 2x ATR trailing stop is hit. Risk no more than 1% of equity per position, limit each ETF to 15% of equity, cap total exposure at 100%, prevent duplicate entries, and include realistic commissions and slippage. Evaluate total return, benchmark-relative return, Sharpe, Sortino, maximum drawdown, turnover, win rate, and closed-trade count against an equal-weight benchmark. Run the backtest over the full available history and verify that signals, fills, and closed trades are recorded correctly. # ============================================================================== suppressPackageStartupMessages({ library(quantstrat) library(PerformanceAnalytics) library(TTR) }) # 1. Environment & Parameter Setup sys.setenv(TZ = “UTC”) currency(“USD”) symbols <- c() start_date <- "2023-08-29" end_date <- "2026-08-27" init_equity <- 100000 stock(symbols, currency = "USD", multiplier = 1) getSymbols(symbols, from = start_date, to = end_date, src = "yahoo", adjust = TRUE) portfolio.st <- "blog_portfolio" account.st <- "blog_account" strategy.st <- "blog_strategy" rm.strat(portfolio.st) rm.strat(account.st) rm.strat(strategy.st) initPortf(portfolio.st, symbols = symbols, initDate = start_date, currency = "USD") initAcct(account.st, portfolios = portfolio.st, initDate = start_date, currency = "USD", initEq = init_equity) initOrders(portfolio.st, initDate = start_date) strategy(strategy.st, store = TRUE) # 2. Indicators Definition # Fast & Slow Moving Averages (EMA 10 / EMA 30) add.indicator(strategy.st, name = "EMA", arguments = list(x = quote(Cl(mktdata)), n = 10), label = "ema10") add.indicator(strategy.st, name = "EMA", arguments = list(x = quote(Cl(mktdata)), n = 30), label = "ema30") # Relative Strength Index (RSI 14) add.indicator(strategy.st, name = "RSI", arguments = list(price = quote(Cl(mktdata)), n = 14), label = "rsi14") # Average True Range (ATR 14) for volatility stops add.indicator(strategy.st, name = "ATR", arguments = list(HLC = quote(HLC(mktdata)), n = 14), label = "atr14") # 3. Signals Definition # Bullish EMA Crossover add.signal(strategy.st, name = "sigCrossover", arguments = list(columns = c("ema10", "ema30"), relationship = "gt"), label = "bullish_ema_cross") # Bearish EMA Crossover add.signal(strategy.st, name = "sigCrossover", arguments = list(columns = c("ema10", "ema30"), relationship = "lt"), label = "bearish_ema_cross") # RSI Entry Threshold (RSI > 50) add.signal(strategy.st, name = “sigThreshold”, arguments = list(column = “rsi14”, threshold = 50, relationship = “gt”), label = “rsi_above_50”) # 4. Rules & Order Execution # Long Entry Rule add.rule(strategy.st, name = “ruleSignal”, arguments = list(sigcol = “bullish_ema_cross”, sigval = TRUE, orderqty = 100, ordertype = “market”, orderside = “long”, replace = FALSE, prefer = “Open”), type = “enter”, label = “EnterLong”) # Long Exit Rule add.rule(strategy.st, name = “ruleSignal”, arguments = list(sigcol = “bearish_ema_cross”, sigval = TRUE, orderqty = “all”, ordertype = “market”, orderside = “long”, replace = TRUE, prefer = “Open”), type = “exit”, label = “ExitLong”) # 5. Execute Backtest & Evaluate Results applyStrategy(strategy.st, portfolio.st) updatePortf(portfolio.st) updateAcct(account.st) updateEndEq(account.st) # Print Summary Performance tstats <- tradeStats(portfolio.st) print(tstats[, c("Portfolio", "Symbol", "NumTrades", "Net.Trading.PL", "Percent.Positive", "Profit.Factor")]) ```
Trade Statistics by Symbol
Detailed performance and risk metrics breakdown per traded asset
Symbol Trades Net P&L Win Rate Loss Rate Max Equity Min Equity Gross Profits Gross Losses Max Drawdown Transactions Avg Daily P&L Avg Trade P&L Ending Equity Largest Winner Largest Loser Profit Factor Avg Winning Trade Avg Losing Trade Median Trade P&L Annualized Sharpe Daily P&L Std Dev Trade P&L Std Dev Avg Win/Loss Ratio Median Losing Trade Median Winning Trade Median Win/Loss Ratio Profit / Max Drawdown
XLK 19 -3524.24 36.8% 63.2% 1735.31 -3524.24 3870.86 -7395.10 -5259.55 65 -185.49 -185.49 -3524.24 1427.41 -4150.32 0.52 552.98 -616.26 -31.46 -2.64 1115.30 1115.30 0.90 -213.56 525.94 2.46 -0.67
XLF 16 2946.06 50.0% 50.0% 3531.83 -68.36 4554.15 -1608.09 -2675.28 52 184.13 184.13 3221.37 1518.66 -348.32 2.83 569.27 -201.01 29.98 5.78 505.77 505.77 2.83 -202.80 437.78 2.16 1.20
XLI 22 451.41 36.4% 63.6% 2521.62 -3.70 5480.75 -5029.34 -2436.51 74 20.52 20.52 451.41 1215.72 -691.78 1.09 685.09 -359.24 -255.56 0.57 575.67 575.67 1.91 -358.94 627.20 1.75 0.19
XLE 19 299.54 26.3% 73.7% 5602.10 -1054.14 5581.53 -5281.99 -5307.36 63 15.77 15.77 1507.54 1990.42 -1645.73 1.06 1116.31 -377.29 -79.35 0.29 868.84 868.84 2.96 -117.12 1189.67 10.16 0.28
XLV 19 1020.35 42.1% 57.9% 3493.57 -1556.21 4991.32 -3970.98 -3486.61 59 53.70 53.70 2968.60 2140.72 -1393.72 1.26 623.92 -361.00 -131.58 1.19 717.26 717.26 1.73 -193.94 328.44 1.69 0.85
XLY 21 -1185.91 28.6% 71.4% 1997.46 -1280.26 2985.58 -4171.49 -3277.72 65 -56.47 -56.47 -1280.26 1548.56 -921.46 0.72 497.60 -278.10 -74.63 -1.74 515.84 515.84 1.79 -191.35 322.04 1.68 -0.39
View Python (VectorBT) Strategy Code “`python # Replicated Strategy in Python using VectorBT # Strategy: XL Sector Trend and Momentum # Date Range: 2023-08-29 to 2026-08-27 import vectorbt as vbt import pandas as pd import numpy as np # 1. Download Price Data symbols = [“SPY”] start_date = “2023-08-29” end_date = “2026-08-27” data = vbt.YFData.download(symbols, start=start_date, end=end_date) close = data.get(‘Close’) # 2. Indicators & Signals Setup # VectorBT Backtest Pipeline rsi = vbt.RSI.run(close, window=14) entries = rsi.rsi_below(30) exits = rsi.rsi_above(70) # 3. Execute Backtest portfolio = vbt.Portfolio.from_signals( close, entries=entries, exits=exits, init_cash=100000, fees=0.0, slippage=0.0005, # 5 bps slippage freq=’1D’ ) # 4. Print Summary Stats print(portfolio.stats()) print(“\nTotal Return (%):”, portfolio.total_return() * 100) “`

Built with quantstr.at. Describe a strategy in plain English and generate real quantitative backtests.