Balance Prediction Scenario Lab
Desktop-first what-if tooling on top of the existing 3-scenario balance prediction engine: saved scenarios, inline adjustments, baseline comparison, budget impact, and affordability checks.
Overview
The scenario lab extends BalancePredictionFilters / BalancePredictionResponse without replacing the core BASE / OPTIMISTIC / PESSIMISTIC model. Users can layer saved scenarios (persisted in PostgreSQL), inline adjustments (session-only), and legacy periodic spends into a single prediction run. When scenarios or inline adjustments are active, the engine can return a baseline curve (compareToBaseline) and per-budget impact rows for the point-in-time view.
Platform coverage (current):
| Capability | Desktop (composeApp) | Android (finance_presentation) | Web |
|---|---|---|---|
| Core prediction chart | Yes | Yes | Yes (BalancePredictionTab.tsx) |
| Saved scenarios CRUD + toggle | Yes | No | No |
| Scenario adjustment dialog | Yes | No | No |
| Affordability (“Can I afford…?”) | Yes | No | No |
| Budget impact panel | Yes | No | No |
| Baseline overlay on chart | Yes | No | No |
| Category dropdowns in dialogs | Yes | Partial (volatility editor still text on Android) | N/A |
Wire dates use dd/MM/yyyy (calendar dates) and dd/MM/yyyy HH:mm for date-times — not ISO yyyy-MM-dd.
Architecture
Adjustment resolution order: enabled adjustments from selected scenarioIds + inlineAdjustments + periodicSpends (each periodic spend is converted to a NEW_TRANSACTION adjustment). Disabled adjustments are skipped.
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /finance/transactions/balance-prediction | Run prediction (BalancePredictionFilters) |
POST | /finance/transactions/balance-prediction/affordability | One-off purchase what-if (AffordabilityRequest) |
GET | /finance/balance-scenarios | List saved scenarios for user |
POST | /finance/balance-scenarios | Create scenario |
PUT | /finance/balance-scenarios/{id} | Update scenario |
DELETE | /finance/balance-scenarios/{id} | Soft-delete scenario |
GET | /finance/balance-prediction/settings | Load per-user engine defaults |
PUT | /finance/balance-prediction/settings | Persist per-user engine defaults |
Prediction filters (scenario lab fields)
| Field | Type | Purpose |
|---|---|---|
scenarioIds | List<String> | UUIDs of saved scenarios to merge |
inlineAdjustments | List<ScenarioAdjustment> | Session-only adjustments |
compareToBaseline | Boolean | When true, response includes baselinePoints (run without scenario/inline layer) |
periodicSpends | List<PeriodicSpend> | Legacy recurring items; merged as NEW_TRANSACTION |
Existing fields (fromDate, toDate, accountIds, includeScheduledTransactions, includePastMonthBehavior, noisePercentage, categoryVolatilityOverrides, amplitudeFactor, safetyFloor, goalId, pagination) behave as before. See Prediction engine and balance-prediction-algorithm-analysis.md.
Scenario adjustment types
Defined in shared/.../models/finance/ScenarioAdjustment.kt:
| Type | Purpose |
|---|---|
NEW_TRANSACTION | Add income/expense on a frequency (ONE_TIME, WEEKLY, …); optional category (enum name, e.g. FOOD) |
MODIFY_CATEGORY | Scale or shift spending for a category between appliesFrom / appliesTo (multiplier and/or delta) |
MODIFY_SCHEDULED | SKIP or OVERRIDE_AMOUNT on a scheduled transaction (scheduledTransactionId, optional occurrenceDate) |
Each adjustment has enabled, name, and appliesToScenarios (BASE, OPTIMISTIC, PESSIMISTIC).
Response extras
| Field | When present |
|---|---|
budgetImpact | Per active budget + unbudgeted bucket: baseline vs scenario spend, verdict (OVER, TIGHT, UNDER, …) |
baselinePoints | compareToBaseline == true |
compareToBaseline | Echo of request flag |
Affordability
POST .../affordability builds a one-time NEW_TRANSACTION expense (inlineAdjustments), runs prediction with compareToBaseline, and returns AffordabilityResult:
| Verdict | Meaning (precedence order) |
|---|---|
BREAKS_BUDGET | Any budget impact verdict == OVER |
GOES_NEGATIVE | Any projected balance < 0 |
RISKY | Any point below safetyFloor |
TIGHT | Any budget impact verdict == TIGHT (≥ 85% of cap) |
OK | Otherwise |
Includes endingBalanceBaseline, endingBalanceWithPurchase, worstDipDate / worstDipBalance, budgetImpact, dangerEvents, explanation, and trimmed points / baselinePoints (~30) for the affordability mini-chart.
Desktop UI
Entry: Finance → Balance Prediction (BalancePredictionContent).
| UI area | Component | Behavior |
|---|---|---|
| Header | BalancePredictionHeader | Date range pill → DateRangePickerDialog; granularity chip; refresh; Settings cog → PredictionSettingsDialog; close |
| Action bar | BalancePredictionActionBar | Toggle Scenarios sidebar (badge = enabled count); + Adjustment; Can I afford…?; link to periodic spends count when non-zero |
| Scenarios sidebar | ScenariosPanel | Cards with color, description, pinned, adjustment count, kebab (edit/duplicate/delete); Session adjustments + save-as-scenario; empty state CTA |
| Chart | ChartContentArea / BalanceChart | Summary stats + chart; dashed baseline when baselinePoints present |
| Insights | InsightsSection | Collapsible: safe-to-spend, danger, goal, budget impact (global), category forecasts |
| Point details | PointDetailsPanel | Slide-in on point select |
| Settings | PredictionSettingsDialog | Data sources, risk thresholds, uncertainty, starting balance override, category volatility, periodic spends (save-on-change, debounced PUT) |
| Scenario editor | ScenarioEditorDialog | Create/edit scenario metadata + adjustment list |
| Add adjustment | ScenarioAdjustmentDialog | New / Modify category / Modify scheduled; target picker (session, scenario, new); date + frequency/action dropdowns |
| Affordability | AffordabilityDialog | Date picker; mini chart; verdict chip; save as scenario |
BalancePredictionViewModel debounces prediction reloads (~300 ms) and persists engine settings (~500 ms debounce) on knob changes. Session-only state (date range, selected scenarios, inline adjustments, baseAmount) is not persisted.
Persisted user settings (BalancePredictionUserSettings)
Stored in usersettings.balance_prediction_settings (migration V26__balance_prediction_settings.sql) as JSON:
| Field | Notes |
|---|---|
granularity, pageSize | Chart paging |
includeScheduledTransactions, includePastMonthBehavior | Data sources |
noisePercentage, amplitudeFactor | Uncertainty |
safetyFloor, overdraftThreshold | Risk |
categoryVolatilityOverrides | Per-category multipliers |
onboardingDismissed | First-run tip |
Not persisted: baseAmount, date range, scenarioIds, inlineAdjustments, periodicSpends (periodic spends remain per-session in filters until saved via settings dialog).
Default filters use compareToBaseline = true when scenarios or inline adjustments are active.
Category inputs
All category pickers on this screen use CategoryDropdown (shared/.../finance/ui/components/CategoryDropdown.kt) backed by the Category enum. Volatility overrides and affordability use the same control; category keys sent to the API are enum names (FOOD, DONATIONS, …).
Dialogs use PopupMaterialTheme so dropdown popups inherit dark/light theme on desktop (Compose Dialog windows do not inherit MaterialTheme by default).
Persistence
Scenarios
- Migration:
V25__balance_scenarios.sql - Entity:
balance_scenarios—adjustmentsstored as JSON text - Repository:
BalanceScenarioRepository— CRUD + soft delete
Engine settings (per user)
- Migration:
V26__balance_prediction_settings.sql— column onusersettings - Repository:
BalancePredictionSettingsRepository - Routes:
GET/PUT/finance/balance-prediction/settings
Key files
| Layer | Path |
|---|---|
| Models | shared/.../models/finance/BalancePrediction.kt, ScenarioAdjustment.kt |
| HTTP client | shared/.../services/finance/FinanceService.kt |
| Merger | server/.../service/ScenarioAdjustmentMerger.kt |
| Category apply | server/.../service/CategoryAdjustmentApplier.kt |
| Scheduled apply | server/.../service/ScheduledAdjustmentApplier.kt |
| Budget impact | server/.../service/BudgetImpactService.kt |
| Affordability | server/.../service/BalancePredictionAffordabilityService.kt |
| Engine | server/.../service/BalancePredictionService.kt |
| Desktop UI | composeApp/.../balanceprediction/*, BalancePredictionContent.kt |
| ViewModel | composeApp/.../viewmodels/BalancePredictionViewModel.kt |
Related docs
- balance-prediction-algorithm-analysis.md — volatility and 3-scenario math
- Prediction engine — pipeline and core API
- Finance feature guide — finance module overview