A modernized web app for experimenting with the vanilla Richard Dennis turtle trading strategy.
This version uses server-side historical EOD data lookup, a simplified strategy engine, and a lightweight browser UI for ticker selection and backtesting.
This repository is a research artifact, not a live trading system.
After a series of realism checks, benchmark comparisons, broader universe tests, relative-strength filters, and hybrid SPY-core experiments, the current conclusion is that these Turtle-inspired long-only equity variants are not compelling enough to use as a replacement for passive index investing.
The project found some interesting behavior, especially around relative-strength filters and passive-core/active-sleeve hybrids, but the remaining edge was small, regime-dependent, and likely not large enough to overcome taxes, slippage, operational burden, overfitting risk, and ordinary benchmark alternatives such as SPY and QQQ.
If you found this repo while looking for a trading strategy to run with real money, start with the closing note:
The short version:
Backtests can be useful, but a strategy that only barely beats a simple benchmark in selected tests has not earned live-capital confidence.
- Install dependencies:
npm install- Run the app:
npm start- Open
http://localhost:3000
Requires Node.js 24 or newer.
Yahoo historical chart responses are cached on disk in .cache/market-data for 24 hours by default. You can tune this with:
MARKET_DATA_CACHE=false
MARKET_DATA_CACHE_DIR=.cache/market-data
MARKET_DATA_CACHE_TTL_HOURS=24Run ticker/date-range batches from the command line and write results to CSV:
npm run matrixBy default this runs the built-in 18-symbol by 4-date-range matrix and writes to reports/matrix-results-*.csv. Useful smaller runs:
npm run matrix -- --dry-run
npm run matrix -- --limit=3
npm run matrix -- --symbols=AAPL,SPY --ranges=2020-01-01:2024-12-31Portfolio and benchmark runners can load symbols from a text file with --symbolsFile.
Files may use commas, spaces, or newlines, and # starts a comment:
npm run portfolio -- --symbolsFile=universes/sector-etfs.txt --gapAwareFills=true --maxUnits=1 --slippageBps=5
npm run benchmarks -- --symbolsFile=universes/sector-etfs.txt --benchmarkSymbols=SPY,QQQ,IWM,DIAPortfolio runs can also sweep same-day entry ordering with --entryRank.
Supported values are alphabetical, momentum63, and momentum126:
npm run portfolio -- --symbolsFile=universes/sp500-top100-established.txt --gapAwareFills=true --maxUnits=1 --slippageBps=5 --entryRank=alphabetical,momentum63,momentum126Risk sizing can be swept with comma-separated --riskPercent values:
npm run portfolio -- --symbolsFile=universes/sp500-top100-established.txt --gapAwareFills=true --maxUnits=1 --slippageBps=5 --entryRank=momentum126 --riskPercent=0.25,0.5,1Concurrent position limits can be swept with comma-separated --maxOpenPositions values:
npm run portfolio -- --symbolsFile=universes/sp500-top100-established.txt --gapAwareFills=true --maxUnits=1 --slippageBps=5 --entryRank=momentum126 --riskPercent=0.25 --maxOpenPositions=10,20,30Market regime filters can gate new entries and add-ons with a broad-market moving average.
Use --marketRegimeMa=0,200 to compare the unfiltered strategy with a prior-close SPY 200-day SMA filter:
npm run portfolio -- --symbolsFile=universes/sp500-top100-established.txt --gapAwareFills=true --slippageBps=5 --entryRank=momentum126 --riskPercent=0.25 --entryPeriod=55 --exitPeriod=50 --maxUnits=1,2,4 --marketRegimeSymbol=SPY --marketRegimeMa=0,200Relative-strength filters can require breakout candidates to outperform a benchmark over a prior return lookback.
Use --relativeStrengthLookback=0,126 to compare unfiltered entries with entries that have beaten SPY over the prior 126 trading days:
npm run portfolio -- --symbolsFile=universes/sp500-top100-established.txt --gapAwareFills=true --slippageBps=5 --entryRank=momentum126 --riskPercent=0.25 --entryPeriod=55 --exitPeriod=50 --maxUnits=1,2,4 --relativeStrengthSymbol=SPY --relativeStrengthLookback=0,126Hybrid runs combine a passive buy-and-hold core with an active trend-following sleeve.
Use --activeAllocationPct to sweep the active sleeve size; the remaining allocation goes to --coreSymbol.
Use --rebalance=none,annual,quarterly to compare initial-allocation-only hybrids with periodic target-weight rebalancing:
npm run hybrid -- --symbolsFile=universes/sp500-top100-established.txt --coreSymbol=SPY --activeAllocationPct=20,30 --rebalance=none,annual,quarterly --gapAwareFills=true --slippageBps=5 --entryRank=momentum126 --riskPercent=0.25 --entryPeriod=55 --exitPeriod=50 --maxUnits=1 --maxOpenPositions=10 --relativeStrengthSymbol=SPY --relativeStrengthLookback=63,84