QuantConnect/Lean · error · RegressionTestException

Expected cash {expectedCash} but got {Portfolio.Cash}

Error message

Expected cash {expectedCash} but got {Portfolio.Cash}

What it means

Asserts compounding correctness. expectedCash = _cashAfterOrder * (1 + InterestRate)^CallCount, compared to Portfolio.Cash within 1e-10. A mismatch means the model applied interest a different number of times, on a different principal, or the cash accounting diverged from the simple compounding assumption.

Source

Thrown at Algorithm.CSharp/CustomMarginInterestRateModelAlgorithm.cs:83

            var security = Securities[_spy];
            var marginInterestRateModel = security.MarginInterestRateModel as CustomMarginInterestRateModel;

            if (marginInterestRateModel == null)
            {
                throw new RegressionTestException("CustomMarginInterestRateModel was not set");
            }

            if (marginInterestRateModel.CallCount == 0)
            {
                throw new RegressionTestException("CustomMarginInterestRateModel was not called");
            }

            var expectedCash = _cashAfterOrder * (decimal)Math.Pow(1 + (double)marginInterestRateModel.InterestRate, marginInterestRateModel.CallCount);

            // add a tolerance since using Math.Pow(double, double) given the lack of a decimal overload
            if (Math.Abs(Portfolio.Cash - expectedCash) > 1e-10m)
            {
                throw new RegressionTestException($"Expected cash {expectedCash} but got {Portfolio.Cash}");
            }
        }

        public class CustomMarginInterestRateModel : IMarginInterestRateModel
        {
            public decimal InterestRate { get; } = 0.01m;

            public int CallCount { get; private set; }

            public void ApplyMarginInterestRate(MarginInterestRateParameters marginInterestRateParameters)
            {
                var security = marginInterestRateParameters.Security;
                var positionValue = security.Holdings.GetQuantityValue(security.Holdings.Quantity);

                if (positionValue.Amount > 0)
                {
                    positionValue.Cash.AddAmount(InterestRate * positionValue.Cash.Amount);
                    CallCount++;

View on GitHub (pinned to d2c3659f87)

Solutions

  1. Ensure ApplyMarginInterestRate applies exactly InterestRate * base consistently and increments CallCount on the same condition each time.
  2. Confirm _cashAfterOrder is captured exactly once at the fill and the position is held flat afterward so the principal does not change.
  3. Reconcile whether interest should compound on positionValue.Cash.Amount vs Portfolio.Cash and make the closed-form formula match the actual base used.
  4. Log each ApplyMarginInterestRate call (principal, delta, cumulative) to find where accounting diverges.

Example fix

// before: interest added on a different base than the formula assumes
positionValue.Cash.AddAmount(InterestRate * Portfolio.Cash);

// after: add on the same principal the closed-form uses
var principal = _cashAfterOrder * Math.Pow(1 + (double)InterestRate, CallCount);
positionValue.Cash.AddAmount(InterestRate * positionValue.Cash.Amount);
Defensive patterns

Strategy: validation

Validate before calling

// Reconcile cash against the model's recorded applications
var applied = marginInterestRateModel.CallCount;
var expected = _cashAfterOrder * (decimal)Math.Pow(1 + (double)marginInterestRateModel.InterestRate, applied);
if (Math.Abs(Portfolio.Cash - expected) > 1e-10m)
    Log($"Cash drift detected: expected {expected}, actual {Portfolio.Cash}");

Prevention

When it happens

Trigger: ApplyMarginInterestRate ran CallCount times, but Portfolio.Cash does not equal the closed-form compound of _cashAfterOrder. Divergence arises if interest is applied to a changing principal, CallCount increments inconsistently, or AddAmount uses the wrong base.

Common situations: The model adds interest on positionValue.Cash.Amount (margin cash) which differs from Portfolio.Cash; position closed/reopened mid-run so _cashAfterOrder no longer represents the principal; or CallCount counts only some applications. Floating double-vs-decimal rounding is explicitly tolerated (1e-10), so larger gaps indicate logic errors.

Related errors


AI-assisted analysis of QuantConnect/Lean@d2c3659f87 (2026-08-13). Data as JSON: /api/errors/6e163691c1775510. Report an issue: GitHub.