In Vietnam’s fast-paced VN30 futures market, choosing the right trading strategy can make a big difference. One of the most popular tools among traders is the Moving Average (MA). But which MA strategy works better: a simple price crossover, or a trend-confirming two-MA filter?
Many traders rely on intuition or basic charts to decide—but that can lead to poor results. This article takes a data-driven approach. Using real 1-minute historical data from the VN30F1M contract and tested Python code, we compare two MA strategies under the same market conditions. We include key performance metrics like Sharpe Ratio, drawdown, and cumulative P&L, all calculated after trading fees.
📌 This backtest is intended for beginner traders and does not currently include trading commissions or slippage, allowing a clearer view of strategy behavior.
1.The Rules of Engagement
To run a fair and scientific comparison, we clearly define the logic used to generate trading signals based on 1-minute VN30F1M futures data.
This is the classic momentum-following approach. Its logic is direct and always keeps you in the market.
The Expected Downside: Its speed is also its biggest weakness. This strategy can be highly susceptible to "whipsaws"—getting repeatedly caught on the wrong side of the market during choppy, directionless periods, leading to a series of small, frustrating losses.
This strategy is more conservative. Instead of a simple crossover, it acts as a robust trend filter, designed to only trade when the market shows strong, confirmed direction.
Now that the rules are set, let's move on to the experiment itself and see how they performed.
A head-to-head comparison is meaningless unless the testing environment is fair, robust, and mirrors real-world conditions. Here’s a look under the hood at the quantitative framework we used to ensure a reliable result.
The entire analysis was built in a Python environment, leveraging powerful libraries to do the heavy lifting:
matplotlib: For visualizing our results and bringing the performance data to life.
A common mistake in backtesting is ignoring trading costs, which can turn a seemingly profitable strategy into a losing one. To avoid this, our backtests were configured to calculate Profit and Loss pnl_type="after_fees". This means the results you're about to see already account for the drag of broker commissions and slippage on every single trade. This isn't a theoretical simulation; it's a practical one.
To declare a winner, we need to look beyond just the final profit. A truly superior strategy must perform well across several key metrics. Here’s what we measured:
With our methodology defined and our metrics for success established, it's time to run the experiment and see what the data reveals.
With the rules defined and the environment set, we ran both strategies through our historical VN30 data. Here’s exactly how they performed.
First, we tested the simple price crossover. Here is the core Python logic that defines a "long" position (+1) when the price is above the 120-period SMA and "short" (-1) when below.
def gen_position_simpleMA(df, gap):
df['position'] = np.sign(df["Close"] - df['Close'].rolling(gap).mean())
df.dropna(inplace=True)
return df
df = derivatives.get_hist("VN30F1M", "1m")
df_pos = gen_position_simpleMA(df, 120)
print(df_pos.head())
# Backtest the strategy
backtest = Backtest_Derivates(df_pos, pnl_type="after_fees")
# Plot the cumulative PNL using pandas/matplotlib
pnl_series = backtest.PNL()
print(pnl_series.tail())
# Plot the cumulative PNL
pnl_series.plot(title="After-fees PNL of VN30F1M with simple MA Strategy", ylabel="After-fees PNL", xlabel="Time", figsize=(15, 7))
When we plot the cumulative Profit and Loss (PnL), we can see the strategy's journey.
Analysis: The strategy is profitable, but the equity curve is volatile. Notice the significant drawdowns and long, flat periods—this would have been a psychologically difficult strategy to trade.
Next, we ran our more conservative trend-filtering strategy. The code below only enters a long position if the price is above both the 50-period and 120-period MAs (and vice-versa for shorts).
def gen_position_twoMA(df, short_gap, long_gap):
df['short_ma'] = df['Close'].rolling(short_gap).mean()
df['long_ma'] = df['Close'].rolling(long_gap).mean()
df['position'] = 0 # Default to do nothing
df.loc[(df['Close'] > df['short_ma']) & (df['Close'] > df['long_ma']), 'position'] = 1 # Long
df.loc[(df['Close'] < df['short_ma']) & (df['Close'] < df['long_ma']), 'position'] = -1 # Short
df.dropna(inplace=True)
return df# Example usage with your existing data
df = derivatives.get_hist("VN30F1M", "1m")
df_pos_twoMA = gen_position_twoMA(df, 50, 120) # Example short_gap=50, long_gap=120
print(df_pos_twoMA.head())# Backtest the strategy
backtest_twoMA = Backtest_Derivates(df_pos_twoMA, pnl_type="raw")# Plot the cumulative PNL using pandas/matplotlib
pnl_series_twoMA = backtest_twoMA.PNL()
print(pnl_series_twoMA.tail())# Plot the cumulative PNL
pnl_series_twoMA.plot(title="After-fees PNL of VN30F1M with Two MA Strategy", ylabel="After-fees PNL", xlabel="Time", figsize=(15, 7), color = 'orange')# Initialize metrics
metrics_twoMA = Metrics(backtest_twoMA)# Calculate Sharpe Ratio
sharpe_ratio_twoMA = metrics_twoMA.sharpe()
print(f"Sharpe Ratio for Two MA Strategy: {sharpe_ratio_twoMA}")The resulting P&L curve tells a very different story.
Analysis: The difference is immediately clear. The P&L curve is visibly smoother, indicating more consistent growth and less volatility.
Visualizing both strategies on the same chart leaves no room for doubt.
The visual evidence is compelling. The Two-MA strategy (orange line) consistently outperforms, achieving higher returns with a much more stable trajectory. But to make our final verdict, we need to look at the single most important number.
While the graphs paint a compelling picture, the numbers deliver the final, objective verdict. To compare strategies, we look at the Sharpe Ratio—the ultimate measure of risk-adjusted return. A higher ratio means more return for each unit of risk taken.
Metric | Simple MA Crossover | Two-MA Confirmation | Winner |
|---|---|---|---|
Sharpe Ratio | 2.06 | 2.93 | Two-MA Strategy |
Drawdown | Higher | Lower | Two-MA Strategy |
Cumulative P&L | Volatile | Smoother | Two-MA Strategy |
The data is unequivocal. The Two-MA Trend Confirmation strategy achieved a Sharpe Ratio of 2.93, a stunning 42% higher than the Simple MA's 2.06. This isn't a marginal improvement; it's a fundamentally more robust and efficient system.
Why was the Two-MA strategy so much more effective?
The answer lies in one word: filtering. The Simple MA Crossover is a pure momentum play. It's fast, but it gets brutally "whipsawed" in choppy, sideways markets, leading to frequent small losses and high psychological stress.
The Two-MA strategy, by contrast, acts as a regime filter. By requiring the price to be above (or below) both moving averages, it effectively waits for confirmation that a real trend is in place. It patiently sits out the market noise that plagues the simpler system.
The trade-off is that it enters and exits later. But as the data shows, what it lost in speed, it more than made up for in accuracy and lower risk. The smoother equity curve and higher Sharpe Ratio mean it would have been a far easier and more profitable strategy to follow in the real world.
The backtest results are clear: the Two-MA Trend Confirmation strategy outperformed the Simple MA Crossover on the VN30F1M contract. While the simple strategy responded faster, it was vulnerable to false signals in sideways markets. In contrast, the Two-MA approach waited for trend confirmation, delivering more stable returns and a significantly higher Sharpe Ratio (2.93 vs. 2.06).
In a volatile futures environment, filtering out noise is more effective than chasing momentum. Although the Two-MA strategy may miss the earliest parts of a move, it avoids costly mistakes and improves long-term performance consistency.