fidelity: push per-fund manual snapshot instead of gains-offset DEPOSIT

PlanViewer's DisplayValuation.action JSON already gives us current
fund units + unit price; we were parsing it and throwing it away,
emitting only a single 'unrealised-gains-offset' DEPOSIT to make
Wealthfolio's totals match the dashboard. That hack double-counted
the gain as a cash contribution, hiding £35k of pension growth from
every contribution/growth/ROI panel.

New flow:
- FidelityPlanViewerProvider exposes last_holdings + last_total_contribution
  after fetch() drains.
- fidelity-ingest CLI converts to a ManualSnapshotPayload (cost basis
  allocated proportionally by current fund value share) and posts to
  WF /api/v1/snapshots/import. WF auto-creates unknown fund symbols
  with kind=INVESTMENT, quoteMode=MANUAL, quoteCcy=GBP.
- The gains-offset emission is removed entirely. Historical offset
  rows already in WF are corrected at the dashboard layer by the
  dav_corrected view shipped in infra@2841347e.

WealthfolioSink gains push_manual_snapshots() + ManualSnapshotPayload /
SnapshotPosition wire types. 11 sink tests (3 new) + 9 fidelity
provider tests (2 changed, 1 new) all green; mypy + ruff clean.
This commit is contained in:
Viktor Barzin 2026-05-16 13:56:25 +00:00
parent 5adc4a7ba4
commit cb159e17d9
5 changed files with 339 additions and 60 deletions

View file

@ -438,6 +438,15 @@ def fidelity_ingest(
sys.exit(2)
async def _run() -> None:
from datetime import date as _date_t
from broker_sync.providers.fidelity_planviewer import (
ACCOUNT_ID as FID_ACCOUNT_ID,
)
from broker_sync.providers.fidelity_planviewer import (
fidelity_holdings_to_snapshot,
)
sink = WealthfolioSink(
base_url=wf_base_url,
username=wf_username,
@ -455,12 +464,31 @@ def fidelity_ingest(
result = await sync_provider_to_wealthfolio(
provider=provider, sink=sink, dedup=dedup, since=since,
)
# PlanViewer has no historical per-fund unit-price feed, so
# the Activity stream above only carries cash flows. The
# current-pot fund positions captured in the same scrape get
# pushed via /api/v1/snapshots/import so per-fund quantity +
# cost basis land in WF (and propagate to the wealth
# dashboard's Positions table via pg-sync).
snapshot_imported = 0
if provider.last_holdings:
snapshot = fidelity_holdings_to_snapshot(
holdings=provider.last_holdings,
total_real_contribution=provider.last_total_contribution,
as_of=_date_t.today(),
)
if snapshot is not None:
push_result = await sink.push_manual_snapshots(
account_id=FID_ACCOUNT_ID, snapshots=[snapshot],
)
snapshot_imported = int(push_result.get("snapshotsImported", 0))
finally:
await sink.close()
typer.echo(f"fidelity-ingest: fetched={result.fetched} "
f"new={result.new_after_dedup} "
f"imported={result.imported} "
f"failed={result.failed}")
f"failed={result.failed} "
f"snapshots={snapshot_imported}")
if result.failed > 0:
sys.exit(1)