Forecasting Cash Flow with Uncertainty, Not Just Averages

Written by Rohan Nandan on September 11, 2026 · 5 min read

Article Image

Problem

Simple finance forecasts usually report one number per day: expected income minus expected expenses. That hides the most important question for a user: how uncertain is the projection?

A rolling average also ignores weekly behavior, monthly cycles, recent spending velocity, and the very different statistical behavior of income versus expenses. Combining income and expenses into one net series too early can wash out useful signal.

The goal was to produce useful projections while making uncertainty explicit. A second goal was to keep forecasts reproducible: the same history plus the same model version should imply the same methodology, even if individual predictions change as new data arrives. For data-engineering interviews, the companion discipline is equally important—user-scoped caching, mutation-driven invalidation, and versioned response schemas around the models.

Approach

I implemented separate LightGBM regressors for income and expense forecasting.

1. Separate the two forecasting problems

Income and expenses behave differently:

Training one regressor per series preserves those differences. The cash-flow projection is then derived from the two independent forecasts. Runway is computed from the resulting average income, expenses, net burn, current balance, and threshold.

2. Engineer time-aware features

Each daily aggregate is expanded into features intended to capture seasonality, momentum, and trend:

This turns raw transaction history into a supervised regression dataset while preserving temporal order.

3. Validate like a time series

The models use walk-forward-style cross-validation rather than random train/test splits. Random splits would leak future behavior into training and overstate performance.

Final accuracy is tracked with MAE, RMSE, MAPE, and R². Training metadata records:

The current implementation is versioned as model 1.0.0.

4. Make uncertainty part of the API

Each forecast includes 80% confidence intervals derived from residual variation. The API response therefore carries:

That lets a client distinguish between “likely stable” and “highly variable” projections, rather than treating every forecast as equally reliable. Narrow intervals suggest repeatable history; wide intervals warn that daily outcomes may diverge from the point forecast.

5. Cache forecasts responsibly

Forecast generation is request-driven and cached per user in the predictions table:

Transaction, account, and recurring-rule mutations invalidate the affected user’s cache. There is no background scheduler yet; that remains explicit technical debt rather than a hidden assumption.

Stack

Results

Handle sparse and degenerate histories explicitly

A forecasting system must define behavior when data is thin. This implementation does that in several ways:

These rules prevent the model from inventing precision where the underlying ledger offers none. A forecast with no credible history should be visibly conservative, not confidently wrong.

Example forecast day:

{
  "date": "2026-09-08",
  "projected_balance": 5114.5,
  "expected_income": 100.0,
  "expected_expense": 85.5,
  "income_confidence_interval": {
    "lower": 75.0,
    "upper": 125.0
  },
  "expense_confidence_interval": {
    "lower": 60.0,
    "upper": 110.0
  }
}

Make the forecast reviewable end to end

An evaluator can follow the full path without guessing:

  1. Read the feature-engineering logic in ForecastRegressor.
  2. Check training thresholds, version, metrics, and feature importance.
  3. Read the cash-flow conversion and runway derivation in the forecast routes.
  4. Inspect the confidence-interval fields in the response schemas.
  5. Verify cache TTLs and invalidation in the prediction-cache service.
  6. Check integration coverage for forecast shape, isolation, cache reuse, expiry, and invalidation.

That trace—from historical transactions to engineered features, model output, confidence interval, cache entry, and API response—is the real deliverable. It also makes future model changes safer: a new version can be compared through the same schemas, caches, tests, and deployment health checks.

Lessons

  1. Don’t model net cash flow directly at first. Separate income and expense models preserve fundamentally different behaviors.
  2. Time order matters. Walk-forward validation is more honest than random splits for forecasting.
  3. Uncertainty is a feature. Confidence intervals make a forecast actionable instead of merely decorative.
  4. Persist everything needed to reproduce a model. Version, features, metrics, and training metadata matter as much as the model file.
  5. Be explicit about missing schedulers. Request-driven caching is a valid architecture, but it should be documented rather than implied.
  6. Keep simple detectors separate. Rule-based anomaly detection remains useful even after introducing regression forecasting; not every analytical question needs the same model.

Part of the "Expense Tracker Case Studies" series