Strategy Backtester
Backtesting: ABCD
Author: Parth_0903
// ═══════════════════════════════════════════════════════════════════════ // EARNINGS MOMENTUM STRATEGY — Wealth-Lab 8 Code Strategy // // STRATEGY LOGIC: // 1. Screens F&O universe for stocks with EPS growth/decline ≥ 20% YoY or QoQ // 2. Auto-assigns CALL direction if EPS UP 20%+, PUT if DOWN 20%+ // 3. Enters directional spread 1-2 days before earnings // 4. Exits day after earnings (or at 50% stop-loss) // 5. Compares implied move vs historical average move for edge detection // 6. Position sizes at 1.5% risk per trade on $300K portfolio // // PASTE THIS INTO: Wealth-Lab > New Strategy > Type: Code // REQUIRED EXTENSIONS: None (uses core WealthScript) // RECOMMENDED DATA: Norgate Data or EODHD (for fundamental data) // ═══════════════════════════════════════════════════════════════════════ using WealthLab.Backtest; using WealthLab.Core; using WealthLab.Indicators; using System; using System.Collections.Generic; namespace WealthScript1 { public class EarningsMomentumStrategy : UserStrategyBase { // ── PARAMETERS ────────────────────────────────────────────── // Adjust these in Wealth-Lab's parameter optimization [Parameter("EPS Growth Threshold (%)", 20, 10, 50, 5)] public int EpsThreshold { get; set; } = 20; [Parameter("Risk Per Trade (%)", 1.5, 0.5, 3.0, 0.5)] public double RiskPerTrade { get; set; } = 1.5; [Parameter("Days Before Earnings to Enter", 2, 1, 5, 1)] public int EntryDaysBefore { get; set; } = 2; [Parameter("IV Rank Max for Long Options", 60, 30, 80, 10)] public int IVRankMax { get; set; } = 60; [Parameter("Stop Loss (%)", 50, 25, 75, 5)] public int StopLossPct { get; set; } = 50; [Parameter("ATR Period", 14, 10, 20, 2)] public int AtrPeriod { get; set; } = 14; [Parameter("RSI Period", 14, 10, 20, 2)] public int RsiPeriod { get; set; } = 14; [Parameter("SMA Long Period", 200, 100, 250, 50)] public int SmaLong { get; set; } = 200; [Parameter("SMA Short Period", 50, 20, 50, 10)] public int SmaShort { get; set; } = 50; // ── INDICATORS ────────────────────────────────────────────── private ATR _atr; private RSI _rsi; private SMA _smaLong; private SMA _smaShort; private SMA _volSma; // Volume SMA for liquidity filter private StdDev _hvol; // Historical volatility proxy // ── STATE TRACKING ────────────────────────────────────────── private Dictionary _earningsDates; private Dictionary _epsGrowth; private Dictionary _direction; // ═══════════════════════════════════════════════════════════ // INITIALIZE — runs once per symbol before the main loop // ═══════════════════════════════════════════════════════════ public override void Initialize(BarHistory bars) { // Set minimum bars needed StartIndex = Math.Max(SmaLong + 10, 220); // Create indicators _atr = ATR.Series(bars, AtrPeriod); _rsi = RSI.Series(bars.Close, RsiPeriod); _smaLong = SMA.Series(bars.Close, SmaLong); _smaShort = SMA.Series(bars.Close, SmaShort); _volSma = SMA.Series(bars.Volume, 50); // Historical volatility (20-day standard deviation of returns, annualized) _hvol = StdDev.Series(bars.Close, 20); // Plot indicators PlotIndicator(_smaLong, WLColor.Red); PlotIndicator(_smaShort, WLColor.Blue); PlotIndicatorLine(_rsi, 70, WLColor.Red, 1, LineStyle.Dashed, "RSI"); PlotIndicatorLine(_rsi, 30, WLColor.Green, 1, LineStyle.Dashed, "RSI"); // Initialize dictionaries _earningsDates = new Dictionary(); _epsGrowth = new Dictionary(); _direction = new Dictionary(); } // ═══════════════════════════════════════════════════════════ // EXECUTE — runs once per bar per symbol // This is where all trading logic lives // ═══════════════════════════════════════════════════════════ public override void Execute(BarHistory bars, int idx) { // ── POSITION MANAGEMENT (check existing positions first) ── Position openPos = FindOpenPosition(0); if (openPos != null) { // EXIT LOGIC double entryPrice = openPos.EntryPrice; double currentPrice = bars.Close[idx]; double pnlPct = (currentPrice - entryPrice) / entryPrice * 100; // Exit condition 1: Stop loss hit if (openPos.PositionType == PositionType.Long && pnlPct < -StopLossPct * 0.01 * entryPrice) { PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0, 0, "Stop Loss"); return; } if (openPos.PositionType == PositionType.Short && pnlPct > StopLossPct * 0.01 * entryPrice) { PlaceTrade(bars, TransactionType.Cover, OrderType.Market, 0, 0, "Stop Loss"); return; } // Exit condition 2: Day after earnings (hold for 1 day post-event) int barsHeld = idx - openPos.EntryBar; if (barsHeld >= EntryDaysBefore + 2) { if (openPos.PositionType == PositionType.Long) PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0, 0, "Post-Earnings Exit"); else PlaceTrade(bars, TransactionType.Cover, OrderType.Market, 0, 0, "Post-Earnings Exit"); return; } // Exit condition 3: Trailing stop at 2x ATR double trailingStop = openPos.PositionType == PositionType.Long ? bars.Close[idx] - 2.0 * _atr[idx] : bars.Close[idx] + 2.0 * _atr[idx]; if (openPos.PositionType == PositionType.Long && bars.Close[idx] < trailingStop) PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0, 0, "Trail Stop"); else if (openPos.PositionType == PositionType.Short && bars.Close[idx] > trailingStop) PlaceTrade(bars, TransactionType.Cover, OrderType.Market, 0, 0, "Trail Stop"); return; // Don't enter new trades while position is open } // ── ENTRY LOGIC ───────────────────────────────────────── // FILTER 1: Liquidity — must have adequate volume if (bars.Volume[idx] < _volSma[idx] * 0.5) return; // Skip illiquid bars // FILTER 2: Price minimum — avoid penny stocks if (bars.Close[idx] < 20) return; // FILTER 3: Calculate EPS growth signal // In Wealth-Lab, you'd use fundamental data provider here // For now, we use price momentum as a proxy for earnings momentum // Replace this section with actual EPS data if you have Norgate/EODHD double priceGrowthQoQ = 0; double priceGrowthYoY = 0; if (idx >= 63) // ~1 quarter of trading days priceGrowthQoQ = (bars.Close[idx] - bars.Close[idx - 63]) / bars.Close[idx - 63] * 100; if (idx >= 252) // ~1 year of trading days priceGrowthYoY = (bars.Close[idx] - bars.Close[idx - 252]) / bars.Close[idx - 252] * 100; // ────────────────────────────────────────────────────────── // EARNINGS MOMENTUM SIGNAL: // If price momentum exceeds threshold → proxy for EPS beat expectation // REPLACE WITH ACTUAL EPS DATA for production use // ────────────────────────────────────────────────────────── bool bullSignal = priceGrowthQoQ >= EpsThreshold || priceGrowthYoY >= EpsThreshold; bool bearSignal = priceGrowthQoQ <= -EpsThreshold || priceGrowthYoY <= -EpsThreshold; if (!bullSignal && !bearSignal) return; // No signal // FILTER 4: Volatility filter (IV rank proxy using HV percentile) // Higher HV relative to recent history = higher IV rank double currentHV = _hvol[idx]; double hvAvg = 0; int hvCount = 0; for (int i = Math.Max(0, idx - 252); i < idx; i++) { hvAvg += _hvol[i]; hvCount++; } hvAvg = hvCount > 0 ? hvAvg / hvCount : currentHV; double ivRankProxy = (currentHV / hvAvg - 0.5) * 200; // Normalize to 0-100ish ivRankProxy = Math.Max(0, Math.Min(100, ivRankProxy)); // For BUYING options: prefer low IV rank (options are cheaper) if (ivRankProxy > IVRankMax) return; // Options too expensive // FILTER 5: Trend confirmation bool uptrend = bars.Close[idx] > _smaLong[idx] && _smaShort[idx] > _smaLong[idx]; bool downtrend = bars.Close[idx] < _smaLong[idx] && _smaShort[idx] < _smaLong[idx]; // Bull signal needs uptrend OR strong momentum override if (bullSignal && !uptrend && priceGrowthQoQ < EpsThreshold * 2) return; // Bear signal needs downtrend OR strong momentum override if (bearSignal && !downtrend && priceGrowthQoQ > -EpsThreshold * 2) return; // FILTER 6: RSI confirmation if (bullSignal && _rsi[idx] > 80) return; // Overbought, skip if (bearSignal && _rsi[idx] < 20) return; // Oversold, skip // ── POSITION SIZING ───────────────────────────────────── // Risk 1.5% of portfolio per trade // Using ATR-based sizing for stock proxy double riskAmount = Backtester.CurrentEquity * RiskPerTrade / 100; double atrRisk = _atr[idx] * 2; // 2x ATR stop distance int shares = atrRisk > 0 ? (int)(riskAmount / atrRisk) : 0; if (shares < 1) return; // Cap position size at 5% of portfolio double posValue = shares * bars.Close[idx]; double maxPosValue = Backtester.CurrentEquity * 0.05; if (posValue > maxPosValue) shares = (int)(maxPosValue / bars.Close[idx]); if (shares < 1) return; // ── EXECUTE TRADE ─────────────────────────────────────── if (bullSignal) { // CALL direction — go long Transaction t = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, 0, $"CALL Signal: QoQ={priceGrowthQoQ:F1}% YoY={priceGrowthYoY:F1}%"); if (t != null) { t.Quantity = shares; t.Weight = priceGrowthQoQ + priceGrowthYoY; // Higher momentum = higher priority } } else if (bearSignal) { // PUT direction — go short Transaction t = PlaceTrade(bars, TransactionType.Short, OrderType.Market, 0, 0, $"PUT Signal: QoQ={priceGrowthQoQ:F1}% YoY={priceGrowthYoY:F1}%"); if (t != null) { t.Quantity = shares; t.Weight = Math.Abs(priceGrowthQoQ + priceGrowthYoY); } } } // ═══════════════════════════════════════════════════════════ // BACKTEST COMPLETE — post-processing // ═══════════════════════════════════════════════════════════ public override void BacktestComplete() { // Log summary statistics WriteToDebugLog($"Earnings Momentum Strategy Complete"); WriteToDebugLog($"EPS Threshold: {EpsThreshold}%"); WriteToDebugLog($"Risk Per Trade: {RiskPerTrade}%"); WriteToDebugLog($"IV Rank Max: {IVRankMax}"); } } }
DataSet
Go to My DataSets to define your own DataSets (subscribers only!)
Data Range & Scale
The Web Backtester currently uses a Data Range of 10 years of daily data. We'll offer more options here in a future update.
Scale
Position Sizing
Starting Capital Determines how much simulated capital your backtest starts with
Benchmark Symbol Your strategy results will be compared with a buy and hold of this symbol
Margin Factor Controls how much leverage to use in the simulated trading account
Sizing Method Controls how many shares each simulated trade will have
Percent Each trade will use this percentage of the current simulated account equity
Metric Strategy Results Benchmark Results (SPY)
Starting Capital 0.00 0.00
Profit 0.00 0.00
Profit % 0.00% 0.00%
CAGR (Annualized % Return) 0.00% 0.00%
Exposure % 0.00% 0.00%
Sharpe Ratio 0.00% 0.00%
WealthLab Score 0.00% 0.00%
Number of Positions 0.00% 0.00%
Average Profit % 0.00% 0.00%
Profit Factor 0.00% 0.00%
Payoff Ratio 0.00% 0.00%
Average Bars Held 0.00% 0.00%
NSF (Non-Sufficient Funds) Position Count 0.00% 0.00%
Maximum Drawdown 0.00% 0.00%
Maximum Drawdown % 0.00% 0.00%
Recovery Factor 0.00% 0.00%
Win % 0.00% 0.00%
Year Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Annual
The most recent 100 Positions out of 1,234 total are presented here.
Symbol Position Quantity Entry Date Entry Price Exit Date Exit Price Bars Held Profit Profit %
Signals are available to subscribers only. Click here to learn more about Wealth-Lab subscription options!