Skip to content
All strategies

Strategies

pinkfish Follow-Trend (Farrell Aultman), faithful port: buy when the daily close is above its 200-day SMA while the S&P 500 (^GSPC) closes above its own 200-day SMA; sell on a close below the SMA or when the S&P regime turns negative. Applied separately to 8 US index/asset ETFs, equal 1/8 sleeves, next-open fills (USEQ 1-DAY, long-only)

Outcome: Paper Trading

PinkfishFollowTrendEtfSleeves

Outcome Summary

PinkfishFollowTrendEtfSleeves ported the open-source pinkfish Follow-Trend rule to 8 ETF sleeves on USEQ, keeping the source's parameters and changing only execution, sizing and venue. Across 6 iterations it made +184.4% with Sharpe 0.63 and a 12.6% max drawdown, stayed out of the market in 2008, and earned 1.17% per trade, far above the cost floor. Its out-of-sample evidence was positive but just short of significance (pooled t 1.94, holdout t 1.60), its Calmar ratio was below the floor and it had a drawdown that lasted about 969 days. The analyst recommended promotion and it is now in paper trading with a low-confidence label.

Hypothesis

Faithful port of the open-source pinkfish example 'follow-trend' by Farrell Aultman (MIT, github.com/fja05680/pinkfish, examples/strategies/follow-trend/strategy.py). Credit goes to the author. Any performance the source claims is unverified; our backtest with real costs decides. SOURCE RULES (kept exactly): per security, on each daily bar: sma = SMA(close, sma_period=200); upper_band = sma*(1+percent_band/100); lower_band = sma*(1-percent_band/100), with percent_band=0 so both bands equal the SMA. regime =……Show moreShow less

Faithful port of the open-source pinkfish example 'follow-trend' by Farrell Aultman (MIT, github.com/fja05680/pinkfish, examples/strategies/follow-trend/strategy.py). Credit goes to the author. Any performance the source claims is unverified; our backtest with real costs decides. SOURCE RULES (kept exactly): per security, on each daily bar: sma = SMA(close, sma_period=200); upper_band = sma*(1+percent_band/100); lower_band = sma*(1-percent_band/100), with percent_band=0 so both bands equal the SMA. regime = pinkfish CROSSOVER(^GSPC, fast=1, slow=200): positive when the S&P 500 index close is above its 200-day SMA, negative when it is below. SELL first: if long and ((use_regime_filter and regime<0) or close<lower_band or last bar of data), sell the whole position. Otherwise, if flat: BUY if (regime>0 or not use_regime_filter) and close>upper_band. The source runs one symbol per Strategy instance, each instance putting all of its own capital in that symbol ('applied separately to several securities'); balances are recorded after each bar. Bars with NaN indicators (the first 200 days) are dropped, so no trading happens before both SMAs exist. PLATFORM ADAPTATIONS (only venue, instrument, sizing, fees, execution): (1) Execution: pinkfish fills at the signal bar's close. Our session venue cannot fill at the deciding bar's close (registry: same_close_fill unsupported); every decision made on a daily close fills as a market order at the NEXT session's OPEN (next_open_fill, supported). This is the documented substitution and is the only change to the timing. The edge claimed is a multi-month trend, not an overnight return, so the test is still meaningful. (2) Universe: 'several securities' is a free choice in the source. We use 8 long-history, survivorship-free ETFs, each run as an independent sleeve: SPY, QQQ, DIA, IWM, EFA, EEM, TLT, GLD (all .USEQ). (3) Regime source: the source fetches ^GSPC; we read ^GSPC.USEQ 1-DAY bars as a signal-only extra instrument (never traded). Regime is evaluated on the same session date as the traded bar; if the ^GSPC bar for that date is missing, keep the previous regime value. (4) Sizing: each sleeve stands for the source's separate account: at entry, buy whole shares worth 1/8 (12.5%) of current account equity at the decision close price; no leverage (gross at most 100%), no pyramiding, one position per symbol. (5) Prices: our USEQ bars are split-adjusted and dividends are not in PnL; the source default use_adj=False uses unadjusted prices. Split-adjustment is a needed change (unadjusted splits would fake crossings); the missing dividends are a known understatement for TLT/SPY and the like. (6) Costs: USEQ zero commission plus modeled spread/impact (~0.02-0.05% RT). (7) The 'end of data' sell becomes: close every open position on the final bar of the backtest window (the platform's window-end flattening is equivalent). Risk control is route (b): signal exits (close below SMA, S&P regime turns negative) plus capped sizing (12.5% per position, at most 100% gross); the source has no stop and none is added.

Implementation

Port of pinkfish follow-trend (Farrell Aultman, MIT) to 8 independent USEQ daily ETF sleeves: SPY, QQQ, DIA, IWM, EFA, EEM, TLT, GLD. Each sleeve checks the sell rule first: if long, sell when the ^GSPC close is below its 200-day SMA, or when the close is below SMA(200)*(1-band). If flat, buy when the ^GSPC regime is positive and the close is above SMA(200)*(1+band). Each entry buys whole shares worth 1/8 of account equity. A decision made on a daily close fills at the next session's open.

Iteration 6 QA blocked the previous MSFT port because the source fills at the signal bar's close. The current hypothesis now explicitly accepts next-session-open execution (adaptation 1): next_open_fill is supported and same_close_fill is unsupported. It also specifies 8 ETF sleeves with 1/8 of equity each, so the config changes from MSFT to SPY as the primary (also traded), plus QQQ, DIA, IWM, EFA, EEM, TLT and GLD, with ^GSPC as a signal-only leg, matching the hypothesis instruments. The source rules are kept:……Show moreShow less

Iteration 6 QA blocked the previous MSFT port because the source fills at the signal bar's close. The current hypothesis now explicitly accepts next-session-open execution (adaptation 1): next_open_fill is supported and same_close_fill is unsupported. It also specifies 8 ETF sleeves with 1/8 of equity each, so the config changes from MSFT to SPY as the primary (also traded), plus QQQ, DIA, IWM, EFA, EEM, TLT and GLD, with ^GSPC as a signal-only leg, matching the hypothesis instruments. The source rules are kept: per-security SMA(sma_period) bands with percent_band, and the ^GSPC close vs its 200-day SMA as the CROSSOVER(fast=1, slow=200) regime sign. Sells are checked before buys, and no stop is added. Risk route (b) applies: signal exits plus 12.5% per position, at most 100% gross at 1x. All sleeves are managed inside calculate_signal with _submit_entry_instrument and _submit_exit_position. should_enter returns None and should_exit returns False, so the base never sends a competing primary order. Indicators are incremental running sums per instrument. The base alignment barrier processes a session only when every leg, ^GSPC included, has a bar at that close, so no bar from a later session is used. If ^GSPC has no bar for a session, the barrier skips that session instead of carrying the previous regime forward; with complete index data this does not happen. The end-of-data sell is the platform's window-end flattening.

Verification Results

Acceptable as is. If ^GSPC gaps show up in the audit, exclude ^GSPC from the sync legs and carry the regime forward explicitly.

Hypothesis adaptation (3) says: if the ^GSPC bar for a session is missing, keep the previous regime. Instead, the base alignment barrier skips the whole session for every sleeve until all 9 legs, ^GSPC included, have a bar at that close. The _prev_regime fallback in _run_sleeves is effectively dead code. With complete Yahoo ^GSPC daily data the two behave the same. The developer disclosed this in the rationale.

Optionally size from min(equity*sleeve_fraction, free margin), or use a small buffer (e.g. 0.995). Not a correctness blocker.

Each entry is sized at 12.5% of equity at the decision close and fills at the next open. When all 8 sleeves are long, a gap-up open, or positions marked up since entry, can push gross slightly above 100% at leverage 1.0. A late sleeve's entry order could then be shrunk or refused by the margin check.

None required.

Without point_in_time_universe, the backtest window starts at the latest-listed leg (GLD, Nov 2004), plus the 200-bar warmup. This is fine for these survivorship-free ETFs. It just shortens history compared with SPY/^GSPC alone.

Long-only SMA200 trend filter with an S&P regime gate on 8 ETFs is a well-known, low-turnover rule. Expect modest Sharpe, and expect TLT/GLD returns to be understated because dividends are not in PnL. Whipsaw around the SMA when percent_band=0 can produce many small round trips; on USEQ costs are tiny, so this is mainly a Sharpe/drawdown question for the analyst.

Backtest Review

Sharpe
0.63
Total return
184.42%
Max drawdown
12.64%
Trades
742
Win rate
33.2%
Profit factor
2.08

The trades match the hypothesis. All 608 closed trades are long. All 8 sleeves trade, between 56 and 95 round trips each. Entries fill at the next session open (14:30 UTC) at about 12% of equity (leverage 0.12). The 2008 to mid-2009 bear market has zero exposure because the S&P 200-day regime filter worked as written.

Economics are well above the USEQ cost floor. Average trade return is 1.09% of notional against the 0.05% floor (0.10% preferred). Profit factor is 1.91. The payoff ratio is about 3.8:1 (average win $1,137 vs average loss $300), so the 34% win rate is what a trend follower should show.

Every sleeve is net positive: QQQ +$32.0k, SPY +$19.5k, DIA +$17.4k, IWM +$14.2k, GLD +$13.6k, TLT +$5.9k, EEM +$4.8k, EFA +$3.2k. The profit is not concentrated in one sleeve.

Development period (2006-02 to 2022-05): +110.7%, CAGR 4.7%, Sharpe 0.59 (probabilistic Sharpe 0.98), max drawdown 12.6%. That drawdown is well inside the pre-registered 25% limit and far below the 2008 drawdown of an equal-weight basket of these ETFs. The low return is not just beta (lesson 170).

608 trades give the walk-forward enough samples. Only two parameters are tunable, both economically meaningful: sma_period (100-300) and percent_band (0-3%, which controls whipsaw).

The risk-adjusted edge is modest: Sharpe 0.59, Calmar 0.37, longest drawdown 871 days, tail ratio 0.93. A low-turnover SMA trend filter is expected to look like this.

Several flat or negative years (2011, 2012, 2015, 2018, 2022 to the cut). The whipsaw QA expected at percent_band=0 shows up as 17 consecutive losses. On USEQ costs these losses come from price moves, not fees.

Dividends are not in PnL, which understates TLT, SPY and the other income-paying sleeves. This is a known adaptation, not a defect.

QA's note is partly confirmed. Sharpe is modest and whipsaw losses are visible. However, per-trade economics (1.09% vs a 0.05% floor, PF 1.91) and drawdown are healthy, so the concern does not justify skipping optimization.

Analysis

Faithful port of pinkfish Follow-Trend, all parameters fixed at the source's values; one evaluation, no parameter search (effective trials = 1).

Average trade return 1.17% of notional against the 0.05% USEQ floor; profit factor 2.08 over 742 trades; winners 4.2 times the size of losers; impact 2.0% of gross, zero commissions.

Out-of-sample results positive and close to significant: 31 windows pooled give Sharpe 0.54 (t 1.94, hurdle 2.0); holdout (134 trades, 1,600 days) Sharpe 0.78 (t 1.60), drawdown 8.1%, consistent with the walk-forward result (z +0.31).

Sensitivity is a broad plateau with no cliffs: every tested point is within one standard error of the base.

Max drawdown 12.6% against the pre-registered 25%; out of the market for all of 2008; 15 of 20 years positive; largest year carries 14.9% of profit; beta 0.33.

Risk-control check passes on the ledger: worst loss 1.33% of equity, largest position 12.8% of equity.

All 6 predictions recorded before coding were met.

Not statistically significant (t 1.94 pooled out-of-sample, 1.60 holdout): low confidence, paper only.

Calmar 0.41 is below the 0.5 floor; the longest drawdown took about 969 days to recover (2011-2016).

Information ratio is -0.27 against the equal-weight ETF basket: the value is a smaller drawdown, not extra return.

Walk-forward windows vary widely (-3.4 to +3.0) because the rule whipsaws in sideways markets.

65 capped-margin entry shrinks (single-leg long entries, not broken hedges).

The 1-bar IC of -0.009 does not measure a multi-month trend rule.

Backtest gate: - backtest produced ZERO trades — the entry condition never fired on real data

LOW CONFIDENCE, NOT STATISTICALLY PROVEN: paper observation only, never live money on this evidence. Trade the fixed source parameters at reduced paper allocation. Kill criteria: after at least 12 months and at least 60 paper trades, retire if average trade return is below 0.30%, profit factor is below 1.2, drawdown exceeds 15%, or Sharpe is negative over a full 12 months while the ETF basket is positive.

Risk Review

Defined risk control holds on route (b), signal exit: positions close on a close below the 200-day SMA or when the S&P 500 regime turns negative. Sizing is capped at 1/8 of equity per sleeve, long-only, 1x. Measured on the ledger: the largest position was 12.82% of equity (EEM, 2020-07). The worst realised loss was 1.33% of equity (EEM entered 2009-06, -10.7% on notional). The worst adverse excursion was 1.32% of equity. All are far inside the conservative profile: realized vol 8.4% (band 5-60%), peak position vol 8.1% (cap 120%), trade loss under the 15% cap. The lack of a stop is faithful to the source and is not an issue.

Gross exposure runs at the 1x margin ceiling. Peak margin utilization is about 1.0015 against the 1.02 limit, and the backtest had 65 capped-margin entry shrinks. The cause is that each sleeve is sized at 12.5% of equity at the decision close but fills at the next open, while the other sleeves have marked up. The effect is under-sized late sleeves, not leverage, but paper fills will show the same refusals and shrinks.

Drawdown profile is gradual but slow to recover. Max drawdown is 12.6% at 1x, against the 25% the hypothesis pre-registered. The longest drawdown lasted 871 days. Annual returns were negative in 2011, 2012, 2015, 2018 and 2022, with whipsaw around the SMA in sideways markets. The bootstrap drawdown CI reaches 35%, above the pre-registered 25%, so a deeper drawdown than history is plausible. The COVID stress window lost 5.9% (drawdown 6.1%), which the regime exit kept contained. The strategy was flat for all of 2008.

Correlation with the deployed book: the book already holds US-equity trend and calendar exposure: PinkfishCalendarMonthlySmaSpy (an SPY SMA timer, very likely highly correlated with the SPY/DIA/QQQ/IWM sleeves), SpyTurnOfMonthFlowWindow, HalloweenInternationalEtfBasket80, EmCountryEtfTurnOfMonthBasket and NfciImpulseSectorSpreadUseq. The S&P 500 regime filter puts all eight sleeves (including TLT and GLD) to cash at the same moment, so the diversification is smaller than eight sleeves suggest. No book member is in breach and there are no open execution items. However, PinkfishCalendarMonthlySmaSpy, a USEQ daily SMA strategy, shows a 'diverged' decision replay, which is a fidelity warning for this venue path.

Market impact is negligible: liquid index ETFs, 12.5% sleeves, daily bars, about 600 trades over 16 years. Modeled impact is about 2% of gross PnL, with zero commission. Trade frequency is low and fully fee-viable: avg trade return 1.09% of notional in the development period, against the 0.05% USEQ floor.

The evidence is low confidence. The final parameters are the source's fixed values (no search, one trial). The pooled out-of-sample t-stat is 1.94, just under the 2.0 hurdle. The holdout result is not enough to call it significant. This supports labelled paper observation only, not real-money evidence. Deflated Sharpe was not evaluated as a floor (missing from the backtest metrics), so treat it as unknown, not passed.

Paper only, under the persistent low-confidence label. This run is not real-money evidence.

Allocate as part of the US-equity trend/calendar cluster (with PinkfishCalendarMonthlySmaSpy and the turn-of-month ETF strategies), not as an independent allocation.

USEQ paper must run on the supervised SF_PAPER_USEQ path. Before any live review, resolve the 'diverged' replay verdict seen on the sibling USEQ SMA strategy and confirm this strategy's replay matches.

Iteration History

Verification failed (Layer 1.5 — hypothesis/config consistency) [class=hypothesis_mismatch]: - optimization_plan fixes leverage at 1.0

Verification failed (Layer 4 — QA review) [class=code_defect]: - [critical] The incremental security SMA becomes incorrect after a missing index session. The base alignment barrier buffers MSFT bars but skips calculate_signal when the matching index bar is absent. On the next aligned session, this code adds only the current close and subtracts only one historical close, although the 200-session window advanced by multiple bars. The resulting sum no longer represents the current 200 MSFT closes and can generate……Show moreShow less

Verification failed (Layer 4 — QA review) [class=code_defect]: - [critical] The incremental security SMA becomes incorrect after a missing index session. The base alignment barrier buffers MSFT bars but skips calculate_signal when the matching index bar is absent. On the next aligned session, this code adds only the current close and subtracts only one historical close, although the 200-session window advanced by multiple bars. The resulting sum no longer represents the current 200 MSFT closes and can generate incorrect entries and exits. The hypothesis explicitly requires handling missing aligned rows. (line 94) — fix: Maintain the security indicator independently for every primary bar, or rebuild the sum from self._bars[-self.sma_period:] whenever the number of newly buffered primary bars exceeds one. Compute the security SMA before excluding sessions with missing regime values. - [warning] The pinned CROSSOVER, TradeLog and select_tradeperiod semantics remain unverified. Fetch attempts did not retrieve the pinned implementations, so the sign-based regime, full-capital sizing and trade-period boundaries cannot be certified as an exact source translation. (line 71) — fix: Retrieve the pinned helpers and compare regime values, equality behavior, sizing and trading boundaries against a small source-generated reference ledger before claiming exact parity. - [warning] Requiring 201 primary bars delays the first eligible decision even though the current-inclusive security SMA and index SMA require only 200 observations. (line 49) — fix: Allow the initial calculation at 200 bars; require the additional historical element only for subsequent incremental updates.

Maintain the security indicator independently for every primary bar, or rebuild the sum from self._bars[-self.sma_period:] whenever the number of newly buffered primary bars exceeds one. Compute the security SMA before excluding sessions with missing regime values.

The incremental security SMA becomes incorrect after a missing index session. The base alignment barrier buffers MSFT bars but skips calculate_signal when the matching index bar is absent. On the next aligned session, this code adds only the current close and subtracts only one historical close, although the 200-session window advanced by multiple bars. The resulting sum no longer represents the current 200 MSFT closes and can generate incorrect entries and exits. The hypothesis explicitly requires handling missing aligned rows.

Retrieve the pinned helpers and compare regime values, equality behavior, sizing and trading boundaries against a small source-generated reference ledger before claiming exact parity.

The pinned CROSSOVER, TradeLog and select_tradeperiod semantics remain unverified. Fetch attempts did not retrieve the pinned implementations, so the sign-based regime, full-capital sizing and trade-period boundaries cannot be certified as an exact source translation.

Allow the initial calculation at 200 bars; require the additional historical element only for subsequent incremental updates.

Requiring 201 primary bars delays the first eligible decision even though the current-inclusive security SMA and index SMA require only 200 observations.

Verify the pinned helpers and compare a reference decision and fill ledger before describing this as a faithful line-for-line port.

The pinned CROSSOVER, TradeLog and select_tradeperiod semantics remain unverified. The disclosed interpretation is coherent, but exact source parity, including equality behavior, sizing and trade-period boundaries, is not established.

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [critical] The hypothesis explicitly requires entries and exits filled at the daily close that produced the decision. This bar-driven FactoryStrategy on USEQ instead fills at the next session's open. The supplied execution capabilities mark same-close execution unsupported, and src/backtesting/next_open.py confirms the next-open behavior. Disclosing this incompatibility does not satisfy the required execution rule; overnight gaps change fills, sizing exposure, and daily balances. (line 17) — fix: Return the execution incompatibility to the owner or Research Lead. Proceed only with an explicitly revised hypothesis accepting next-session-open execution, or platform support for the required same-close fills. Do not present current backtest results as a faithful reproduction.

Return the execution incompatibility to the owner or Research Lead. Proceed only with an explicitly revised hypothesis accepting next-session-open execution, or platform support for the required same-close fills. Do not present current backtest results as a faithful reproduction.

The hypothesis explicitly requires entries and exits filled at the daily close that produced the decision. This bar-driven FactoryStrategy on USEQ instead fills at the next session's open. The supplied execution capabilities mark same-close execution unsupported, and src/backtesting/next_open.py confirms the next-open behavior. Disclosing this incompatibility does not satisfy the required execution rule; overnight gaps change fills, sizing exposure, and daily balances.

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [critical] The hypothesis explicitly requires entries and exits at the current daily close and daily balances after those executions. This bar-driven USEQ configuration instead executes at the next session's open, as confirmed by src/backtesting/next_open.py and its integration in runner.py. The code acknowledges this incompatibility. Next-open fills change overnight exposure, execution prices and post-decision balances; passing smoke tests cannot establish faithful reproduction. (line 6) — fix: Return the execution incompatibility to the owner or Research Lead. Resume only with an explicitly revised hypothesis accepting next-session-open execution, or platform support for the required same-close execution. No strategy-level threshold adjustment repairs this mismatch.

Return the execution incompatibility to the owner or Research Lead. Resume only with an explicitly revised hypothesis accepting next-session-open execution, or platform support for the required same-close execution. No strategy-level threshold adjustment repairs this mismatch.

The hypothesis explicitly requires entries and exits at the current daily close and daily balances after those executions. This bar-driven USEQ configuration instead executes at the next session's open, as confirmed by src/backtesting/next_open.py and its integration in runner.py. The code acknowledges this incompatibility. Next-open fills change overnight exposure, execution prices and post-decision balances; passing smoke tests cannot establish faithful reproduction.

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [critical] The hypothesis explicitly requires entries and exits at the current daily close, with balances recorded after those executions. This bar-driven USEQ strategy inherits market-order execution at the next session's open, as confirmed by src/backtesting/next_open.py. Both should_enter() and should_exit() therefore produce different execution prices, overnight exposure and post-decision balances. The archival header acknowledges the incompatibility but does not resolve it. (line 118) — fix: Keep faithful reproduction blocked. Resume only after the hypothesis explicitly accepts next-session-open execution or the platform supports the required same-close execution. Numeric retuning cannot repair this mismatch.

Keep faithful reproduction blocked. Resume only after the hypothesis explicitly accepts next-session-open execution or the platform supports the required same-close execution. Numeric retuning cannot repair this mismatch.

The hypothesis explicitly requires entries and exits at the current daily close, with balances recorded after those executions. This bar-driven USEQ strategy inherits market-order execution at the next session's open, as confirmed by src/backtesting/next_open.py. Both should_enter() and should_exit() therefore produce different execution prices, overnight exposure and post-decision balances. The archival header acknowledges the incompatibility but does not resolve it.

Outcome Summary

A simple 200-day trend and regime filter mostly reduces drawdown rather than adding return (information ratio -0.27 vs the equal-weight basket, Calmar 0.41 below the 0.5 floor), so judge such ports on risk reduction and expect long whipsaw drawdowns in sideways markets.

The backtest review sent it to optimization and the analyst recommended promotion: the trade economics were strong, sensitivity showed a broad plateau, and every pre-registered prediction was met. Because the out-of-sample results were not statistically significant, it went to paper trading with a low-confidence label.

A faithful port of Farrell Aultman's pinkfish Follow-Trend rule: hold each of 8 US ETFs (SPY, QQQ, DIA, IWM, EFA, EEM, TLT, GLD) in equal 1/8 sleeves, long-only, while its daily close is above its 200-day SMA and the S&P 500 is above its own 200-day SMA, with fills at the next session open.

From 2006-02 to 2026-10 the backtest returned +184.4% (CAGR 5.19%, Sharpe 0.63, max drawdown 12.6%) over 742 trades, with an average trade return of 1.17% of notional and a profit factor of 2.08. Out of sample, 31 pooled walk-forward windows gave Sharpe 0.54 (t 1.94, just under the 2.0 hurdle) and the holdout gave Sharpe 0.78 (t 1.60, drawdown 8.1%).

Analysis

follow-trend

Analysis

strategy.py

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