Skip to content
All strategies

Strategies

Double-7s Portfolio (pinkfish, F. Aultman after Connors & Alvarez): Buy a 7-Day Closing Low Above the 200-SMA, Sell a 7-Day Closing High, Equal Weight Across SPY/QQQ/DIA/IWM, USEQ Daily Bars, Long-Only

Outcome: Abandoned

Double7sPortfolioUseq

Outcome Summary

Double7sPortfolioUseq put the Connors/Alvarez Double-7s rule on four broad US index ETFs on the zero-commission USEQ venue, long-only and equal-weighted, with a 200-SMA regime filter. It never passed verification: Layer 3 sandbox smoke tests timed out at 300s on all 3 iterations, including iteration 2, which made per-bar work incremental. The iteration-3 profiling notes say the time went to the backtest runner's catalog instrument scan, not the strategy. The run was abandoned before any backtest, optimization, analyst or risk review, so its edge was never measured.

Hypothesis

A faithful statement of pinkfish's 'double-7s-portfolio' example (Farrell Aultman, MIT licence; https://github.com/fja05680/pinkfish/blob/18a0d557ead25e0e82de3ece86bcab6c325a59ed/examples/portfolios/double-7s-portfolio/strategy.py). It applies Larry Connors and Cesar Alvarez's Double 7's rule from 'Short Term Strategies that Work' to a portfolio of ETFs. The rule is long-only mean reversion on daily closes: buy when an ETF closes at its 7-day closing low while it is above its 200-day SMA, and sell when it closes……Show moreShow less

A faithful statement of pinkfish's 'double-7s-portfolio' example (Farrell Aultman, MIT licence; https://github.com/fja05680/pinkfish/blob/18a0d557ead25e0e82de3ece86bcab6c325a59ed/examples/portfolios/double-7s-portfolio/strategy.py). It applies Larry Connors and Cesar Alvarez's Double 7's rule from 'Short Term Strategies that Work' to a portfolio of ETFs. The rule is long-only mean reversion on daily closes: buy when an ETF closes at its 7-day closing low while it is above its 200-day SMA, and sell when it closes at its 7-day closing high. The source was written for ETFs, so it goes on the zero-commission USEQ venue with DAILY bars, which have decades of history. The source takes the symbol list from the caller and does not hard-code one. We use the four broad US index ETFs Connors tested the rule on: SPY, QQQ, DIA and IWM. That is 4 names, within the ~10-name cap from lesson 173. The list is a fixed choice, but all four are index trackers that have existed and stayed liquid since 1993-2000, so there is no survivorship selection among single names. Default options, copied exactly from the source: period=7, stop_loss_pct=1.0 (the source reads this as 100%, so the stop sits at 0 and never fires; it stays in as a parameter), margin=1 (no leverage), use_regime_filter=True, use_vola_weight=False (equal weight 1/N), use_adj=False. use_adj cannot be honoured here: our USEQ prices are split-adjusted and dividends are not in PnL. That is a known, small deviation from the source, because it uses unadjusted closes and pinkfish's own accounting also leaves dividends out. Fee arithmetic: USEQ has zero commission, and ~0.02-0.05% round trip for spread and impact on these ETFs. Published Double-7s results on SPY show an average trade of roughly +0.6 to +1.0% gross over a 3-8 session hold. Even if half of that decays after publication (~0.4%), it is about 10x the cost. The expected lifetime trade count across 4 ETFs is ~8-12 trades per ETF per year over ~25 years, so about 800-1,100 trades. The strategy has a flat state. Each ETF is in the market about 25-35% of the time. The book is fully flat whenever no ETF is at a 7-day low above its 200-SMA, and it is also flat through bear regimes, when closes sit below the 200-SMA.

Iteration 3 of a Layer-3 sandbox_timeout. The trading rules are unchanged from iteration 2; only the docstring changed, recording the diagnosis. I ran iteration 2's exact code on the same 2400-day, 4-leg window through run_backtest_in_subprocess and sampled the child with py-spy. All of its wall time was in src/backtesting/runner.py:535 -> ParquetDataCatalog.instruments(instrument_ids=...) -> fsspec glob, which walks every instrument class directory in data/catalog/data, including 56,006 binary_option……Show moreShow less

Iteration 3 of a Layer-3 sandbox_timeout. The trading rules are unchanged from iteration 2; only the docstring changed, recording the diagnosis. I ran iteration 2's exact code on the same 2400-day, 4-leg window through run_backtest_in_subprocess and sampled the child with py-spy. All of its wall time was in src/backtesting/runner.py:535 -> ParquetDataCatalog.instruments(instrument_ids=...) -> fsspec glob, which walks every instrument class directory in data/catalog/data, including 56,006 binary_option (Polymarket) directories. The host load was ~77 on 18 cores. The child used ~9% CPU, blocked on disk I/O, and was still inside the scan with no bar processed when it hit a 900s limit (3x the sandbox budget). Strategy per-bar cost is already constant: a deque with a running-sum SMA200 and an O(7) high/low per leg, over ~2.4k daily bars per leg. So this timeout is catalog and host plumbing (lesson 155 (b)), not strategy code, and no code edit can fix it. The fix belongs in the runner: a per-class/per-id instrument lookup instead of a whole-catalog glob, or pruning the Polymarket dirs. Deviations from the source, as before: use_adj cannot be honoured (USEQ prices are split-adjusted and dividends are not in PnL); the end-of-data forced exit is left to the engine's close-out at stop; use_vola_weight=False (equal weight), as in the source defaults.

verification_loop: sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of SPY.USEQ-1-DAY-LAST-EXTERNAL). This almost always means per-bar work that scales with history — e.g. rescanning the full funding/supplementary series, or rebuilding a list and calling min()/sorted() inside calculate_signal()/on_bar() on every bar. Precompute sorted timestamp arrays ONCE in __init__ and use bisect, or cache lookups keyed by timestamp, so per-bar cost is O(log n) not O(n).

Implementation

Stratmill translation of pinkfish's double-7s-portfolio (F. Aultman, MIT). Uses Connors & Alvarez's Double 7's rule on SPY/QQQ/DIA/IWM daily USEQ bars, long-only with equal 1/N weights. An ETF is bought when it closes at its 7-day closing low while above its 200-day SMA. It is sold when it closes at its 7-day closing high, or below its stop (the default stop_loss_pct=1.0 puts the stop at 0, so it never fires, as in the source).

Verification Results

sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of SPY.USEQ-1-DAY-LAST-EXTERNAL). This almost always means per-bar work that scales with history — e.g. rescanning the full funding/supplementary series, or rebuilding a list and calling min()/sorted() inside calculate_signal()/on_bar() on every bar. Precompute sorted timestamp arrays ONCE in __init__ and use bisect, or cache lookups keyed by timestamp, so per-bar cost is O(log n) not O(n).

Outcome Summary

When a sandbox timeout comes from catalog scanning before the first bar and not from strategy code, rewriting the strategy cannot fix it; it needs an infrastructure fix, not more coding iterations.

After 3 iterations it was abandoned in a verification loop: each Layer 3 sandbox backtest went past the 300s wall-clock limit on the 2400-day SPY.USEQ daily window. The iteration-3 notes say profiling traced the wall time to the ParquetDataCatalog.instruments() glob over the catalog (about 56k Polymarket dirs), which runs before any strategy code, not to per-bar strategy work.

A long-only, equal-weight Double-7s mean-reversion portfolio on SPY/QQQ/DIA/IWM daily USEQ bars (a translation of pinkfish's example of the Connors & Alvarez rule): buy an ETF at its 7-day closing low while it is above its 200-day SMA, and sell it at a 7-day closing high.

No performance numbers exist. The backtest report is empty, optimization did not run, and no return, Sharpe or trade count was produced.

Analysis

double-7s-portfolio

Backtest and paper results are hypothetical. Trading involves risk of loss.