Skip to content

[CFD] Cannot set ContractMultiplier for CFD securities #9310

@Marcowu7756

Description

@Marcowu7756

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,XAUUSD

This 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:

  1. Cannot dynamically modify ContractMultiplier through algorithm code
  2. Can only rely on static configuration in symbol-properties-database.csv
  3. 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 * 5000

Solution 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,XAGUSD

But this requires:

  1. Knowing the correct contract_size for all CFD instruments
  2. Manually calculating ContractMultiplier for each instrument
  3. High maintenance cost

Reproduction Steps

  1. 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
  1. Run backtest and observe that PnL calculation results do not match expectations

Related Code Locations

  • Common/Securities/Cfd/Cfd.cs - CFD class definition
  • Common/Securities/SymbolProperties.cs - SymbolProperties base class
  • Common/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

  1. Ability to dynamically set CFD ContractMultiplier in algorithms
  2. PnL calculation results consistent with other backtesting engines
  3. 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

  1. Strategy Backtest Accuracy: Incorrect PnL calculations render strategy evaluation completely invalid
  2. Risk Management: Incorrect PnL affects position sizing, stop-loss/take-profit, and other critical decisions
  3. Multi-Engine Validation: Cannot compare results with other backtesting engines (e.g., MT5, proprietary engines)
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions