feat(performance): investment performance vs Moscow Exchange index (WealthLedger/main#54) #200

Merged
codex-bot merged 2 commits from feature/54-investment-performance-vs-index into develop 2026-11-08 16:45:30 +03:00
Owner

Summary

Adds GET /api/v1/brokerage-accounts/{accountId|all}/performance:

  • profit in ₽;
  • XIRR return over all time;
  • "the same money in the Moscow Exchange index" (MCFTRR) and outperformance; IMOEX as reference only.

Stores daily MOEX index closes locally.

Linked Issue

WealthLedger/main#54 · spec: WealthLedger/specs#85 (change 2026-11-07-investment-performance-vs-index)

Changes

  • market-index:
    • MarketIndexDailyClose, MarketIndexSyncState (additive migration);
    • MoexClient.indexHistory with strict pagination; one bad page means failure, no partial saves;
    • confirmed coverage range;
    • backward extension fetched immediately, tail and retry-after-failure at most hourly;
    • single-flight;
    • logs integration.moex.index_history.*.
  • performance:
    • external flows: RUB INPUT*/OUTPUT* only, cut off at fetchedAt;
    • coverage check up to the valuation instant;
    • reasons: HISTORY_START_UNKNOWN, OPERATIONS_HISTORY_INCOMPLETE, PORTFOLIO_VALUATION_INCOMPLETE, NON_RUB_ACTIVITY, UNVALUED_SECURITIES_TRANSFER, UNCONFIRMED_TRANSFER, ACCOUNT_VALUATION_DATES_DIFFER, NO_SOLUTION, AMBIGUOUS_RETURN, INDEX_HISTORY_UNAVAILABLE.
  • XIRR: bisection with a Descartes uniqueness check. More than one sign change returns AMBIGUOUS_RETURN (conservative, owner decision).
  • Period: starts at the first external flow (owner decision); under 365 days → basis=period.
  • Shadow portfolio: exact decimal units at the flow-day close (previous trading day on weekends); a negative position → index unavailable.
  • isCompleteValuation: extracted into portfolio domain and reused by value history.

Verification

  • npm run verify: 52 suites / 364 tests passed, lint and typecheck clean, nest build ok.
  • npm run openapi:check: matches runtime; npm run test:openapi-contract: 5/5.
  • Migration applied on a disposable SQLite DB (prisma migrate deploy); migrate diff from migrations to schema is empty.
  • Reference cases in tests: 10% over 730 days; 1000→1100 index gives 110 000 ₽; Friday close for a Saturday flow; two-root case → AMBIGUOUS_RETURN; period from first deposit (+10% over 100 days).
  • Independent review done; findings fixed (coverage throttling, unpublished tail close, period basis, negative units, missing-amount reason).

Notes

  • The regenerated openapi.json also drops the stale sync=observe query and tbank-sync/{id} that were already gone from runtime code; the frontend snapshot was simply behind.
  • Before closing the Issue, transfers must be checked manually on real data: UNCONFIRMED_TRANSFER excludes accounts until a confirmed link exists.
## Summary Adds `GET /api/v1/brokerage-accounts/{accountId|all}/performance`: - profit in ₽; - XIRR return over all time; - "the same money in the Moscow Exchange index" (MCFTRR) and outperformance; IMOEX as reference only. Stores daily MOEX index closes locally. ## Linked Issue WealthLedger/main#54 · spec: WealthLedger/specs#85 (change `2026-11-07-investment-performance-vs-index`) ## Changes - **`market-index`**: - `MarketIndexDailyClose`, `MarketIndexSyncState` (additive migration); - `MoexClient.indexHistory` with strict pagination; one bad page means failure, no partial saves; - confirmed coverage range; - backward extension fetched immediately, tail and retry-after-failure at most hourly; - single-flight; - logs `integration.moex.index_history.*`. - **`performance`**: - external flows: RUB INPUT*/OUTPUT* only, cut off at `fetchedAt`; - coverage check up to the valuation instant; - reasons: `HISTORY_START_UNKNOWN`, `OPERATIONS_HISTORY_INCOMPLETE`, `PORTFOLIO_VALUATION_INCOMPLETE`, `NON_RUB_ACTIVITY`, `UNVALUED_SECURITIES_TRANSFER`, `UNCONFIRMED_TRANSFER`, `ACCOUNT_VALUATION_DATES_DIFFER`, `NO_SOLUTION`, `AMBIGUOUS_RETURN`, `INDEX_HISTORY_UNAVAILABLE`. - **XIRR**: bisection with a Descartes uniqueness check. More than one sign change returns `AMBIGUOUS_RETURN` (conservative, owner decision). - **Period**: starts at the first external flow (owner decision); under 365 days → `basis=period`. - **Shadow portfolio**: exact decimal units at the flow-day close (previous trading day on weekends); a negative position → index unavailable. - **`isCompleteValuation`**: extracted into portfolio domain and reused by value history. ## Verification - `npm run verify`: 52 suites / 364 tests passed, lint and typecheck clean, `nest build` ok. - `npm run openapi:check`: matches runtime; `npm run test:openapi-contract`: 5/5. - Migration applied on a disposable SQLite DB (`prisma migrate deploy`); `migrate diff` from migrations to schema is empty. - Reference cases in tests: 10% over 730 days; 1000→1100 index gives 110 000 ₽; Friday close for a Saturday flow; two-root case → `AMBIGUOUS_RETURN`; period from first deposit (+10% over 100 days). - Independent review done; findings fixed (coverage throttling, unpublished tail close, period basis, negative units, missing-amount reason). ## Notes - The regenerated `openapi.json` also drops the stale `sync=observe` query and `tbank-sync/{id}` that were already gone from runtime code; the frontend snapshot was simply behind. - Before closing the Issue, transfers must be checked manually on real data: `UNCONFIRMED_TRANSFER` excludes accounts until a confirmed link exists.
codex-bot added 1 commit 2026-11-08 14:22:20 +03:00
ksv741 added 1 commit 2026-11-08 16:26:12 +03:00
feat(performance): add investment performance vs index endpoint (WealthLedger/main#54)
Backend verify / commits (pull_request) Successful in 1m34s
Backend verify / security (pull_request) Successful in 1m11s
Backend verify / verify (pull_request) Successful in 6m47s
42b1d60fba
ksv741 force-pushed feature/54-investment-performance-vs-index from fc0da12032 to 42b1d60fba 2026-11-08 16:26:12 +03:00 Compare
codex-bot merged commit fee61537b6 into develop 2026-11-08 16:45:30 +03:00
codex-bot deleted branch feature/54-investment-performance-vs-index 2026-11-08 16:45:30 +03:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: WealthLedger/backend#200