|
ABCD
// ═══════════════════════════════════════════════════════════════════════
// 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}");
}
}
}
|
|