Comparing Strategies Across Symbols¶
Why Scan Multiple Symbols¶
Testing a strategy on a single symbol provides limited information. The strategy might work well on that particular security due to luck or unique characteristics, but fail on others.
Scanning multiple symbols helps you: - Identify robust strategies: Strategies that work across many symbols are more likely to continue working - Avoid overfitting: A strategy tuned to one symbol may not generalize - Find best opportunities: Compare which symbols work best with your strategy - Understand limitations: See where and why a strategy fails - Build confidence: Consistent performance across symbols is more convincing than one good result
The Scanner Module¶
TzuTrader's Scanner module automates testing a strategy across multiple symbols:
import tzutrader
# Create a scanner with your strategy
let strategy = newRSIStrategy(period = 14, oversold = 30, overbought = 70)
let scanner = newScanner(strategy, @["AAPL", "MSFT", "GOOG", "TSLA"])
# Scan all symbols (assumes CSV files in data/ directory)
let results = scanner.scanFromCSV("data/", initialCash = 100000.0, commission = 0.001)
# View summary
echo scanner.summary(results)
The scanner runs the same strategy on each symbol and collects all backtest reports.
Setting Up a Scanner¶
Basic Setup¶
Create a scanner with a strategy and symbol list:
import tzutrader
let strategy = newMACDStrategy(fastPeriod = 12, slowPeriod = 26, signalPeriod = 9)
let symbols = @["AAPL", "MSFT", "GOOG", "AMZN", "TSLA"]
let scanner = newScanner(strategy, symbols)
Manual Scanning¶
Provide data for each symbol manually:
import std/tables
# Load data for each symbol
var dataDict = initTable[string, seq[OHLCV]]()
dataDict["AAPL"] = readCSV("data/AAPL.csv")
dataDict["MSFT"] = readCSV("data/MSFT.csv")
dataDict["GOOG"] = readCSV("data/GOOG.csv")
# Scan with the data
let results = scanner.scan(dataDict, initialCash = 100000.0, commission = 0.001)
CSV Directory Scanning¶
Automatically load CSV files from a directory:
# Expects files: AAPL.csv, MSFT.csv, GOOG.csv, etc.
let results = scanner.scanFromCSV(
"data/",
initialCash = 100000.0,
commission = 0.001
)
The scanner looks for files named {symbol}.csv in the directory.
Ranking Results¶
After scanning, rank results by various metrics:
Rank by Total Return¶
Find symbols with highest returns:
let ranked = scanner.rankBy(results, RankBy.TotalReturn)
for i in 0..<min(5, ranked.len):
let result = ranked[i]
echo result.symbol, ": ", result.report.totalReturn, "% return"
Rank by Sharpe Ratio¶
Find best risk-adjusted returns:
let ranked = scanner.rankBy(results, RankBy.SharpeRatio)
echo "Top 3 by Sharpe Ratio:"
for i in 0..2:
if i < ranked.len:
let result = ranked[i]
echo result.symbol, ": Sharpe = ", result.report.sharpeRatio
Available Ranking Metrics¶
# All ranking options
RankBy.TotalReturn # Highest total return
RankBy.AnnualizedReturn # Highest annualized return
RankBy.SharpeRatio # Best risk-adjusted returns
RankBy.WinRate # Highest win rate
RankBy.ProfitFactor # Best profit factor
RankBy.MaxDrawdown # Smallest drawdown (ascending)
RankBy.TotalTrades # Most trading activity
Note: MaxDrawdown ranks ascending (smaller drawdown is better), all others rank descending (higher is better).
Filtering Results¶
Filter results to find quality opportunities:
Filter by Minimum Return¶
Only show symbols that meet return thresholds:
let filtered = scanner.filter(results, minReturn = 10.0)
echo "Symbols with >10% return: ", filtered.len
Filter by Multiple Criteria¶
Combine filters for strict requirements:
let filtered = scanner.filter(
results,
minReturn = 8.0, # At least 8% return
minSharpe = 1.0, # Sharpe ratio above 1.0
minWinRate = 50.0, # Win rate above 50%
minTrades = 10, # At least 10 trades
maxDrawdown = -20.0 # Max drawdown less than -20%
)
echo "Symbols meeting all criteria: ", filtered.len
Filter parameters: - minReturn: Minimum total return percentage - minSharpe: Minimum Sharpe ratio - minWinRate: Minimum win rate percentage - minTrades: Minimum number of trades - maxDrawdown: Maximum drawdown (as negative percentage)
All criteria must be met (AND logic).
Getting Top N Results¶
Get the best N symbols after ranking:
# Get top 5 by Sharpe ratio
let top5 = scanner.topN(results, n = 5, rankBy = RankBy.SharpeRatio)
for result in top5:
echo result.symbol, ": Sharpe = ", result.report.sharpeRatio
Viewing Scan Results¶
Summary Table¶
Display a formatted comparison table:
Output example:
=== Scanner Results ===
Strategy: RSI Strategy
Symbols scanned: 10
Date range: 2023-01-01 to 2023-12-31
Symbol Return% Sharpe MaxDD% Trades WinRate% ProfitFactor
------ ------- ------ ------ ------ -------- ------------
AAPL 12.5 1.23 -8.2% 24 58.3 1.45
MSFT 15.2 1.45 -6.5% 28 64.3 1.67
GOOG 8.7 0.89 -11.3% 20 55.0 1.32
AMZN 6.2 0.72 -15.4% 22 50.0 1.18
TSLA -2.1 0.15 -22.1% 31 41.9 0.89
Accessing Individual Results¶
Process each result programmatically:
for result in results:
echo "Symbol: ", result.symbol
echo "Return: ", result.report.totalReturn, "%"
echo "Sharpe: ", result.report.sharpeRatio
echo "---"
# Access full report
if result.report.totalReturn > 10.0:
echo result.report.summary()
Each ScanResult contains: - symbol: The symbol tested - report: Full BacktestReport with all metrics - data: Historical data used
Interpreting Scan Results¶
Look for Consistency¶
Good strategies show consistent performance across symbols:
# Calculate statistics across symbols
var returns: seq[float64] = @[]
for result in results:
returns.add(result.report.totalReturn)
let avgReturn = returns.sum() / returns.len.float64
let stdDev = calculateStdDev(returns)
echo "Average return: ", avgReturn, "%"
echo "Std deviation: ", stdDev, "%"
Green flags: - Most symbols are profitable - Returns are relatively consistent (low std dev) - Sharpe ratios are positive across symbols - Similar number of trades per symbol
Red flags: - Only one or two symbols are profitable - Huge variance in returns - Some symbols have excessive trades, others have very few - Strategy works on tech stocks but fails on everything else
Consider Market Conditions¶
All symbols tested should cover similar time periods:
for result in results:
echo result.symbol, ": ",
result.data.len, " bars from ",
fromUnix(result.data[0].timestamp), " to ",
fromUnix(result.data[^1].timestamp)
If time periods vary significantly, results aren't comparable.
Statistical Significance¶
More trades provide more reliable statistics:
for result in results:
if result.report.totalTrades < 30:
echo result.symbol, ": Only ", result.report.totalTrades,
" trades (insufficient sample)"
With fewer than 20-30 trades, results may be due to luck rather than strategy effectiveness.
Example: Complete Scan Workflow¶
Here's a complete example of scanning and analyzing results:
import tzutrader
# Create strategy
let strategy = newRSIStrategy(period = 14, oversold = 30, overbought = 70)
# Create scanner with symbols
let symbols = @["AAPL", "MSFT", "GOOG", "AMZN", "TSLA",
"NVDA", "META", "NFLX", "AMD", "INTC"]
let scanner = newScanner(strategy, symbols)
# Run scan
echo "Scanning ", symbols.len, " symbols..."
let results = scanner.scanFromCSV("data/", initialCash = 100000.0, commission = 0.001)
# Show summary
echo "\n", scanner.summary(results)
# Filter for quality results
let filtered = scanner.filter(
results,
minReturn = 5.0,
minSharpe = 0.8,
minWinRate = 50.0,
minTrades = 15
)
echo "\nSymbols meeting quality criteria: ", filtered.len
# Rank by Sharpe ratio
let ranked = scanner.rankBy(filtered, RankBy.SharpeRatio)
# Show top 3
echo "\nTop 3 by risk-adjusted returns:"
for i in 0..<min(3, ranked.len):
let result = ranked[i]
echo (i + 1), ". ", result.symbol
echo " Return: ", result.report.totalReturn, "%"
echo " Sharpe: ", result.report.sharpeRatio
echo " Max DD: ", result.report.maxDrawdown, "%"
echo " Trades: ", result.report.totalTrades
echo " Win Rate: ", result.report.winRate, "%"
Comparing Different Strategies¶
You can scan the same symbols with different strategies:
# Test RSI strategy
let rsiStrategy = newRSIStrategy(period = 14, oversold = 30, overbought = 70)
let rsiScanner = newScanner(rsiStrategy, symbols)
let rsiResults = rsiScanner.scanFromCSV("data/")
# Test MACD strategy
let macdStrategy = newMACDStrategy()
let macdScanner = newScanner(macdStrategy, symbols)
let macdResults = macdScanner.scanFromCSV("data/")
# Compare average returns
let rsiAvg = rsiResults.mapIt(it.report.totalReturn).sum() / rsiResults.len.float64
let macdAvg = macdResults.mapIt(it.report.totalReturn).sum() / macdResults.len.float64
echo "RSI average return: ", rsiAvg, "%"
echo "MACD average return: ", macdAvg, "%"
This helps identify which strategy approach works better for your symbol universe.
Using the CLI for Scanning¶
The CLI tool provides scanning without writing code:
# Scan multiple symbols
./tzutrader_cli scan data/ AAPL,MSFT,GOOG,AMZN,TSLA \
--strategy=rsi \
--initial-cash=100000 \
--commission=0.001
# Rank by Sharpe ratio
./tzutrader_cli scan data/ AAPL,MSFT,GOOG,AMZN,TSLA \
--strategy=macd \
--rank-by=sharpe
# Filter results
./tzutrader_cli scan data/ AAPL,MSFT,GOOG,AMZN,TSLA \
--strategy=rsi \
--min-return=5.0 \
--min-sharpe=0.8 \
--min-trades=15
# Get top N
./tzutrader_cli scan data/ AAPL,MSFT,GOOG,AMZN,TSLA \
--strategy=rsi \
--rank-by=sharpe \
--top=5
# Export results
./tzutrader_cli scan data/ AAPL,MSFT,GOOG,AMZN,TSLA \
--strategy=rsi \
--export=scan_results.csv
See Chapter 8 (Advanced Workflows) for more CLI usage patterns.
Export Scan Results¶
Export results for further analysis:
import tzutrader/exports
# Export to JSON
exportJson(results, "scan_results.json")
# Export to CSV
exportCsv(results, "scan_results.csv")
The CSV includes columns for symbol, return, Sharpe ratio, max drawdown, trades, win rate, and profit factor.
Avoiding Common Mistakes¶
Mistake 1: Cherry-Picking¶
Don't choose only the best-performing symbols for deployment. Those may have been lucky. Instead: - Look at average performance across all symbols - Consider what percentage of symbols are profitable - Use out-of-sample testing on the top performers
Mistake 2: Ignoring Sectors¶
If all tested symbols are from one sector (e.g., tech), the strategy may only work in that sector:
# Better: diverse sectors
let symbols = @[
"AAPL", # Tech
"JPM", # Finance
"XOM", # Energy
"JNJ", # Healthcare
"WMT" # Retail
]
Mistake 3: Different Time Periods¶
Ensure all symbols cover the same date range. Comparing 2023 performance to 2020 performance isn't meaningful due to different market conditions.
Mistake 4: Too Few Symbols¶
Testing 2-3 symbols doesn't provide enough evidence. Aim for at least 10-20 symbols across different sectors.
Next Steps¶
The next chapter covers advanced workflows including parameter optimization, walk-forward testing, and batch processing with the CLI tool.
Key Takeaways¶
- Test strategies across multiple symbols to identify robust approaches
- Use the Scanner module to automate multi-symbol backtesting
- Rank results by return, Sharpe ratio, or other metrics
- Filter results to find quality opportunities meeting multiple criteria
- Look for consistency across symbols, not just a few winners
- Ensure adequate sample size (30+ trades) for statistical significance
- Test diverse sectors, not just one industry
- Use the CLI tool for quick scanning without writing code
- Export results for analysis in spreadsheets or other tools
- Avoid cherry-picking winners - focus on average performance