Re-capture starting portfolio value after warm-up and fire OnWarmupFinished before post-warm-up data - #9653
Open
jhonabreul wants to merge 6 commits into
Conversation
In live mode with a warm-up period, the setup-time StartingPortfolioValue snapshot mixes deploy-time holdings prices with currency conversion rates seeded at the warm-up start, overstating or understating the baseline for net return, drawdown and the initial reported equity. The LiveTradingResultHandler now re-captures the starting portfolio value, cumulative max and daily portfolio values on the first synchronous event after warm-up finishes, when both holdings prices and conversion rates are current, and discards the equity bar accumulated during warm-up so the first sample opens at the corrected value. Closes QuantConnect#9647
jhonabreul
marked this pull request as ready for review
August 4, 2026 21:30
jhonabreul
marked this pull request as draft
August 5, 2026 15:03
…g portfolio value re-capture - Move the warm-up finished transition from AlgorithmManager.Stream into the Run loop so OnWarmupFinished fires in the same time slice that flips IsWarmingUp, before the first non-warm-up data reaches OnData - Notify the result handler through a new IResultHandler.OnWarmupFinished so the starting portfolio value is re-captured for both backtesting and live trading before any post-warm-up user code can trade - Add OnWarmupFinishedOrderingRegressionAlgorithm asserting the callback ordering
…tfolio value re-capture - WarmupStartingPortfolioValueRegressionAlgorithm reproduces GH issue QuantConnect#9647 in backtesting: foreign-currency cash with a one-year warm-up, pinning the Start Equity statistic to the warm-up-end portfolio value instead of the stale snapshot valued with the warm-up start conversion rates - BacktestingResultHandler unit tests covering the warm-up finished notification and the daily sample re-capture trigger paths
11 tasks
jhonabreul
marked this pull request as ready for review
August 5, 2026 18:55
The algorithm manager notifies the result handler's OnWarmupFinished only once, right when warm-up finishes, so the re-capture pending flag is not needed. The daily sample re-capture check is not needed either: the synchronizer's warm-up end time pulse guarantees the warm-up finished notification happens before any post-warm-up sample.
Martin-Molinero
approved these changes
Aug 5, 2026
The algorithm manager only notifies it once warm-up has actually finished
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Two related warm-up fixes:
1. Starting portfolio value captured with stale currency conversion rates (#9647).
In deployments with a warm-up period,
StartingPortfolioValueis captured at setup time with the cash book conversion rates seeded point-in-time at the warm-up start (in live mode additionally mixed with deploy-time holdings prices). For portfolios holding cash or positions in a currency other than the account currency, the further back warm-up reaches, the staler the FX component of the snapshot — producing a permanent phantom net return, contaminated drawdown statistics and an incorrect initial reported equity. It recurs on every live restart. Backtesting is affected by the same mechanism in a milder form:BacktestingSetupHandlercapturesPortfolio.Cashright after the conversion rates are seeded at the warm-up start, so the reported net return absorbs the FX drift that occurs during the warm-up window, before the algorithm can trade.2.
OnWarmupFinishedfired after the first non-warm-up data reachedOnData.AlgorithmManager.Streamcould only detect the warm-up transition when pulling the next time slice — one slice afterIsWarmingUpflipped inside the engine loop. When data ends exactly at the warm-up boundary (the normal case for intraday subscriptions on markets trading through it), oneOnDatacall withIsWarmingUpalready false executed beforeOnWarmupFinished, and orders placed there (the common "trade as soon as warm-up is done" pattern) could fill before the callback ran.The fix:
AlgorithmManager.Streaminto theRunloop, where it is handled in the same time slice that flipsIsWarmingUp: after the slice's securities and cash book updates — soOnWarmupFinishedsees the same market data as before — and before any user code runs for that slice. The transition is also checked at the time-pulse shortcut, since the boundary slice can be a dataless pulse; this keepsOnWarmupFinishedfiring with the algorithm time at the warm-up end.IResultHandler.OnWarmupFinished, then triggers the algorithm'sOnWarmupFinishedcallback (still invoked through the Python wrapper) and the running status update.BaseResultsHandler.OnWarmupFinishedre-capturesStartingPortfolioValue,CumulativeMaxPortfolioValueandDailyPortfolioValuefrom the current portfolio value — generically, for both backtesting and live trading — before any post-warm-up user code can trade. The algorithm manager notifies it exactly once, right when warm-up finishes; the re-capture only reassigns values that actually changed (so unaffected algorithms keep byte-identical statistics) and discards the equity bar accumulated during warm-up so the first equity sample opens at the re-captured value.Seeding the conversion rates point-in-time at the warm-up start is kept as is: it is correct for epoch-consistent warm-up replay, and chart sampling during warm-up was already skipped by the result handlers. Only the capture timing changes. Algorithms without warm-up are unaffected, and algorithms with warm-up but no foreign-currency balances re-capture a value identical to the setup-time one, keeping their statistics unchanged.
Related Issue
Closes #9647
Motivation and Context
Live algorithms with a long warm-up and multi-currency holdings report a permanent, incorrect net return (observed: -4.55% on an account that had not traded) and skewed drawdown statistics from the moment of deployment. Backtests combining warm-up with foreign-currency cash report returns contaminated by FX drift from a period when the algorithm could not trade. Additionally, algorithms could receive tradable post-warm-up data before
OnWarmupFinished, breaking the expectation that the callback marks the start of live data.Requires Documentation Change
No.
How Has This Been Tested?
WarmupStartingPortfolioValueRegressionAlgorithm: reproduces the issue in backtesting — foreign-currency cash with a one-year warm-up, so the conversion rate seeded at the warm-up start (EURUSD ~1.05) drifts ~14% during replay. The expected "Start Equity" statistic (220025.00) pins the re-captured warm-up-end value instead of the stale snapshot (~205290), and "Net Profit" reflects only the trading window. Fails on master, passes with the fix.OnWarmupFinishedOrderingRegressionAlgorithm: minute forex data with the warm-up boundary at mid-week midnight, so there is data ending exactly at the boundary; assertsOnWarmupFinishedfires at the warm-up end time and before anyOnDatacall withIsWarmingUpfalse. Fails on master, passes with the fix.LiveTradingResultHandlerTests.RecapturesStartingPortfolioValueAfterWarmupandBacktestingResultHandlerTests.RecapturesStartingPortfolioValueAfterWarmup: verifyStartingPortfolioValue,CumulativeMaxPortfolioValueandDailyPortfolioValueare re-captured from the current portfolio value on the warm-up finished notification.OnWarmupFinished*,WarmupConversionRatesand the option/future/universe warm-up variants): all pass with unchanged expected statistics.LimitOrdersAreFilledAfterHoursForFuturesRegressionAlgorithm, which places orders insideOnWarmupFinished: unchanged statistics, confirming the callback still sees the same market data as before.Engine.Results,AlgorithmManager,PaperBrokerageandAlgorithmLiveTradingunit test fixtures: 50 passed, 0 failed.End-to-end reproduction in local paper trading and backtesting
Live mode anchors to the real clock while the repo's EURUSD daily sample data covers 2007-2018, so a warm-up long enough to reach back into that data reproduces the mixed-epoch snapshot: setup seeds the EURUSD rate at the warm-up start (2013 => ~1.35) and warm-up replay carries it to the end of the sample data (2018-12-31 => ~1.147).
Scratch algorithm (EUR cash in a USD account, warm-up back to ~2013-09):
Launcher/config.json:"environment": "live-paper", with the environment'sdata-queue-handlerswitched to[ "QuantConnect.Lean.Engine.DataFeeds.Queues.FakeDataQueue" ](the defaultLiveDataQueueis a stub that throws on subscribe).Without the fix — the stale snapshot is captured and never corrected, a permanent ~-8.75% phantom return on an account that never traded:
With the fix — same setup snapshot (warm-up replay still gets its epoch-consistent seed), but the starting value is re-captured right when warm-up completes, landing exactly on the algorithm's own portfolio value:
The same effect reproduces in a backtest with the algorithm above modified with
SetStartDate(2018, 1, 1),SetEndDate(2018, 1, 15)andSetWarmUp(TimeSpan.FromDays(1461))(warm-up back to ~2014-01, EURUSD ~1.38 there vs ~1.20 at the start date), run in thebacktestingenvironment. The algorithm never trades, and EUR actually appreciated during the two test weeks:Without the fix — a phantom -6.378% net profit from the FX drift during the warm-up window:
With the fix — the reported return reflects only the actual FX movement during the trading window:
Types of changes
Checklist:
bug-<issue#>-<description>orfeature-<issue#>-<description>