-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Description
LEAN CFD ContractMultiplier Configuration Issue
Problem Description
When backtesting CFD (Contract for Difference) instruments in LEAN, I discovered that ContractMultiplier cannot be dynamically set for CFD securities, causing significant PnL calculation discrepancies compared to other backtesting engines (such as our proprietary DLL engine).
Environment
- LEAN Version: v2.5+ (running on .NET 10)
- Broker: Oanda
- Security Type: CFD (SecurityType.Cfd)
- Affected Symbols: XAGUSD (Silver), XAUUSD (Gold), and other CFD instruments
Issue Details
1. Current Behavior
For Oanda CFD instruments, ContractMultiplier is hardcoded to 1 in symbol-properties-database.csv:
# symbol-properties-database.csv
cfd,oanda,XAGUSD,USD,1,0.001,1,XAGUSD
cfd,oanda,XAUUSD,USD,1,0.01,1,XAUUSDThis causes all CFD instruments to use ContractMultiplier = 1 for PnL calculations:
// Cfd.cs line 111-114
public decimal ContractMultiplier
{
get { return SymbolProperties.ContractMultiplier; }
}2. Expected Behavior
Different CFD instruments should have different ContractMultiplier values to correctly reflect contract specifications:
| Symbol | tick_size | contract_size | Expected ContractMultiplier |
|---|---|---|---|
| XAGUSD | 0.001 | 5000 | 5.0 |
| XAUUSD | 0.01 | 100 | 1.0 |
| EURUSD | 0.00001 | 100000 | 1.0 |
The correct PnL calculation formula should be:
PnL = price_change × ContractMultiplier × quantity
Where ContractMultiplier = tick_size × contract_size
3. Actual Impact
Using XAGUSD as an example, with identical trading logic and parameters:
| Engine | Trades | Total Return | Max Drawdown |
|---|---|---|---|
| DLL (Correct) | 599 | -34.43% | 37.70% |
| LEAN (Incorrect) | 601 | -94.48% | 94.60% |
60% return difference - completely unusable for strategy evaluation.
Root Cause Analysis
1. CFD Class Does Not Support Dynamic ContractMultiplier Setting
Inspecting LEAN source code reveals:
Option class supports dynamic setting:
// Option.cs line 268-273
public decimal ContractMultiplier
{
get { return _symbolProperties.ContractMultiplier; }
set { _symbolProperties.SetContractMultiplier(value); }
}But CFD class only has getter, no setter:
// Cfd.cs line 111-114
public decimal ContractMultiplier
{
get { return SymbolProperties.ContractMultiplier; }
// ❌ No setter!
}2. SymbolProperties ContractMultiplier is internal set
// SymbolProperties.cs line 47-51
public virtual decimal ContractMultiplier
{
get => _properties.ContractMultiplier;
internal set => _properties.ContractMultiplier = value; // ❌ internal, not accessible externally
}This means:
- Cannot dynamically modify
ContractMultiplierthrough algorithm code - Can only rely on static configuration in
symbol-properties-database.csv - But Oanda CFD configurations are all hardcoded to 1
Suggested Solutions
Solution 1: Add ContractMultiplier setter to CFD class (Recommended)
Modify Common/Securities/Cfd/Cfd.cs:
/// <summary>
/// Gets or sets the contract multiplier for this CFD security
/// </summary>
public decimal ContractMultiplier
{
get { return SymbolProperties.ContractMultiplier; }
set
{
if (SymbolProperties is CfdSymbolProperties cfdProps)
{
cfdProps.SetContractMultiplier(value);
}
}
}Also add CfdSymbolProperties class (similar to OptionSymbolProperties):
namespace QuantConnect.Securities.Cfd
{
public class CfdSymbolProperties : SymbolProperties
{
internal void SetContractMultiplier(decimal multiplier)
{
ContractMultiplier = multiplier;
}
}
}Solution 2: Allow Overriding SymbolProperties in Initialize()
Allow this code in the algorithm's Initialize() method:
def Initialize(self):
symbol = self.AddCfd("XAGUSD", Resolution.Hour, Market.Oanda)
# Set correct ContractMultiplier
security = self.Securities[symbol]
security.ContractMultiplier = 5.0 # tick_size * contract_size = 0.001 * 5000Solution 3: Update symbol-properties-database.csv
Provide correct ContractMultiplier configuration for Oanda CFD instruments:
# Before
cfd,oanda,XAGUSD,USD,1,0.001,1,XAGUSD
# After
cfd,oanda,XAGUSD,USD,5.0,0.001,1,XAGUSDBut this requires:
- Knowing the correct
contract_sizefor all CFD instruments - Manually calculating
ContractMultiplierfor each instrument - High maintenance cost
Reproduction Steps
- Create a simple CFD algorithm:
class CfdContractMultiplierTest(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2023, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(10000)
symbol = self.AddCfd("XAGUSD", Resolution.Hour, Market.Oanda)
security = self.Securities[symbol]
# Print ContractMultiplier
self.Debug(f"ContractMultiplier: {security.SymbolProperties.ContractMultiplier}")
# Output: ContractMultiplier: 1
# Try to set (will fail)
# security.ContractMultiplier = 5.0 # AttributeError: 'Cfd' object has no attribute 'ContractMultiplier' setter- Run backtest and observe that PnL calculation results do not match expectations
Related Code Locations
Common/Securities/Cfd/Cfd.cs- CFD class definitionCommon/Securities/SymbolProperties.cs- SymbolProperties base classCommon/Securities/Option/OptionSymbolProperties.cs- Option SymbolProperties (reference implementation)Common/Securities/Option/Option.cs- Option class (reference implementation)Data/symbol-properties/symbol-properties-database.csv- Symbol properties database
Expected Results
- Ability to dynamically set CFD
ContractMultiplierin algorithms - PnL calculation results consistent with other backtesting engines
- Different CFD instruments can use correct contract specifications
Additional Information
Test Data
Using identical strategy parameters (Deviation factor, 2023-2024 data):
XAGUSD (contract_size=5000, expected ContractMultiplier=5.0):
- Current LEAN (ContractMultiplier=1): Return -94.48%
- DLL Engine (correct calculation): Return -34.43%
- Difference: 60%
XAUUSD (contract_size=100, expected ContractMultiplier=1.0):
- Current LEAN (ContractMultiplier=1): Return -59.02%
- DLL Engine (correct calculation): Return -55.42%
- Difference: 3.6% ✅ (basically aligned, because ContractMultiplier happens to be correct)
Why This Matters
- Strategy Backtest Accuracy: Incorrect PnL calculations render strategy evaluation completely invalid
- Risk Management: Incorrect PnL affects position sizing, stop-loss/take-profit, and other critical decisions
- Multi-Engine Validation: Cannot compare results with other backtesting engines (e.g., MT5, proprietary engines)
- Live Trading Consistency: Backtest results inconsistent with live trading PnL calculations
Related Issues
- Similar issues may affect other instruments with non-standard contract specifications (e.g., certain futures, crypto CFDs, etc.)
Thank you to the LEAN team for your hard work! Looking forward to seeing this issue resolved.