Include investment holding values in net worth and account balances

- Net worth report, balance sheet, and daily snapshots now add holding
  market values (falling back to cost basis) to investment-type account
  balances (investment, pension, stocks_shares_isa, crypto_wallet)
- Accounts list shows total value for investment accounts with a
  breakdown line ("£X cash + £Y holdings") when both are non-zero
- Add Holding modal gains a "Debit account for this purchase" toggle
  that creates a matching withdrawal transaction, enabling proper cash
  flow tracking for users who fund their brokerage via transfer first
- Both simple (holdings-only) and full cash-flow workflows produce
  correct net worth figures without double-counting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-23 10:10:19 +00:00
parent cdc1e67321
commit 312594f3d2
5 changed files with 139 additions and 11 deletions

View file

@ -142,6 +142,51 @@ async def get_portfolio(db: AsyncSession, user_id: uuid.UUID, base_currency: str
)
async def get_portfolio_value_by_account(
db: AsyncSession, user_id: uuid.UUID, base_currency: str
) -> dict[uuid.UUID, Decimal]:
"""Return total holding value (in base_currency) keyed by account_id."""
result = await db.execute(
select(InvestmentHolding).where(
InvestmentHolding.user_id == user_id,
InvestmentHolding.quantity > 0,
)
)
holdings = result.scalars().all()
assets: dict[uuid.UUID, Asset] = {}
for h in holdings:
if h.asset_id not in assets:
asset = await _get_asset(db, h.asset_id)
if asset:
assets[h.asset_id] = asset
pairs_needed: set[tuple[str, str]] = set()
for h in holdings:
asset = assets.get(h.asset_id)
if not asset:
continue
if asset.currency != h.currency:
pairs_needed.add((asset.currency, h.currency))
if h.currency != base_currency:
pairs_needed.add((h.currency, base_currency))
fx_rates: dict[tuple[str, str], Decimal] = {}
for from_curr, to_curr in pairs_needed:
fx_rates[(from_curr, to_curr)] = await _fetch_fx_rate(db, from_curr, to_curr)
totals: dict[uuid.UUID, Decimal] = {}
for h in holdings:
asset = assets.get(h.asset_id)
if not asset:
continue
r = _holding_to_response(h, asset, fx_rates)
value = r.current_value if r.current_value is not None else r.cost_basis_total
to_base = fx_rates.get((h.currency, base_currency), Decimal("1")) if h.currency != base_currency else Decimal("1")
totals[h.account_id] = totals.get(h.account_id, Decimal("0")) + value * to_base
return totals
async def get_holding(db: AsyncSession, user_id: uuid.UUID, holding_id: uuid.UUID) -> InvestmentHolding | None:
result = await db.execute(
select(InvestmentHolding).where(