fidelity: replace snapshot-push with delta gains-offset DEPOSITs
Per-fund snapshot import landed quantities but dropped cost basis + needed a separate quote-push path we never identified. Snapshotting also collided with WF's own TOTAL aggregation and ZEROED the Fidelity cash balance. Simpler plan: each monthly scrape emits a single DEPOSIT (or WITHDRAWAL on a market drop) sized to the delta between the live PlanViewer pot value and Wealthfolio's running total. dav_corrected PG view continues to subtract these offsets from net_contribution so the dashboard Growth/ROI math stays right. - New gains_offset_delta_activity() — current_gain - prior_offset. - New WealthfolioSink.cumulative_amount_with_notes_prefix() — sums the existing fidelity-planviewer:unrealised-gains-offset DEPOSITs in WF so we know what's already been emitted. - CLI runs sync_provider_to_wealthfolio first (cash flows), then computes + emits the delta via import_activities. - 4 new provider tests for the delta logic; full suite (144 + 1 skipped) green; mypy + ruff clean. The old fidelity_holdings_to_snapshot helper + push_manual_snapshots sink method stay for future use but are no longer called.
This commit is contained in:
parent
c9c0310733
commit
98c4729622
4 changed files with 183 additions and 35 deletions
|
|
@ -17,6 +17,7 @@ _ACCOUNTS_PATH = "/api/v1/accounts"
|
|||
_IMPORT_CHECK = "/api/v1/activities/import/check"
|
||||
_IMPORT_REAL = "/api/v1/activities/import"
|
||||
_SNAPSHOTS_IMPORT = "/api/v1/snapshots/import"
|
||||
_ACTIVITIES_SEARCH = "/api/v1/activities/search"
|
||||
|
||||
|
||||
class WealthfolioError(Exception):
|
||||
|
|
@ -266,6 +267,54 @@ class WealthfolioSink:
|
|||
assert isinstance(got, list)
|
||||
return [r for r in got if isinstance(r, dict)]
|
||||
|
||||
# -- activity lookups --
|
||||
|
||||
async def cumulative_amount_with_notes_prefix(
|
||||
self,
|
||||
account_id: str,
|
||||
notes_prefix: str,
|
||||
) -> Decimal:
|
||||
"""Sum the amount of DEPOSIT/WITHDRAWAL activities whose notes start
|
||||
with ``notes_prefix``, signed (deposits positive, withdrawals negative).
|
||||
|
||||
Used by the Fidelity provider to compute the delta gains-offset:
|
||||
``current_gain - cumulative_existing_offset`` becomes the new
|
||||
DEPOSIT to emit on each monthly run.
|
||||
"""
|
||||
try:
|
||||
resp = await self._request(
|
||||
"POST", _ACTIVITIES_SEARCH,
|
||||
json={"accountIds": [account_id], "page": 1, "pageSize": 500},
|
||||
)
|
||||
except Exception:
|
||||
return Decimal(0)
|
||||
if resp.status_code >= 400:
|
||||
return Decimal(0)
|
||||
payload = resp.json()
|
||||
rows = payload.get("data", payload) if isinstance(payload, dict) else payload
|
||||
if not isinstance(rows, list):
|
||||
return Decimal(0)
|
||||
total = Decimal(0)
|
||||
for r in rows:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
notes = r.get("comment") or r.get("notes") or ""
|
||||
if not isinstance(notes, str) or not notes.startswith(notes_prefix):
|
||||
continue
|
||||
amt_raw = r.get("amount")
|
||||
if amt_raw is None:
|
||||
continue
|
||||
try:
|
||||
amt = Decimal(str(amt_raw))
|
||||
except Exception:
|
||||
continue
|
||||
atype = (r.get("activityType") or r.get("activity_type") or "").upper()
|
||||
if atype == "WITHDRAWAL":
|
||||
total -= amt
|
||||
else:
|
||||
total += amt
|
||||
return total
|
||||
|
||||
# -- manual holdings snapshots --
|
||||
|
||||
async def push_manual_snapshots(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue