Dividend1000: what a strategy backtest reveals when you look past the winning row
Methodology study · Data reviewed
A strategy can look exceptional in a parameter table and still leave basic questions unanswered. What actually selects the stocks? How much of the result comes from staying in cash? Was the attractive setting chosen after seeing the whole history? And does the accounting faithfully represent the execution rule?
We examined those questions in the supplied Dividend1000 leadership engine. A fresh run of its six-factor specification, with the NIFTY 50 moving-average gate explicitly selected, produced a historical compound annual growth rate of 19.7% and a maximum drawdown of 19.6%. The highest-return row in the accompanying sweep reported 40.0% and a 17.4% drawdown. These are outputs of research software, not audited investment returns. Understanding the distance between them is more useful than treating either number as an expectation.
“Dividend” describes the universe; price leadership drives the score
The engine reads a frozen ticker list and price history. Its database contains 1,759,554 rows for 994 symbols, from 1 January 2018 to 3 September 2026. It is therefore misleading to describe this particular dataset simply as 500 current index constituents. The simulation requires at least 250 available closing-price observations before a stock can enter the ranking.
None of the six ranking factors directly measures dividend yield, dividend growth, payout sustainability or corporate earnings. The supplied list establishes the dividend-screen context; the implemented selection process measures price and volume behaviour within that list. It does not reconstruct which companies would have passed the dividend screen at each historical date.
| Factor | Actual measurement |
|---|---|
| Relative strength | The stock’s 20-session gross return divided by the NIFTY 50’s corresponding gross return, minus one. |
| Acceleration | Current relative strength minus relative strength over the preceding, non-overlapping 20-session interval. |
| High-price behaviour | A count of proximity conditions: within 3% of the running closing high, within 1% of the 20-session closing high, and within 0.1% of the running closing high. |
| Trend structure | The fraction of four moving averages—20, 50, 100 and 200 sessions—below the current close. |
| Breakout proxy | The average of two proximity flags: within 1% of the 20-session closing high and within 3% of the running closing high. |
| Volume confirmation | Current volume divided by its 20-session average, capped at five times; missing ratios receive the cross-sectional median. |
Each factor becomes a cross-sectional percentile, and the score is their equal-weight mean. That makes unlike units comparable, but does not make the factors independent. The high-price and breakout measures overlap substantially. Nor does the “breakout” factor measure the length or depth of a consolidation. Names in code are useful labels; their formulas establish what was actually tested.
Selection, retention and market exposure are different decisions
The reference run targets 20 positions. At month-end it ranks eligible stocks, retains existing positions whose rank remains within 180, and fills vacant slots from the remaining leaders after excluding the most volatile 20% of the new-entry pool by 63-session return volatility. That volatility restriction applies to new entries; it is not an automatic exit rule for existing positions.
Retained positions keep their share counts, so weights drift. New positions receive equal slot values. The sticky parameter changes how forgiving retention is: with three observations, a holding can remain eligible if it ranked within the retention boundary in any of the last three stored, gate-on ranking observations. These are not necessarily three consecutive calendar months. The six-factor reference run uses the script’s default of one observation.
The SMA200 gate compares the NIFTY 50’s month-end close with its 200-session simple moving average. A close below that average schedules a move to cash at the next session’s open. This monthly rule does not promise immediate protection during an intra-month fall. The model assumes 20 basis points of cost on each traded side and a constant 5.5% annual cash yield; neither assumption is a guaranteed live outcome.
Explicit arguments matter here. Although the file’s introductory description discusses SMA200, its current command-line default is navst, a gate based on the strategy’s own value. The runs discussed here explicitly use --gate sma200.
What the sweep actually contains
The sweep generator schedules 248 entries before de-duplication. The saved results contain 233 unique configuration rows: 50 in the first stage and 183 in the second. Across those saved rows, reported CAGR ranges from 18.3% to 40.0%, with a median of 31.0%. This is a distribution across related parameter choices on the same history, not 233 independent experiments or a probability distribution of future returns.
| Specification | CAGR | Maximum drawdown | FY2025–26 return |
|---|---|---|---|
| Six factors; SMA200; 20-session RS; sticky 1; no stock stop | 19.7% | −19.6% | 2.1% |
| Sweep centre with SMA200; volume and acceleration dropped; sticky 2; 15% stock stop | 34.5% | −22.4% | 13.5% |
| Highest-CAGR sweep row; four factors; SMA200; 40-session RS; sticky 3; 15% stock stop | 40.0% | −17.4% | 16.6% |
| Engine’s NIFTY 50 closing-price comparator | 10.2% | −38.4% | −3.6% |
All three strategy rows retain a target of 20 positions and a rank boundary of 180. The first and third rows were rerun for this article; the middle row is read directly from the sweep. The winning row is a four-factor variant with several simultaneous changes, so its advantage cannot be attributed solely to a longer relative-strength window or a more patient retention rule. Its 15% stop is checked at the close and executed at the following open; it does not cap realised losses at 15%.
The sweep runner has another reproducibility trap: it omits zero-valued arguments. A row labelled cash-yield=0 therefore falls back to the engine’s 5.5% default; vol-exclude=0 likewise falls back to 20%. Those labels do not prove that zero-cash-yield or zero-exclusion experiments actually ran.
The market gate also produces a revealing trade-off. Within the second-stage rows, the breadth-gated variants have a median reported CAGR of 33.6% and median drawdown of −15.0%, compared with 34.0% and −19.7% for SMA200. But their median FY2025–26 return is only 1.2%, versus 12.1% for SMA200. The breadth gate here is computed from the frozen dividend universe, not a reconstructed historical all-market universe. The aggregate comparison describes this search; it does not isolate a causal gate advantage.
An execution audit changes the numbers
The original monthly rebalance values existing holdings at the execution day’s close before calculating transactions priced at that day’s open. That uses information unavailable at the open and creates an inconsistent cash reconciliation. We tested a narrowly scoped diagnostic copy that values existing holdings at opening prices instead, using the previous close only when an opening value is missing.
| Run | Original CAGR / drawdown | Opening-value diagnostic CAGR / drawdown |
|---|---|---|
| Six-factor reference | 19.70% / −19.58% | 19.59% / −18.80% |
| Highest-CAGR sweep configuration | 39.99% / −17.38% | 39.17% / −18.16% |
The diagnostic does not erase the stronger variant’s historical result, but it demonstrates why an executable backtest still needs accounting review. It is not a complete repair or validation: missing-open execution fallbacks, possible negative cash balances, financing assumptions and transaction accounting still need examination. In particular, the printed turnover statistic omits the sale notional of some daily stop exits, so its 3.71-times figure for the winning row should not be treated as a complete trading-cost budget.
There are broader limitations. The universe is frozen, the engine uses closing prices without explicitly crediting cash dividends, and the database’s non-null adjusted closes equal its closes. These records do not establish a dividend-reinvested total-return history. The NIFTY comparator is also a closing-price series. No separate untouched test period appears in the supplied sweep. Even the reference run’s 78.6% FY2023–24 return followed by 2.1% in FY2025–26 illustrates how much a long-window average can conceal.
The useful result of this exercise is a more precise research question: whether leadership selection, forgiving retention and conditional market exposure remain effective after point-in-time universe reconstruction, consistent accounting and genuinely unseen testing. A winning sweep row helps formulate that question. It does not answer it on its own.
Sources and reproducibility
Source files: div1000_leaders.py, sweep.py, sweep_results.csv, div1000_tickers.txt, div1000_xl_prices.db and gf_index.db, under /opt/investezee/div1000/. The accompanying editorial evidence archive preserves the source snapshots, arguments, diagnostic diff, daily values and result tables. CAGR is recomputed as (ending NAV / starting NAV) ** (365.25 / elapsed days) - 1; drawdown is the minimum of NAV / running maximum NAV - 1.
SELECT COUNT(*), COUNT(DISTINCT symbol), MIN(d), MAX(d),
SUM(close != adj_close)
FROM px;
cd /opt/investezee/div1000
OPENBLAS_NUM_THREADS=1 /opt/investezee/venv/bin/python3 div1000_leaders.py \
--db div1000_xl_prices.db --universe div1000_tickers.txt \
--index-db gf_index.db --gate sma200 \
--nav /tmp/div_base_nav.csv --trades /tmp/div_base_trades.csv
# Highest-CAGR sweep row: add these explicit arguments to the command above,
# using different NAV and trades output filenames:
--drop vol,acc --hold 20 --retain 180 --rs-win 40 --sticky 3 --stop 0.15