Skip to content
All strategies

Strategies

SPY Bull-Market Pullback Countertrend (faithful port of pinkfish 'ac-countertrend' by Farrell Aultman) — Long-Only All-In, DAILY bars, SPY.USEQ

Outcome: Abandoned

SpyAcCountertrendPullback

Outcome Summary

SpyAcCountertrendPullback ported the pinkfish 'ac-countertrend' bull-market dip-buying strategy to daily SPY.USEQ, keeping the source's rules, defaults and pullback quirk unchanged. It went through 3 iterations and hit repeated Layer 3 sandbox timeouts (over the 300s limit). The last version already used O(1) per-bar updates, and the developer noted the time appeared to go outside strategy code. The run was abandoned in the verification loop before any backtest metrics were produced, so the hypothesis itself was never tested.

Hypothesis

A line-by-line port of the MIT-licensed pinkfish example strategy 'ac-countertrend' by Farrell Aultman (https://github.com/fja05680/pinkfish/blob/18a0d557ead25e0e82de3ece86bcab6c325a59ed/examples/strategies/ac-countertrend/strategy.py). Credit: the strategy is by Farrell Aultman. The source trades SPY on daily bars. Our platform carries that instrument as SPY.USEQ, with decades of split-adjusted daily history, zero commissions and about 0.02-0.05% round-trip spread plus impact. So no venue or fee adaptation is……Show moreShow less

A line-by-line port of the MIT-licensed pinkfish example strategy 'ac-countertrend' by Farrell Aultman (https://github.com/fja05680/pinkfish/blob/18a0d557ead25e0e82de3ece86bcab6c325a59ed/examples/strategies/ac-countertrend/strategy.py). Credit: the strategy is by Farrell Aultman. The source trades SPY on daily bars. Our platform carries that instrument as SPY.USEQ, with decades of split-adjusted daily history, zero commissions and about 0.02-0.05% round-trip spread plus impact. So no venue or fee adaptation is needed. This is not the fee-regime mismatch that killed the FMZ ports (lesson 167). Mechanism: in an uptrend (40-day EMA above 80-day EMA), buy a dip below the 20-day highest close. Exit on the snap-back above the 20-day SMA, a stop, a regime flip or a 20-bar time limit. Long-only, one position, flat most of the time. All indicators use the daily CLOSE and are computed exactly as in the source: ema_fast = EMA(close, 40); ema_slow = EMA(close, 80) (pinkfish.EMA is the TA-Lib EMA, seeded with the SMA of the first N closes); bull = ema_fast > ema_slow; daily_std = rolling 40-bar sample std (ddof=1, the pandas default) of close.pct_change(); high_close = rolling 20-bar max of close INCLUDING the current bar; pullback = (high_close - close) / daily_std; exit_ma = SMA(close, 20). Warm-up: no trading until every indicator is non-NaN (the source drops NaN rows, about 81 bars). DEFAULT PARAMETERS EXACTLY AS WRITTEN: std_period=40, high_period=20, ema_fast=40, ema_slow=80, hold_period=20, min_pullback=3.0, exit_ma=20, stop_loss_pct=0.15, exit_on_bear=True, margin=1 (no leverage), use_adj=False. SOURCE QUIRK THE DEVELOPER MUST KEEP (do not 'fix' it): pullback divides a DOLLAR distance (high_close - close) by a unitless return std (about 0.01 for SPY), so it is NOT a true z-score. With SPY near $500 and daily_std near 1%, pullback >= 3.0 only needs the close to be about $0.03-0.15 below the 20-day high close. In practice the entry fires on almost any close below the 20-day high while the market is in a bull regime. The port keeps this formula as written. A unit-correct variant (dividing by close*daily_std) would be a SEPARATE hypothesis, not this one. Position sizing: all-in, meaning 100% of account equity (pinkfish margin=1 → leverage 1.0), in whole shares: floor(equity / price). Fees: USEQ has zero commission; our engine models spread and impact. Expected flat fraction is about 55-70% of days, because the strategy exits at the first close above SMA20.

Iteration 3 fixes the second Layer-3 sandbox_timeout. It changes nothing in the rules, indicators or parameters, and the signal output is numerically identical to v2 (max relative diff ~3e-15 over 3,000 test bars). Lesson 155(c) diagnosis: v2 was already O(1) per bar. The window holds only ~1,650 daily bars, which is under 1 s of strategy time even at host load ~50. The wall-clock is therefore most likely spent in catalog/engine work outside the strategy (the same finding as the Eth/Bnb v3 timeout diagnoses). Even so, per-bar work is now cheaper still: the rolling std of returns and the exit SMA use running sums instead of per-bar list copies and re-summing. The rolling 20-close max scans a fixed 20-slot deque, and the EMAs are seeded once and then updated recursively. If this times out again, the cause is sandbox plumbing or host load, not the strategy code.

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

Faithful Stratmill port of pinkfish 'ac-countertrend' (Farrell Aultman, MIT) on SPY.USEQ daily bars. In a bull regime (TA-Lib-style SMA-seeded EMA40 > EMA80), buy all-in (whole shares, leverage 1) when pullback = (20-day highest close incl. current - close) / 40-bar sample std of pct_change >= 3.0 (source's dollar/unitless quirk kept). Exit on bear flip, close > SMA20, close < entry*(1-0.15), or 20 bars held.

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

The developer had already reduced per-bar work to O(1) with running sums and fixed-size deques, and its notes say the time was spent outside strategy code (catalog/engine under host load), so repeated sandbox timeouts on USEQ daily bars should be checked as an infrastructure issue rather than answered with more code rewrites.

It was abandoned in a verification loop after 3 iterations. The deciding failure was the Layer 3 sandbox backtest going over the 300s wall-clock limit on a ~20,000-bar (2,400-day) SPY.USEQ daily window. Backtest review, optimization, analyst review and risk review were never reached.

A faithful port of Farrell Aultman's pinkfish 'ac-countertrend' strategy on daily SPY.USEQ: long-only, all-in, it buys dips below the 20-day highest close while EMA(40) > EMA(80) and exits on a close above SMA(20), a 15% stop, a regime flip or after 20 bars. The source's dollar-over-return-std 'pullback' quirk was kept on purpose.

No performance data exists: the backtest report is empty, there is no optimization report, and no return, Sharpe or trade count was ever produced.

Analysis

ac-countertrend

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