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
Expected Behavior
With a non-USD account currency,
FutureSettlementModelshould 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.Scancomputes the daily P&L withFutureHolding.TotalCloseProfit(...), which returns an amount in the account currency (SecurityHolding.TotalCloseProfitconverts both legs through.InAccountCurrency,Common/Securities/SecurityHolding.cs:477-505), and adds that number unchanged to the quote-currency cash: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
TotalPortfolioValuedrifts bysettledProfit * (rate - 1)in account currency. The same mismatch is inApplyFunds(FutureSettlementModel.cs:52-55):removedSettledProfitis a slice ofSettledProfit(account currency) and is subtracted fromapplyFundsParameters.CashAmount, whichSecurityPortfolioModel.ProcessCloseTradeProfitbuilds in the quote currency (Common/Securities/SecurityPortfolioModel.cs:181-195).The existing
FutureSettlementModelTestsall 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, project37046068, backtest5862b9bb0a8043f65171c20d2bfdb1b3(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:Potential Solution
Keep
SettledProfitin the account currency (it is compared withTotalCloseProfit, also account currency, inFutureHolding.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
FutureSettlementModelTestswithSetAccountCurrency("SGD")(or any rate != 1 on the USDCash) and the existingDailySettlementcases 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
SettledProfitand 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
masterbranch