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!