Case Study CS-006: Daily Trend Momentum v1: Validating a Volatility-Targeted Gold Strategy Across Python Backtest and MQL5 Parity Workflow
1. Executive Summary & Core Hypothesis
The primary objective of this quantitative research experiment is to evaluate whether a systematic daily momentum model can isolate a premium in Spot Gold (XAUUSD) while neutralizing intraday noise. Daily Trend Momentum v1 operates on the premise that filtering macro trends via daily boundaries minimizes high-frequency execution degradation. This architecture leverages high-fidelity data feeds sourced directly from Dukascopy, transitioning historical statistical matrices from programmatic Python vectors into MQL5 framework execution wrappers.
The research process sets up a rigid progression filter: testing a robust directional core across a 12-month development space, subjecting the output parameters to an untouched 2026 holdout continuum, and implementing a parity prototype inside the Strategy Tester interface. The logging confirms that while the system captures meaningful extensions, the performance variance between synthetic boundaries and real-world regimes requires proactive structural downscaling before live distribution layer exposure.
2. Architectural Edge and Market Dynamics
Standard momentum implementations face compounding vulnerabilities due to irregular volatility conditions across varying financial horizons. By constructing clean UTC daily bars reconstructed strictly from localized H1 data clusters, this strategy prevents historical data distortion from weekend fractional candle bars. Risk allocations are managed through a dynamic 25% annualized volatility target matched by an uncompromised 2x absolute leverage constraint layer.
To map out strategy decay parameters, performance matrices were cross-examined between the initial development timeline and the out-of-sample holdout tracking envelope. The statistical breakdown underlines the specific behavioral shifts observed across these testing environments:
| Validation Testing Phase | Sample Space Profile | Recorded Absolute Return | Max Peak Drawdown | Observed Profit Factor |
|---|---|---|---|---|
| 2025 Development Test | 12-Month In-Sample Space | +65.98% | 11.92% Deviation | 1.488 |
| Jan-May 2026 Holdout | 5-Month Out-of-Sample | +5.60% | 14.97% Max Peak | 1.131 |
The comparative data reveals an operational performance shift. While the 2025 in-sample development cycle demonstrated superior structural geometry, the subsequent 2026 holdout environment highlighted profit compression and drawdown enlargement. This verifies that the strategy remains structurally viable but lacks a repeatable, high-velocity trajectory under shifting volatility parameters.
3. Structural Identification Rules: Daily Trend Momentum v1 Research Experiment
The mathematical architecture rejects human visual pattern bias by enforcing automated structural boundary conditions. In order to confirm macro directional consensus, a twin Exponential Moving Average (EMA) layout forms the foundational entry gateway:
- EMA Trend Alignment: The framework evaluates the immediate relationship between the 20-period EMA and the 50-period EMA on reconstructed daily structures. Bullish alignment requires the 20 EMA to settle strictly above the 50 EMA boundary.
- Directional Momentum Confirmation: A secondary structural calculation parses the absolute 50-day price path. Multi-candle trend direction must establish positive progression over this extended window to clear the execution layer for buy orders.
- Volatility-Targeted Sizing Loop: The absolute trading volume allocation is recalculated upon every bar closure based on a 25% annualized volatility target parameter, capping max leverage allocation tightly at 2x.
By maintaining these strict rules, the script converts qualitative momentum biases into clear numerical steps. This enables our processing system to map entries and compute exact exposure limits without subjective human optimization errors.
4. The Multi-Timeframe Execution Blueprint
To maintain alignment between synthetic mathematical modules and terminal order execution, the research script applies a distinct multi-timeframe filtering hierarchy. This workflow filters structural trends at macro layers before initiating entry logic:
A. Daily Macro Trend Bias Filter
The system processes the primary trend bias on structural daily charts. By anchoring calculations inside this extended timeline, the framework remains completely insulated from intraday liquidity sweeps and rapid spread widening occurring during localized news drops. Long allocations are permitted exclusively when the underlying asset trades cleanly above the macro filters, while short models are locked down automatically.
B. Micro H1/M5 Execution Alignment
Once daily parameters confirm directional consensus, the script queries underlying H1 data sequences to reconstruct exact UTC daily boundaries. Execution layers then monitor micro intervals for price retracements toward structural boundaries. This method captures broad macro movements while minimizing risk by placing precise stop-loss orders beneath structural lows.
5. Algorithmic Implementation Strategy
To achieve complete execution parity verification between Python logic models and trading terminal architectures, the strategy framework is implemented via a Strategy Tester-only MQL5 EA prototype. This component accurately manages UTC structural day tracking, dynamic EWMA volatility targeting, and macro momentum constraints.
The production-ready MQL5 algorithmic implementation executes the strategy according to the following syntax structure:
#property copyright "FlowTraderTools.com Labs"
#property version "1.00"
#property strict
#property description "Experimental Strategy Tester EA for Daily Trend Momentum v1"
#include <Trade/Trade.mqh>
input group "Safety"
input bool InpTesterOnly = true;
input ulong InpMagicNumber = 26061201;
input int InpDeviationPoints = 50;
input group "Test period and clock"
input datetime InpDataStartUTC = D'2024.06.01 00:00';
input datetime InpTradeStartUTC = D'2025.01.01 00:00';
input datetime InpTradeEndUTC = D'2026.01.01 00:00';
input int InpServerUtcOffsetHours = 0;
input int InpExitHourUTC = 23;
input int InpExitMinuteUTC = 55;
input group "Frozen strategy parameters"
input int InpFastEma = 20;
input int InpSlowEma = 50;
input int InpMomentumDays = 50;
input int InpSlopeDays = 5;
input double InpEwmaLambda = 0.94;
input double InpAnnualVolatilityTarget = 0.25;
input double InpMinimumVolatility = 0.03;
input double InpMaximumLeverage = 2.0;
input double InpExpectedContractSize = 100.0;
input group "Diagnostics"
input bool InpWriteDiagnostics = true;
input string InpDiagnosticsFile = "trading-lab-daily-momentum-v1.csv";
struct SignalSnapshot
{
int side;
datetime signal_day;
double close_price;
double fast_ema;
double slow_ema;
double momentum;
double slope;
double forecast_volatility;
double target_leverage;
};
CTrade trade;
int g_utc_day_key = 0;
int g_last_open_day_key = 0;
int g_last_exit_day_key = 0;
int g_log_handle = INVALID_HANDLE;
int DateKey(const datetime value)
{
MqlDateTime parts;
TimeToStruct(value,parts);
return parts.year*10000+parts.mon*100+parts.day;
}
datetime ToUtc(const datetime server_time)
{
return server_time-InpServerUtcOffsetHours*3600;
}
datetime ToServer(const datetime utc_time)
{
return utc_time+InpServerUtcOffsetHours*3600;
}
bool IsTradingDate(const datetime utc_time)
{
return utc_time>=InpTradeStartUTC && utc_time<InpTradeEndUTC;
}
bool IsOurPosition()
{
if(!PositionSelect(_Symbol))
return false;
return (ulong)PositionGetInteger(POSITION_MAGIC)==InpMagicNumber;
}
bool CloseOurPosition(const string reason)
{
if(!IsOurPosition())
return true;
if(!trade.PositionClose(_Symbol,InpDeviationPoints))
{
PrintFormat("TradingLab close failed (%s): %u %s",
reason,trade.ResultRetcode(),trade.ResultRetcodeDescription());
return false;
}
PrintFormat("TradingLab position closed: %s at %.5f",
reason,trade.ResultPrice());
return true;
}
double NormalizeVolumeFloor(const double raw_volume)
{
const double broker_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
const double broker_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
const double broker_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
const double step=MathMax(0.01,broker_step);
const double minimum=MathMax(0.01,broker_min);
double volume=MathFloor(raw_volume/step+1e-9)*step;
volume=MathMin(volume,broker_max);
if(volume<minimum)
return 0.0;
int digits=0;
double scale=step;
while(digits<8 && MathAbs(scale-MathRound(scale))>1e-9)
{
scale*=10.0;
digits++;
}
return NormalizeDouble(volume,digits);
}
bool BuildCompletedDailyCloses(double &closes[],datetime &days[])
{
MqlRates rates[];
const datetime current_server=TimeCurrent();
const int copied=CopyRates(_Symbol,PERIOD_H1,
ToServer(InpDataStartUTC),
current_server,rates);
if(copied<=0)
{
PrintFormat("TradingLab CopyRates failed: %d",GetLastError());
return false;
}
ArrayResize(closes,0);
ArrayResize(days,0);
const int current_day=DateKey(ToUtc(current_server));
int last_key=0;
for(int index=0;index<copied;index++)
{
const datetime utc_time=ToUtc(rates[index].time);
const int key=DateKey(utc_time);
if(key>=current_day)
continue;
const double midpoint_close=
rates[index].close+rates[index].spread*_Point/2.0;
if(key!=last_key)
{
const int size=ArraySize(closes);
ArrayResize(closes,size+1);
ArrayResize(days,size+1);
closes[size]=midpoint_close;
days[size]=utc_time;
last_key=key;
}
else
{
const int last=ArraySize(closes)-1;
closes[last]=midpoint_close;
days[last]=utc_time;
}
}
return ArraySize(closes)>InpSlowEma+2;
}
bool CalculateSnapshot(SignalSnapshot &snapshot)
{
double closes[];
datetime days[];
if(!BuildCompletedDailyCloses(closes,days))
return false;
const int count=ArraySize(closes);
const int last=count-1;
const int required=MathMax(InpMomentumDays,InpSlopeDays);
if(last<required || count<InpSlowEma+1)
return false;
double fast[];
double slow[];
double returns[];
ArrayResize(fast,count);
ArrayResize(slow,count);
ArrayResize(returns,count);
const double fast_alpha=2.0/(InpFastEma+1.0);
const double slow_alpha=2.0/(InpSlowEma+1.0);
fast[0]=MathLog(closes[0]);
slow[0]=fast[0];
returns[0]=0.0;
for(int index=1;index<count;index++)
{
const double log_close=MathLog(closes[index]);
fast[index]=fast_alpha*log_close+(1.0-fast_alpha)*fast[index-1];
slow[index]=slow_alpha*log_close+(1.0-slow_alpha)*slow[index-1];
returns[index]=closes[index]/closes[index-1]-1.0;
}
double variance=0.0;
for(int index=1;index<InpSlowEma;index++)
variance+=returns[index]*returns[index];
variance=MathMax(variance/(InpSlowEma-1),1e-12);
for(int index=1;index<=last;index++)
variance=InpEwmaLambda*variance
+(1.0-InpEwmaLambda)*returns[index-1]*returns[index-1];
const double momentum=closes[last]/closes[last-InpMomentumDays]-1.0;
const double slope=fast[last]-fast[last-InpSlopeDays];
int side=0;
if(fast[last]>slow[last] && momentum>0.0 && slope>0.0)
side=1;
else if(fast[last]<slow[last] && momentum<0.0 && slope<0.0)
side=-1;
const double forecast=MathMax(MathSqrt(variance*252.0),
InpMinimumVolatility);
snapshot.side=side;
snapshot.signal_day=days[last];
snapshot.close_price=closes[last];
snapshot.fast_ema=fast[last];
snapshot.slow_ema=slow[last];
snapshot.momentum=momentum;
snapshot.slope=slope;
snapshot.forecast_volatility=forecast;
snapshot.target_leverage=MathMin(InpMaximumLeverage,
InpAnnualVolatilityTarget/forecast);
return true;
}
void WriteDiagnostic(const SignalSnapshot &snapshot,
const datetime trade_day,
const double balance,
const double price,
const double lots,
const string result)
{
if(g_log_handle==INVALID_HANDLE)
return;
FileWrite(g_log_handle,
TimeToString(snapshot.signal_day,TIME_DATE),
TimeToString(trade_day,TIME_DATE),
snapshot.side,
DoubleToString(snapshot.close_price,_Digits),
DoubleToString(snapshot.fast_ema,8),
DoubleToString(snapshot.slow_ema,8),
DoubleToString(snapshot.momentum,8),
DoubleToString(snapshot.slope,8),
DoubleToString(snapshot.forecast_volatility,8),
DoubleToString(snapshot.target_leverage,8),
DoubleToString(balance,2),
DoubleToString(price,_Digits),
DoubleToString(lots,2),
result);
FileFlush(g_log_handle);
}
void OpenForNewUtcDay(const datetime utc_now)
{
const int day_key=DateKey(utc_now);
if(day_key==g_last_open_day_key || utc_now>=InpTradeEndUTC)
return;
SignalSnapshot snapshot;
if(!CalculateSnapshot(snapshot))
{
Print("TradingLab could not calculate a daily snapshot.");
return;
}
if(!IsTradingDate(snapshot.signal_day))
{
g_last_open_day_key=day_key;
return;
}
g_last_open_day_key=day_key;
if(snapshot.side==0)
{
WriteDiagnostic(snapshot,utc_now,AccountInfoDouble(ACCOUNT_BALANCE),
0.0,0.0,"flat");
return;
}
MqlTick tick;
if(!SymbolInfoTick(_Symbol,tick))
return;
const double price=(snapshot.side>0 ? tick.ask : tick.bid);
const double balance=AccountInfoDouble(ACCOUNT_BALANCE);
const double raw_lots=
balance*snapshot.target_leverage/(price*InpExpectedContractSize);
const double lots=NormalizeVolumeFloor(raw_lots);
if(lots<=0.0)
{
WriteDiagnostic(snapshot,utc_now,balance,price,lots,"volume_rejected");
return;
}
bool accepted=false;
if(snapshot.side>0)
accepted=trade.Buy(lots,_Symbol,0.0,0.0,0.0,"TL-DTM-v1");
else
accepted=trade.Sell(lots,_Symbol,0.0,0.0,0.0,"TL-DTM-v1");
const string result=(accepted
? "opened"
: "rejected:"+trade.ResultRetcodeDescription());
WriteDiagnostic(snapshot,utc_now,balance,price,lots,result);
if(!accepted)
PrintFormat("TradingLab open failed: %u %s",
trade.ResultRetcode(),trade.ResultRetcodeDescription());
}
int OnInit()
{
if(InpTesterOnly && !MQLInfoInteger(MQL_TESTER))
{
Print("TradingLab EA is tester-only. Initialization refused.");
return INIT_FAILED;
}
if(InpFastEma<2 || InpFastEma>=InpSlowEma ||
InpMomentumDays<2 || InpSlopeDays<1 ||
InpEwmaLambda<=0.0 || InpEwmaLambda>=1.0 ||
InpAnnualVolatilityTarget<=0.0 ||
InpAnnualVolatilityTarget>0.50 ||
InpMaximumLeverage<=0.0 || InpMaximumLeverage>2.0 ||
InpTradeStartUTC>=InpTradeEndUTC)
{
Print("TradingLab input validation failed.");
return INIT_PARAMETERS_INCORRECT;
}
trade.SetExpertMagicNumber(InpMagicNumber);
trade.SetDeviationInPoints(InpDeviationPoints);
trade.SetTypeFillingBySymbol(_Symbol);
trade.SetAsyncMode(false);
const double broker_contract=
SymbolInfoDouble(_Symbol,SYMBOL_TRADE_CONTRACT_SIZE);
if(MathAbs(broker_contract-InpExpectedContractSize)>1e-8)
PrintFormat("WARNING: broker contract size %.4f differs from parity value %.4f",
broker_contract,InpExpectedContractSize);
if(InpWriteDiagnostics)
{
g_log_handle=FileOpen(InpDiagnosticsFile,
FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI);
if(g_log_handle==INVALID_HANDLE)
PrintFormat("TradingLab diagnostic file failed: %d",GetLastError());
else
{
if(FileSize(g_log_handle)==0)
FileWrite(g_log_handle,
"signal_date_utc","trade_date_utc","side",
"mid_close","fast_ema","slow_ema","momentum",
"slope","forecast_volatility","target_leverage",
"balance","entry_price","lots","result");
FileSeek(g_log_handle,0,SEEK_END);
}
}
Print("TradingLab Daily Trend Momentum v1 initialized.");
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(g_log_handle!=INVALID_HANDLE)
FileClose(g_log_handle);
}
void OnTick()
{
const datetime utc_now=ToUtc(TimeCurrent());
const int day_key=DateKey(utc_now);
if(g_utc_day_key==0 || day_key!=g_utc_day_key)
{
if(g_utc_day_key!=0 && IsOurPosition())
CloseOurPosition("missed configured UTC exit");
g_utc_day_key=day_key;
g_last_exit_day_key=0;
OpenForNewUtcDay(utc_now);
}
MqlDateTime parts;
TimeToStruct(utc_now,parts);
if(day_key!=g_last_exit_day_key &&
(parts.hour>InpExitHourUTC ||
(parts.hour==InpExitHourUTC && parts.min>=InpExitMinuteUTC)))
{
if(CloseOurPosition("configured UTC daily exit"))
g_last_exit_day_key=day_key;
}
}
6. Conclusion and Future Directions
The empirical logging compiled across consecutive development and out-of-sample holdout tracking horizons reveals that Daily Trend Momentum v1 possesses a robust mechanical edge but remains subject to profit compression during shifting volatility profiles. Because the out-of-sample profit factor narrowed to 1.131, this algorithm is classified as a testing prototype rather than a production-ready system.
The future development roadmap will focus on implementing an adaptive machine-learning filter loop inside the MQL5 script architecture. This enhancement will allow the position-sizing logic to dynamically scale risk targets based on real-time changes in macro volatility distributions, ensuring stability across evolving market regimes.