Skip to content
All strategies

Strategies

QuantRocket UpMinusDown: Fixed 252/22-Day Stock Momentum, Monthly Equal-Weight Long/Short

Outcome: Abandoned

QuantRocketUpMinusDownFixedStockMomentum

Outcome Summary

The strategy translated QuantRocket’s fixed skipped-month momentum rules to a declared ten-stock USEQ universe with monthly equal-weight long/short allocations. Its sandbox produced negative average trade returns and a Sharpe of -0.16 across 104 closed legs. After three iterations, QA identified a blocking warm-up defect caused by differing instrument history start dates, and the strategy was abandoned before full-history backtesting or optimization.

Hypothesis

Faithful translation of QuantRocket LLC's Apache-2.0 UpMinusDown signal and allocation rules, with explicitly declared platform instruments and costs. Source: https://github.com/quantrocket-codeload/moonshot-intro/blob/8717e87e279e3615bdd4c7787ce746b92d0713d7/intro_moonshot/umd.py. Preserve MOMENTUM_WINDOW=252, RANKING_PERIOD_GAP=22, TOP_N_PCT=50 and REBALANCE_INTERVAL='M'. Use synchronized USEQ daily session bars. The base UpMinusDown class does not specify a universe; this experiment declares a fixed ten-stock platform universe and does not claim to reproduce the demo's unspecified usstock-free membership. UpMinusDownDemo additionally sets CODE='umd-demo', DB='usstock-free-1d', UNIVERSES='usstock-free', TOP_N_PCT=50 and COMMISSION_CLASS=USStockCommission; that commission class specifies $0.005 per share. Our experiment uses USEQ's zero commissions with modeled spread and impact instead. No optimization, additional filter, stop-loss or signal-strength sizing is permitted.

The user's faithful-source instruction determines the mechanism and stock market; changing it to crypto, options or a different ranking scheme would violate that instruction. This supplies multi-instrument long/short exposure, addressing the portfolio's 82.6% long-only concentration. Corpus instrument concentration was not supplied. The failed sector-ETF experiment used risk-adjusted momentum and defensive allocation; this experiment instead freezes the source's skipped-month raw-return ranks and equal weights. Their related mechanisms remain a reason for caution, not evidence of an edge. The fixed present-day stock basket introduces survivorship and selection bias and must be reported as a platform-universe experiment.

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] In the 1379-day sandbox, avg_trade_return_pct was -1.84% over 104 closed legs, PF 0.80 and Sharpe -0.16. Short legs won only 27% of the time. 12-1 momentum on 10 present-day mega-caps, half long and half short, gives a very thin cross-section that tends to be dominated by sector beta (XOM, KO, PEP, PG, JNJ vs tech). It also carries survivorship bias and modeled borrow cost (~1.2% over the sandbox). The full-history……Show moreShow less

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] In the 1379-day sandbox, avg_trade_return_pct was -1.84% over 104 closed legs, PF 0.80 and Sharpe -0.16. Short legs won only 27% of the time. 12-1 momentum on 10 present-day mega-caps, half long and half short, gives a very thin cross-section that tends to be dominated by sector beta (XOM, KO, PEP, PG, JNJ vs tech). It also carries survivorship bias and modeled borrow cost (~1.2% over the sandbox). The full-history backtest should measure it; this is not a blocking issue. - [critical] The full-history backtest is certain to crash. The factory takes the backtest/optimisation window from the PRIMARY leg's first catalog bar (subprocess_entry.py:765/994). For AAPL.USEQ that is 1980-12-12, but GOOGL.USEQ starts 2004-08-19, AMZN 1997-05-15 and MSFT 1986-03-13. The base template's alignment barrier holds primary processing back until every sync leg has a bar at the same timestamp. So the first processed bar is GOOGL's first session. At that point len(self._bars)=275 >= min_bars_required, but the extra-leg buffers hold only one bar each. _seed_rows finds 1 aligned row (needs 253) and raises ValueError('Insufficient timestamp-aligned USEQ momentum history'). In backtests, _maybe_process_primary re-raises exceptions instead of skipping the bar, so the run fails. The same failure hits every walk-forward window that starts before 2004 plus about 253 sessions. The 1379-day sandbox missed it because all legs already had full history in that window. (line 62) — fix: Make seeding tolerant instead of raising. Return 0.0 with no rebalance until len(self._rows) >= window+1, appending one aligned row per processed bar. Alternatively, keep re-seeding on later bars until enough history exists, and clear _rebalance_due while the buffer is short. Also make min_bars_required() check aligned rows, not just primary bars. Keep the hypothesis rules unchanged (252/22, 50%, monthly). - [warning] Once seeded, a row is appended only when a primary bar is processed. If one leg is missing a session, the barrier skips that timestamp and the row is never added. Every later t-22/t-252 offset then shifts by one session for all legs. The effect is small, but it differs from Moonshot's pandas index, which keeps NaN rows. (line 91) — fix: Acceptable as is. Optionally track session dates and skip-forward explicitly. - [warning] The hypothesis-consistency warning ('22-Day bars') is a false positive: 22 is RANKING_PERIOD_GAP in daily sessions, and 1-DAY bars are correct for UMD. The instruments (10 declared USEQ names), the 252/22 skipped-month momentum (close[t-22]/close[t-252]-1 via deque indices -23/-253), pandas average pct rank with top/bottom 50%, resample('M').last() labelling at calendar month-end with weights.shift() (activation one session later when month-end is not a session), equal 1/active weights with gross 1.0x, and the absence of any stop or filter all match the source. Rebalancing is anchored to bar-timestamp calendar months, not an uptime counter. Extra legs are protected against look-ahead by an explicit ts equality check. Whole-share sizing truncates toward zero. — fix: None.

Implementation

Stratmill translation of QuantRocket UpMinusDown on a fixed 10-stock USEQ universe. It ranks each stock by its 12-month return skipping the latest month (close[t-22]/close[t-252]-1), goes long the top 50% and short the bottom 50% at equal weights, and takes the last signal of each calendar month. That signal is applied on the next session (as weights.shift() does) and held until the next monthly rebalance.

This iteration fixes the only Layer-1 failure by removing the disallowed zoneinfo import. USEQ daily bars are stamped at the 16:00 ET session close (20:00 or 21:00 UTC; 17:00 or 18:00 UTC on half-days), so the UTC calendar date always equals the New York session date and month-boundary detection is unchanged. Signal, ranking, rebalance and sizing logic are the same as the previous iteration. Platform adaptations: (1) fills come from next-session market orders, not Moonshot's open-to-open returns; (2) share counts are whole and rounded toward zero; (3) costs are USEQ's zero commission plus modeled spread and impact, not $0.005 per share. The fixed present-day universe adds survivorship bias.

Verification Results

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] In the 1379-day sandbox, avg_trade_return_pct was -1.84% over 104 closed legs, PF 0.80 and Sharpe -0.16. Short legs won only 27% of the time. 12-1 momentum on 10 present-day mega-caps, half long and half short, gives a very thin cross-section that tends to be dominated by sector beta (XOM, KO, PEP, PG, JNJ vs tech). It also carries survivorship bias and modeled borrow cost (~1.2% over the sandbox). The full-history backtest should……Show moreShow less

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] In the 1379-day sandbox, avg_trade_return_pct was -1.84% over 104 closed legs, PF 0.80 and Sharpe -0.16. Short legs won only 27% of the time. 12-1 momentum on 10 present-day mega-caps, half long and half short, gives a very thin cross-section that tends to be dominated by sector beta (XOM, KO, PEP, PG, JNJ vs tech). It also carries survivorship bias and modeled borrow cost (~1.2% over the sandbox). The full-history backtest should measure it; this is not a blocking issue. - [critical] The full-history backtest is certain to crash. The factory takes the backtest/optimisation window from the PRIMARY leg's first catalog bar (subprocess_entry.py:765/994). For AAPL.USEQ that is 1980-12-12, but GOOGL.USEQ starts 2004-08-19, AMZN 1997-05-15 and MSFT 1986-03-13. The base template's alignment barrier holds primary processing back until every sync leg has a bar at the same timestamp. So the first processed bar is GOOGL's first session. At that point len(self._bars)=275 >= min_bars_required, but the extra-leg buffers hold only one bar each. _seed_rows finds 1 aligned row (needs 253) and raises ValueError('Insufficient timestamp-aligned USEQ momentum history'). In backtests, _maybe_process_primary re-raises exceptions instead of skipping the bar, so the run fails. The same failure hits every walk-forward window that starts before 2004 plus about 253 sessions. The 1379-day sandbox missed it because all legs already had full history in that window. (line 62) — fix: Make seeding tolerant instead of raising. Return 0.0 with no rebalance until len(self._rows) >= window+1, appending one aligned row per processed bar. Alternatively, keep re-seeding on later bars until enough history exists, and clear _rebalance_due while the buffer is short. Also make min_bars_required() check aligned rows, not just primary bars. Keep the hypothesis rules unchanged (252/22, 50%, monthly). - [warning] Once seeded, a row is appended only when a primary bar is processed. If one leg is missing a session, the barrier skips that timestamp and the row is never added. Every later t-22/t-252 offset then shifts by one session for all legs. The effect is small, but it differs from Moonshot's pandas index, which keeps NaN rows. (line 91) — fix: Acceptable as is. Optionally track session dates and skip-forward explicitly. - [warning] The hypothesis-consistency warning ('22-Day bars') is a false positive: 22 is RANKING_PERIOD_GAP in daily sessions, and 1-DAY bars are correct for UMD. The instruments (10 declared USEQ names), the 252/22 skipped-month momentum (close[t-22]/close[t-252]-1 via deque indices -23/-253), pandas average pct rank with top/bottom 50%, resample('M').last() labelling at calendar month-end with weights.shift() (activation one session later when month-end is not a session), equal 1/active weights with gross 1.0x, and the absence of any stop or filter all match the source. Rebalancing is anchored to bar-timestamp calendar months, not an uptime counter. Extra legs are protected against look-ahead by an explicit ts equality check. Whole-share sizing truncates toward zero. — fix: None.

Make seeding tolerant instead of raising. Return 0.0 with no rebalance until len(self._rows) >= window+1, appending one aligned row per processed bar. Alternatively, keep re-seeding on later bars until enough history exists, and clear _rebalance_due while the buffer is short. Also make min_bars_required() check aligned rows, not just primary bars. Keep the hypothesis rules unchanged (252/22, 50%, monthly).

The full-history backtest is certain to crash. The factory takes the backtest/optimisation window from the PRIMARY leg's first catalog bar (subprocess_entry.py:765/994). For AAPL.USEQ that is 1980-12-12, but GOOGL.USEQ starts 2004-08-19, AMZN 1997-05-15 and MSFT 1986-03-13. The base template's alignment barrier holds primary processing back until every sync leg has a bar at the same timestamp. So the first processed bar is GOOGL's first session. At that point len(self._bars)=275 >= min_bars_required, but the extra-leg buffers hold only one bar each. _seed_rows finds 1 aligned row (needs 253) and raises ValueError('Insufficient timestamp-aligned USEQ momentum history'). In backtests, _maybe_process_primary re-raises exceptions instead of skipping the bar, so the run fails. The same failure hits every walk-forward window that starts before 2004 plus about 253 sessions. The 1379-day sandbox missed it because all legs already had full history in that window.

Acceptable as is. Optionally track session dates and skip-forward explicitly.

Once seeded, a row is appended only when a primary bar is processed. If one leg is missing a session, the barrier skips that timestamp and the row is never added. Every later t-22/t-252 offset then shifts by one session for all legs. The effect is small, but it differs from Moonshot's pandas index, which keeps NaN rows.

None.

The hypothesis-consistency warning ('22-Day bars') is a false positive: 22 is RANKING_PERIOD_GAP in daily sessions, and 1-DAY bars are correct for UMD. The instruments (10 declared USEQ names), the 252/22 skipped-month momentum (close[t-22]/close[t-252]-1 via deque indices -23/-253), pandas average pct rank with top/bottom 50%, resample('M').last() labelling at calendar month-end with weights.shift() (activation one session later when month-end is not a session), equal 1/active weights with gross 1.0x, and the absence of any stop or filter all match the source. Rebalancing is anchored to bar-timestamp calendar months, not an uptime counter. Extra legs are protected against look-ahead by an explicit ts equality check. Whole-share sizing truncates toward zero.

Iteration History

Verification failed (Layer 1.5 — hypothesis/config consistency) [class=hypothesis_mismatch]: - optimization_plan fixes leverage at 1.0 Also note: - hypothesis states 22-Day bars but config.bar_type is AAPL.USEQ-1-DAY-LAST-EXTERNAL (1-DAY) — QA rejects a timeframe that contradicts the hypothesis; align bar_type or revise the text.

Verification failed (Layer 1 — static analysis): - Import outside the sandbox allowlist: from zoneinfo — strategies may import __future__, abc, bisect, calendar, collections, copy, dataclasses, datetime, decimal, enum, fractions, functools, heapq, itertools, json, math, nautilus_trader, numpy, operator, re, src, statistics, string, typing (line 11)

Outcome Summary

Warm-up readiness must depend on synchronized history across every basket leg, with trading deferred until enough aligned rows exist.

After three iterations, Layer 4 QA verification failed because mismatched instrument histories would leave insufficient aligned warm-up rows and crash the full-history backtest. Weak sandbox performance was explicitly nonblocking.

Apply fixed 252/22-session momentum rankings to ten USEQ stocks, with monthly equal-weight long/short allocations and no optimization or additional filters.

QA reported sandbox Sharpe of -0.16, profit factor of 0.80 and average trade return of -1.84% across 104 closed legs over 1,379 days; short legs won 27% of the time. Full-history backtesting and optimization were not reached.

Abandon Reason

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] In the 1379-day sandbox, avg_trade_return_pct was -1.84% over 104 closed legs, PF 0.80 and Sharpe -0.16. Short legs won only 27% of the time. 12-1 momentum on 10 present-day mega-caps, half long and half short, gives a very thin cross-section that tends to be dominated by sector beta (XOM, KO, PEP, PG, JNJ vs tech). It also carries survivorship bias and modeled borrow cost (~1.2% over the sandbox). The full-history……Show moreShow less

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] In the 1379-day sandbox, avg_trade_return_pct was -1.84% over 104 closed legs, PF 0.80 and Sharpe -0.16. Short legs won only 27% of the time. 12-1 momentum on 10 present-day mega-caps, half long and half short, gives a very thin cross-section that tends to be dominated by sector beta (XOM, KO, PEP, PG, JNJ vs tech). It also carries survivorship bias and modeled borrow cost (~1.2% over the sandbox). The full-history backtest should measure it; this is not a blocking issue. - [critical] The full-history backtest is certain to crash. The factory takes the backtest/optimisation window from the PRIMARY leg's first catalog bar (subprocess_entry.py:765/994). For AAPL.USEQ that is 1980-12-12, but GOOGL.USEQ starts 2004-08-19, AMZN 1997-05-15 and MSFT 1986-03-13. The base template's alignment barrier holds primary processing back until every sync leg has a bar at the same timestamp. So the first processed bar is GOOGL's first session. At that point len(self._bars)=275 >= min_bars_required, but the extra-leg buffers hold only one bar each. _seed_rows finds 1 aligned row (needs 253) and raises ValueError('Insufficient timestamp-aligned USEQ momentum history'). In backtests, _maybe_process_primary re-raises exceptions instead of skipping the bar, so the run fails. The same failure hits every walk-forward window that starts before 2004 plus about 253 sessions. The 1379-day sandbox missed it because all legs already had full history in that window. (line 62) — fix: Make seeding tolerant instead of raising. Return 0.0 with no rebalance until len(self._rows) >= window+1, appending one aligned row per processed bar. Alternatively, keep re-seeding on later bars until enough history exists, and clear _rebalance_due while the buffer is short. Also make min_bars_required() check aligned rows, not just primary bars. Keep the hypothesis rules unchanged (252/22, 50%, monthly). - [warning] Once seeded, a row is appended only when a primary bar is processed. If one leg is missing a session, the barrier skips that timestamp and the row is never added. Every later t-22/t-252 offset then shifts by one session for all legs. The effect is small, but it differs from Moonshot's pandas index, which keeps NaN rows. (line 91) — fix: Acceptable as is. Optionally track session dates and skip-forward explicitly. - [warning] The hypothesis-consistency warning ('22-Day bars') is a false positive: 22 is RANKING_PERIOD_GAP in daily sessions, and 1-DAY bars are correct for UMD. The instruments (10 declared USEQ names), the 252/22 skipped-month momentum (close[t-22]/close[t-252]-1 via deque indices -23/-253), pandas average pct rank with top/bottom 50%, resample('M').last() labelling at calendar month-end with weights.shift() (activation one session later when month-end is not a session), equal 1/active weights with gross 1.0x, and the absence of any stop or filter all match the source. Rebalancing is anchored to bar-timestamp calendar months, not an uptime counter. Extra legs are protected against look-ahead by an explicit ts equality check. Whole-share sizing truncates toward zero. — fix: None.

Analysis

UpMinusDown

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