Investment Account Reconciliation#
Investment account reconciliation is an anchor-point approach that lets users periodically record a total portfolio value instead of entering every trade. When a user submits a total balance for a given date, the system automatically splits it into cash and holdings components — making it a practical alternative to trade-by-trade entry for manual investment accounts.
How It Works#
A reconciliation creates (or updates) a Valuation entry with kind: "reconciliation" in the entries table . This valuation acts as an anchor point: the ForwardCalculator reads it as an absolute balance override for that date rather than deriving balance from accumulated flows .
Cash/holdings split for investment accounts:
When the user sets a total balance, the system calculates the implied cash balance by subtracting the pre-existing non-cash (holdings) value from the new total :
new_cash_balance = total_balance − existing_non_cash_balance
This preserves the current market value of holdings and only adjusts the cash component, so a user entering "my portfolio is worth $50,000 today" doesn't inadvertently overwrite individually-tracked positions.
Key Classes#
| Class / Module | Role |
|---|---|
Account::ReconciliationManager | Core logic: prepares/saves the Valuation, computes derived_cash_balance, and fires GoalPledge::Reconciler with the balance delta |
Account::Reconcileable | Concern mixed into Account; exposes create_reconciliation and update_reconciliation, then calls sync_later on success |
Valuation | AR model stored in entries table with kind enum: reconciliation, opening_anchor, current_anchor |
Api::V1::ValuationsController | REST API surface (POST /api/v1/valuations); delegates directly to account.create_reconciliation |
Write Flow#
- API (
POST /api/v1/valuationsorPATCH /api/v1/valuations/:id) callsaccount.create_reconciliation/account.update_reconciliation. Account::Reconcileabledelegates toReconciliationManager#reconcile_balance.ReconciliationManager:- Finds or builds a
Valuationentry for the date (kind: "reconciliation"). - Saves it with the user-supplied
amount. - Computes
derived_cash_balanceby looking up the currentBalancerow for that date and subtracting non-cash holdings . - Fires
GoalPledge::Reconcilerwith the balance delta . Note: investment accounts usetransferpledge kind, so the delta is not consumed as a "deposit" .
- Finds or builds a
sync_lateris enqueued, which triggersBalance::ForwardCalculatorto reproject the account's balance history with the new valuation anchor .
Valuation as Override in Balance Projection#
The ForwardCalculator gives valuation entries special treatment: on any date where a Valuation row exists, the calculated ending balance is taken directly from the valuation amount rather than derived from daily flows . This means each reconciliation resets the absolute portfolio value, and subsequent days continue forward from that new anchor until the next reconciliation or entry.
API Notes#
POST /api/v1/valuations requires account_id, amount, and date . It supports an upsert flag : when upsert=true and a valuation already exists for the given date, the controller returns 200 OK instead of 201 Created; the write path is identical either way — ReconciliationManager finds and updates the existing entry . PATCH /api/v1/valuations/:id requires both amount and date when updating the reconciliation value .
Related Sources#
app/models/account/reconciliation_manager.rb— full reconciliation logic includingderived_cash_balanceapp/models/account/reconcileable.rb— account concernapp/models/valuation.rb—Valuationmodel withkindenumapp/controllers/api/v1/valuations_controller.rb— REST API- Balance History System — how
ForwardCalculatorconsumes valuation overrides and the full balance projection pipeline