Skip to content

FutureSettlementModel moves the daily settled P&L in the account currency into the quote-currency cash (non-USD account currency) #9837

Description

@AlexCatarino

Expected Behavior

With a non-USD account currency, FutureSettlementModel should move the daily settled futures P&L into the contract's quote-currency cash in units of that quote currency. For one ES contract (USD-quoted) held in an SGD account, a settlement of -431.27 USD should change the USD cash by -431.27.

Actual Behavior

FutureSettlementModel.Scan computes the daily P&L with FutureHolding.TotalCloseProfit(...), which returns an amount in the account currency (SecurityHolding.TotalCloseProfit converts both legs through .InAccountCurrency, Common/Securities/SecurityHolding.cs:477-505), and adds that number unchanged to the quote-currency cash:

// Common/Securities/Future/FutureSettlementModel.cs:80-84
var dailyProfitLoss = futureHolding.TotalCloseProfit(includeFees: false, exitPrice: _settlementPrice) - futureHolding.SettledProfit;
if (dailyProfitLoss != 0)
{
    futureHolding.SettledProfit += dailyProfitLoss;
    settlementParameters.Portfolio.CashBook[security.QuoteCurrency.Symbol].AddAmount(dailyProfitLoss);

So an SGD account holding a USD-quoted contract has its USD cash moved by the SGD figure: with SGD/USD at about 1.35 the USD cash moves by 1.35x the real settlement every day, and TotalPortfolioValue drifts by settledProfit * (rate - 1) in account currency. The same mismatch is in ApplyFunds (FutureSettlementModel.cs:52-55): removedSettledProfit is a slice of SettledProfit (account currency) and is subtracted from applyFundsParameters.CashAmount, which SecurityPortfolioModel.ProcessCloseTradeProfit builds in the quote currency (Common/Securities/SecurityPortfolioModel.cs:181-195).

The existing FutureSettlementModelTests all run with a USD account and USD-quoted contracts, so the two currencies coincide and the mismatch is invisible there.

Reproduce

QC Cloud backtest, LEAN v2.5.0.0.18130, project 37046068, backtest 5862b9bb0a8043f65171c20d2bfdb1b3 (QuantConnect org). SGD account currency, 500,000 USD cash, one ES contract bought on 2024-02-05 and held through 2024-02-09. On every settlement the USD cash change equals the settled profit in SGD, not its USD value:

2024-02-05 09:32:00 FILLED qty=1.0 avg=4968.75 USD cash=499997.85 rate(SGD per USD)=1.34692 TPV(SGD)=1673319.52
2024-02-06 09:31:00 SETTLEMENT usd_cash_change=-580.29 settled_profit_change(SGD)=-580.29 same_profit_in_USD=-431.27 rate(SGD per USD)=1.34554 price=4973.875 unsettled(SGD)=913.78 TPV(SGD)=1672900.09
2024-02-07 09:31:00 SETTLEMENT usd_cash_change=823.76 settled_profit_change(SGD)=823.76 same_profit_in_USD=613.10 rate(SGD per USD)=1.34362 price=4997.625 unsettled(SGD)=1685.08 TPV(SGD)=1673816.83
2024-02-08 09:31:00 SETTLEMENT usd_cash_change=2906.24 settled_profit_change(SGD)=2906.24 same_profit_in_USD=2156.73 rate(SGD per USD)=1.34753 price=5010.625 unsettled(SGD)=-339.66 TPV(SGD)=1677664.27
2024-02-09 09:31:00 SETTLEMENT usd_cash_change=-77.21 settled_profit_change(SGD)=-77.21 same_profit_in_USD=-57.34 rate(SGD per USD)=1.34648 price=5017.875 unsettled(SGD)=223.48 TPV(SGD)=1677600.18
from AlgorithmImports import *

class FutureSettlementCurrencyRepro(QCAlgorithm):
    """Account currency SGD, one USD-quoted CME contract held across daily settlements.
    At each settlement, compares the USD cash change with the settled profit (which the
    engine computes in the account currency) and with that profit converted to USD."""

    def initialize(self):
        self.set_start_date(2024, 2, 5)
        self.set_end_date(2024, 2, 9)
        self.set_account_currency("SGD")
        self.set_cash(1_000_000)          # SGD
        self.set_cash("USD", 500_000)
        self._es = self.add_future(Futures.Indices.SP_500_E_MINI, Resolution.MINUTE)
        self._es.set_filter(0, 90)
        self._contract = None
        self._prev_usd = None
        self._prev_settled = 0.0

    def on_data(self, data):
        mapped = self._es.mapped
        if mapped is None:
            return
        if self._contract is None:
            if not self.securities.contains_key(mapped) or self.securities[mapped].price == 0:
                return
            self.market_order(mapped, 1)
            self._contract = mapped
            return
        h = self.securities[self._contract].holdings
        usd = self.portfolio.cash_book["USD"]
        if self._prev_usd is None:
            if h.quantity == 0:
                return
            self._prev_usd = usd.amount   # first bar after the fill: fees already applied
            self.log(f"{self.time} FILLED qty={h.quantity} avg={h.average_price} USD cash={usd.amount:.2f} rate(SGD per USD)={usd.conversion_rate:.5f} TPV(SGD)={self.portfolio.total_portfolio_value:.2f}")
            return
        settled = h.settled_profit
        if settled != self._prev_settled:
            d_usd = usd.amount - self._prev_usd
            d_settled = settled - self._prev_settled
            self.log(f"{self.time} SETTLEMENT usd_cash_change={d_usd:.2f} settled_profit_change(SGD)={d_settled:.2f} "
                     f"same_profit_in_USD={d_settled / usd.conversion_rate:.2f} rate(SGD per USD)={usd.conversion_rate:.5f} "
                     f"price={self.securities[self._contract].price} unsettled(SGD)={h.unsettled_profit:.2f} TPV(SGD)={self.portfolio.total_portfolio_value:.2f}")
            self._prev_usd = usd.amount
            self._prev_settled = settled

Potential Solution

Keep SettledProfit in the account currency (it is compared with TotalCloseProfit, also account currency, in FutureHolding.UnsettledProfit) and convert only the cash movement to the quote currency:

  • Scan: CashBook[security.QuoteCurrency.Symbol].AddAmount(dailyProfitLoss / security.QuoteCurrency.ConversionRate) (guard a zero rate), or compute the daily P&L directly in the quote currency with GetQuantityValue(quantity, settlementPrice).Amount - GetQuantityValue(quantity, averagePrice).Amount and keep a quote-currency counterpart of the settled amount.
  • ApplyFunds: subtract removedSettledProfit / security.QuoteCurrency.ConversionRate from the quote-currency CashAmount.

A unit test in FutureSettlementModelTests with SetAccountCurrency("SGD") (or any rate != 1 on the USD Cash) and the existing DailySettlement cases would cover it.

Open question: converting at the rate of the day means a later close-out at a different rate leaves a small residue between the account-currency SettledProfit and the quote-currency cash that was actually moved; tracking the settled amount in the quote currency as well would avoid it.

Reported via Intercom conversation 215476121360693 (live IB account with SGD base currency trading CME futures).

Checklist

  • I have completely filled out this template
  • I have confirmed that this issue exists on the current master branch
  • I have confirmed that this is not a duplicate issue by searching issues
  • I have provided detailed steps to reproduce the issue

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions