Skip to content
All strategies

Strategies

Asset-Class Trend Following (Faber 10-Month SMA) on Five USEQ ETFs: SPY / EFA / IEF / DBC / GLD, Equal-Weight Long-or-Cash, Month-End Decision on DAILY Bars

Outcome: Abandoned

UseqFaberAssetClassTrendSma10

Outcome Summary

UseqFaberAssetClassTrendSma10 set out to replicate the published Faber 10-month-SMA long-or-cash rule on five commission-free USEQ asset-class ETFs, with equal-weight 20% sleeves decided at each month end. The code was designed to keep per-bar work O(1), but across 3 iterations it never passed Layer 3 verification because the sandbox smoke test kept exceeding the 300s limit on a 2400-day daily window. The run was abandoned as a sandbox_timeout verification loop before any backtest, optimization or analyst review took place, so the trend-following hypothesis itself was never tested.

Hypothesis

This is a direct test of the QuantConnect / Quantpedia 'Asset Class Trend Following' mechanism (Faber 2007, 'A Quantitative Approach to Tactical Asset Allocation'). It runs on the commission-free USEQ venue, which has decades of split-adjusted daily history. The universe comes from the source's asset-class design and was not picked with hindsight. It holds one liquid ETF per asset class: US equities (SPY.USEQ), developed ex-US equities (EFA.USEQ), US 7-10y Treasuries (IEF.USEQ), broad commodities (DBC.USEQ) and……Show moreShow less

This is a direct test of the QuantConnect / Quantpedia 'Asset Class Trend Following' mechanism (Faber 2007, 'A Quantitative Approach to Tactical Asset Allocation'). It runs on the commission-free USEQ venue, which has decades of split-adjusted daily history. The universe comes from the source's asset-class design and was not picked with hindsight. It holds one liquid ETF per asset class: US equities (SPY.USEQ), developed ex-US equities (EFA.USEQ), US 7-10y Treasuries (IEF.USEQ), broad commodities (DBC.USEQ) and gold (GLD.USEQ). The source's fifth sleeve is REITs (VNQ), but VNQ is not in our USEQ universe or catalog, so GLD replaces it as a real-asset sleeve whose data is present. That swap is the only deviation from the source. On the last trading session of each calendar month, each ETF's 10-month SMA is the mean of its last 10 month-end closes. It is maintained incrementally: one month-end close is appended per month to a 10-slot deque. Per-bar cost is O(1) with no history rescans, which keeps the run inside the 300s sandbox limit. If the month-end close is above its 10-month SMA, the sleeve is held at 1/5 of equity (20% target weight); otherwise the sleeve sits in cash. There is no leverage (1.0x on the margin account), no shorting, and no other filter or gate: the mechanism runs ungated, exactly as published. Flat state: a sleeve is in cash whenever it is below its SMA. Historically each sleeve spends about 25-40% of months in cash, and the whole book is fully in cash in about 5-10% of months (late 2008, for example). SPY.USEQ is the primary instrument (bar_type SPY.USEQ-1-DAY-LAST-EXTERNAL); the other four come in through extra_instruments / extra_bar_types at 1-DAY. The mechanism with instrument and timeframe removed: each asset follows its own slow monthly moving-average trend, is either long or in cash, and gets an equal budget across uncorrelated asset classes. That is different from the factory's crypto cross-sectional rotation and crypto daily TSMOM runs: different asset class, different venue, a monthly clock, and no ranking. source_ref: QuantConnect Tutorials, 04 Strategy Library / 12 Asset Class Trend Following, section 'Method' (commit 4a341890).

Iteration 3: the iteration-2 submission accidentally shipped a placeholder string instead of source (Layer 1 syntax error). This restores the full iteration-1 source and applies only the intended Layer-2 fix: _pending() no longer unconditionally calls cache.orders_inflight (missing on the synthetic mock cache); it checks orders_open and calls orders_inflight only when the cache class provides it. Signal, month-end logic, sizing (5 x 20% = 100% gross, whole shares) unchanged.

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

Faber (2007) asset-class trend following on SPY/EFA/IEF/DBC/GLD (USEQ, daily bars). On each month's last session each ETF's close is compared with the mean of its last 10 month-end closes; above -> hold that sleeve at 1/5 of equity, below -> cash. Long-only, 1x, equal weight, no other filters.

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).

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['sma_months', 'min_notional', 'sleeve_weight'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: AttributeError: '_MockCache' object has no attribute 'orders_inflight' (bar timestamp: 1735689660000) - steady_downtrend: AttributeError: '_MockCache' object has no attribute 'orders_inflight' (bar timestamp: 1735689660000) - flat_ranging: AttributeError: '_MockCache' object has no attribute 'orders_inflight' (bar timestamp: 1735689660000) - volatility_spike: AttributeError: '_MockCache' object has no attribute 'orders_inflight' (bar timestamp: 1735689660000) - zero_volume: AttributeError: '_MockCache' object has no attribute 'orders_inflight' (bar timestamp: 1735689660000) - price_gap: AttributeError: '_MockCache' object has no attribute 'orders_inflight' (bar timestamp: 1735689660000)

Verification failed (Layer 1 — static analysis): - Syntax error: invalid character '—' (U+2014) (<unknown>, line 1) (line 1)

Outcome Summary

The hypothesis and code both claimed O(1) per-bar work, yet this multi-leg daily strategy still timed out repeatedly, so a future iteration should measure actual per-bar cost across all five legs before resubmitting rather than assume the monthly-clock design is cheap.

The run was abandoned in a verification loop after 3 iterations because the Layer 3 sandbox backtest went over the 300s wall-clock limit on a ~20000-bar window (2400 days of SPY.USEQ-1-DAY-LAST-EXTERNAL). The backtest review, optimization, analyst and risk stages were never reached.

A direct test of Faber's (2007) asset-class trend following on five USEQ ETFs (SPY, EFA, IEF, DBC, GLD, with GLD replacing the unavailable VNQ): each sleeve is held at 1/5 of equity when its month-end close is above its 10-month SMA and sits in cash otherwise, with no leverage or shorting.

There are no performance figures: the backtest report is empty and no optimization report was produced, so no return, Sharpe or trade-count numbers exist.

Analysis

Asset Class Trend Following

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