Problem Description
Solution
To build a simple Forex trading bot on QuantConnect, we will implement a classic Trend Following Strategy using an Exponential Moving Average (EMA) Crossover.
Strategy Logic
- Asset: EURUSD (The most liquid Forex pair).
- Indicators:
- Fast EMA (20-period): Reacts quickly to price changes.
- Slow EMA (50-period): Identifies the longer-term trend.
- Entry Rules:
- Buy (Long): When the Fast EMA crosses above the Slow EMA.
- Sell (Short): When the Fast EMA crosses below the Slow EMA.
- Exit Rules: The strategy is "always in the market." A Buy signal automatically closes a Short position, and a Sell signal automatically closes a Long position.
Python Implementation
Here is the complete, runnable code for the QuantConnect Lean engine.
# region imports
from AlgorithmImports import *
# endregion
class SimpleForexBot(QCAlgorithm):
def initialize(self):
# 1. Set Strategy Settings
self.set_start_date(2022, 1, 1) # Backtest Start Date
self.set_end_date(2024, 1, 1) # Backtest End Date
self.set_cash(100000) # Starting Capital (USD)
# 2. Add Forex Pair
# We use OANDA as the brokerage model for realistic spreads
self.pair = self.add_forex("EURUSD", Resolution.HOUR, Market.OANDA).symbol
# 3. Define Indicators
self.fast_ema = self.ema(self.pair, 20, Resolution.HOUR)
self.slow_ema = self.ema(self.pair, 50, Resolution.HOUR)
# 4. Warm Up
# Automatically feed historical data to indicators before the start date
# so they are ready to trade immediately.
self.set_warm_up(50, Resolution.HOUR)
# 5. Setup Plotting (Optional, for visualization)
chart = Chart("Trade Plot")
chart.add_series(Series("Price", SeriesType.LINE, 0))
chart.add_series(Series("Fast EMA", SeriesType.LINE, 0))
chart.add_series(Series("Slow EMA", SeriesType.LINE, 0))
self.add_chart(chart)
def on_data(self, data: Slice):
# Ensure data exists for our pair before processing
if self.pair not in data:
return
# Ensure indicators are ready (handled by WarmUp, but good practice)
if not self.fast_ema.is_ready or not self.slow_ema.is_ready:
return
# Get current indicator values
fast = self.fast_ema.current.value
slow = self.slow_ema.current.value
price = data[self.pair].close
# Plotting
self.plot("Trade Plot", "Price", price)
self.plot("Trade Plot", "Fast EMA", fast)
self.plot("Trade Plot", "Slow EMA", slow)
# Current Holdings
# quantity > 0 means Long, quantity < 0 means Short, 0 means Flat
holdings = self.portfolio[self.pair].quantity
# --- Trading Logic ---
# Signal: Fast EMA crosses ABOVE Slow EMA (Bullish)
if fast > slow:
# If we are not already Long
if holdings <= 0:
self.set_holdings(self.pair, 1.0) # Allocate 100% of buying power to Long
self.debug(f"BUY Signal at {self.time} | Price: {price}")
# Signal: Fast EMA crosses BELOW Slow EMA (Bearish)
elif fast < slow:
# If we are not already Short
if holdings >= 0:
self.set_holdings(self.pair, -1.0) # Allocate 100% of buying power to Short
self.debug(f"SELL Signal at {self.time} | Price: {price}")
Key Concepts Explained
self.add_forex: This subscribes to the data feed. We choseResolution.HOURto reduce noise compared to minute data.Market.OANDAis specified to ensure the backtest uses realistic Forex pricing and volume data.self.set_warm_up: Indicators like EMA require previous data to calculate their current value. Warming up ensures the algorithm doesn't wait 50 hours after the start date to place its first trade.self.set_holdings: This is a helper method that calculates how many units to buy or sell based on your total buying power.1.0uses 100% of equity to go Long.-1.0uses 100% of equity to go Short.- This automatically handles the math of converting USD to EUR.
self.plot: This draws the chart in the backtest results, allowing you to visually verify that trades are happening exactly when the lines cross.
How to Improve This Bot
Once you have this running, consider these next steps to make it production-ready:
- Risk Management: Add Stop Loss and Take Profit orders using
self.stop_market_orderorself.limit_order. - Volatility Filter: Add an indicator like ATR (Average True Range) to avoid trading when the market is flat/choppy.
- Session Filters: Forex markets are 24/5, but liquidity varies. You might want to restrict trading to London or New York sessions.
Q&A: Forex Algorithmic Trading
Q: Why use OANDA as the market in the code?
A: In QuantConnect, specifying the market (e.g., Market.OANDA or Market.FXCM) ensures that the backtest uses the specific historical price data, time zone conventions, and margin models of that broker. This produces more accurate results than generic data.
Q: Can I trade multiple pairs with this code?
A: The code above is designed for a single pair. To trade multiple pairs (e.g., EURUSD and GBPUSD), you would need to create a list of symbols in initialize, store indicators in a dictionary keyed by symbol, and loop through them in on_data.
Q: How does leverage work in this script?
A: Forex accounts usually have high leverage (e.g., 20x or 50x). self.set_holdings(self.pair, 1.0) uses 1x leverage (no borrowed money). To use leverage, you would increase the value, for example, self.set_holdings(self.pair, 10.0) would use 10x leverage. Be careful, as this significantly increases risk.
Q: Why use Hourly resolution instead of Minute?
A: For a "simple" moving average strategy, Minute data often contains too much "noise" (random price fluctuations), leading to false signals (whipsaws). Hourly data smooths out the price action, making trend identification easier for beginners.