[WSAPI-10] Resolve security symbol + market price for held positions #8

Merged
chris merged 5 commits from feature/WSAPI-10 into main 2026-09-18 10:34:33 -06:00
Owner

Ticket

WSAPI-10 Resolve security symbol + market price for held positions

Summary

  • lib/wealthsimple/balances (WSAPI-4) returns raw { securityId, quantity } holdings with no ticker/symbol and no price, so /positions' quantities couldn't become a dollar value. This adds lib/wealthsimple/securities — fetchSecurityQuotes(securityIds) — which batches a securityId list through Wealthsimple's FetchIdentityPositions GraphQL query (ported verbatim from gboudreau/ws-api-python) and resolves each to { securityId, symbol, price, currency }.
  • No sync/cache wiring yet — this ticket deliberately produces plain data for a future sync/endpoint to consume, per the ticket's own scope.
  • The module's variable set (includeSecurity: true, first/cursor/aggregated) deliberately diverges from upstream's own usage of the query, so the security/stock/quoteV2 sub-shape it parses is unverified against any real Wealthsimple response — flagged in code (queries.ts and securities/index.ts) alongside the existing currency: 'CAD' assumption, for a downstream ticket exercising live data to confirm.
  • A pre-PR review (two independent agents) found and fixed a whole-batch-poisoning bug: stock and quoteV2.currency were non-nullable in the zod schema, so a single non-equity or no-currency security in a page threw for the entire batch instead of being dropped the way an unpriced security already was. Both are now nullable with a symmetric drop guard, plus a defensive dedupe-by-securityId guarding the unverified aggregated: true assumption. Covered by 10 tests (request shape, full-page mapping, four independent null-field drop cases, empty-list short-circuit, two-page pagination, dedupe).
  • lib/wealthsimple/CLAUDE.md and the root CLAUDE.md updated for the new operation module.

Test plan

  • bun run type-check
  • bun run lint
  • bun run format:check
  • bun test lib/wealthsimple/securities/.test.ts (10 pass)
  • Full suite (bun test): 318 pass / 1 fail — the one failure (lib/config/.test.ts) is pre-existing and unrelated to this branch (caused by this environment's local .env leaking a real WS_EMAIL into process.env; reproduced identically on main)

🤖 Generated with Claude Code

## Ticket [WSAPI-10](http://192.168.2.100:7123/home/browse/WSAPI-10/) Resolve security symbol + market price for held positions ## Summary - `lib/wealthsimple/balances` (WSAPI-4) returns raw `{ securityId, quantity }` holdings with no ticker/symbol and no price, so `/positions`' quantities couldn't become a dollar value. This adds `lib/wealthsimple/securities` — `fetchSecurityQuotes(securityIds)` — which batches a securityId list through Wealthsimple's `FetchIdentityPositions` GraphQL query (ported verbatim from `gboudreau/ws-api-python`) and resolves each to `{ securityId, symbol, price, currency }`. - No sync/cache wiring yet — this ticket deliberately produces plain data for a future sync/endpoint to consume, per the ticket's own scope. - The module's variable set (`includeSecurity: true`, `first`/`cursor`/`aggregated`) deliberately diverges from upstream's own usage of the query, so the `security`/`stock`/`quoteV2` sub-shape it parses is unverified against any real Wealthsimple response — flagged in code (`queries.ts` and `securities/index.ts`) alongside the existing `currency: 'CAD'` assumption, for a downstream ticket exercising live data to confirm. - A pre-PR review (two independent agents) found and fixed a whole-batch-poisoning bug: `stock` and `quoteV2.currency` were non-nullable in the zod schema, so a single non-equity or no-currency security in a page threw for the entire batch instead of being dropped the way an unpriced security already was. Both are now nullable with a symmetric drop guard, plus a defensive dedupe-by-`securityId` guarding the unverified `aggregated: true` assumption. Covered by 10 tests (request shape, full-page mapping, four independent null-field drop cases, empty-list short-circuit, two-page pagination, dedupe). - `lib/wealthsimple/CLAUDE.md` and the root `CLAUDE.md` updated for the new operation module. ## Test plan - [x] `bun run type-check` - [x] `bun run lint` - [x] `bun run format:check` - [x] `bun test lib/wealthsimple/securities/.test.ts` (10 pass) - [x] Full suite (`bun test`): 318 pass / 1 fail — the one failure (`lib/config/.test.ts`) is pre-existing and unrelated to this branch (caused by this environment's local `.env` leaking a real `WS_EMAIL` into `process.env`; reproduced identically on `main`) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Verbatim copy of ws-api-python's FetchIdentityPositions query, the one
operation that resolves a securityId to its ticker symbol and live market
price in a batch. No wiring yet — lib/wealthsimple/securities/ (next step)
is the operation module that calls it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fetchSecurityQuotes(securityIds) batches a securityId list through
FetchIdentityPositions and resolves each to { securityId, symbol, price,
currency }, closing the gap balances/ left: raw holdings had no way to
become a dollar value. Paginated like accounts/, tolerant of an unpriced
or delisted security like balances/ is tolerant of a missing balance key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Five operations exist now, not four; adds the securities module bullet
and the security-positions.json synthetic-fixture note.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes two whole-batch-poisoning correctness bugs found and empirically
verified by the review: `securitySchema.stock` and `quoteSchema.currency`
were both non-nullable, so a single non-equity/no-currency security in a
page threw for the entire fetchSecurityQuotes() batch instead of being
dropped like an unpriced one already was. Both are now nullable with a
symmetric drop guard, covered by new null-stock/null-currency/null-security
tests plus a dedupe-by-securityId test for the new defensive dedupe this
also adds (guarding the unverified `aggregated: true` assumption).

Also: fixes stubFetch's confusing failure on an unexpected extra request,
extracts the inline page size to a named POSITION_PAGE_SIZE constant,
rewords queries.ts's FETCH_IDENTITY_POSITIONS comment to stop overclaiming
fidelity to upstream's own usage (upstream never passes includeSecurity,
so the security/stock/quoteV2 sub-shape this module parses is unverified
against any real response), documents fetchSecurityQuotes' actual
"currently-held securities only" constraint and the positionsFor() caller
contract, and fixes three stale operation-count/consumer-list references
in CLAUDE.md/schema.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[WSAPI-10] Fix remaining CLAUDE.md doc-accuracy leftovers.
All checks were successful
server / check (pull_request) Successful in 25s
ed4a44f303
lib/wealthsimple/CLAUDE.md still listed numericAmountSchema/pageInfoSchema's
consumers without securities/, described fetchSecurityQuotes as reading
balances/'s raw holdings instead of positionsFor() output, and repeated the
upstream-fidelity overclaim about FETCH_IDENTITY_POSITIONS that was already
reworded away in queries.ts's own comment. Also fixes an ungrammatical
POSITION_PAGE_SIZE comment in securities/index.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
chris merged commit ba9b772fb7 into main 2026-09-18 10:34:33 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chris/wealthsimple-api!8
No description provided.