Repository navigation
FutureSettlementModel moves the daily settled P&L in the account currency into the quote-currency cash (non-USD account currency) #9837
Description
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 = settledPotential 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 withGetQuantityValue(quantity, settlementPrice).Amount - GetQuantityValue(quantity, averagePrice).Amountand keep a quote-currency counterpart of the settled amount.ApplyFunds: subtractremovedSettledProfit / security.QuoteCurrency.ConversionRatefrom the quote-currencyCashAmount.
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
masterbranch - I have confirmed that this is not a duplicate issue by searching issues
- I have provided detailed steps to reproduce the issue
Activity
Settle futures daily P&L in the contract's quote currency (QuantConne…