Skip to content

feat(tinvest): read a broker account's positions, against a captured response - #96

Closed
foxcool wants to merge 1 commit into
mainfrom
feature/tgrg-broker-read
Closed

foxcool wants to merge 1 commit into
mainfrom
feature/tgrg-broker-read

Conversation

@foxcool

@foxcool foxcool commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The adapter knew four API paths and all four were about quotes, so nothing could read a position; the catalogue loaded only shares and etfs, so a bond had no venue to take a market from.

Adds GetAccounts, GetPortfolio, Bonds, and entity.BrokerSyncer as a third sync shape beside WalletSyncer and ExchangeSyncerExchangeBalance is Symbol/Amount/Decimals and loses the three things a broker line needs: its instrument id, the currency of that row, and the instrument type.

First step of the chain behind stocks reading $0.00 while the broker holds ≈389 808 ₽. Supersedes vcy0.

The capture corrected four things

Committed to testdata/ — sanitised (pseudonymous account ids, rounded money, blanked names; structure untouched). Composition matches the 2026-08-27 measurement exactly: 34 share, 7 currency, 4 etf, 2 bond.

A cash line never states its own currency. currentPrice.currency is rub on every one of them, including the dollars, because that field says what the position is worth in. This branch read the code from there until the capture landed — booking 0.88 dollars as 0.88 roubles, and merging dollars, roubles and euros into one row. The currency is in the ticker: USD000UTSTOM, RUB000UTSTOM, EUR_RUB__TOM_CETS. The unit test missed it because it used a rouble line, where the two agree.

Partial blocking does not exist here. All seven blocked positions carry blockedLots = 0 — the broker says that a line is restricted, never how much. splitByLiquidity still handles a quantity (the field exists; ignoring it silently is its own bug), but a test records that the branch is unexercised, so nobody reads it as observed behaviour.

The portfolio response does carry a venue, in classCode — the brief said it did not. That makes it the fallback ahead of inferring a market from the currency: DE0005190003 trades on SPBXM_OTC quoted in euros, which no currency rule places correctly.

The id field is not always a FIGI. It is a real one, a synthetic T-Invest id (TCS00A1055Y4), or — for the four blocked foreign holdings with no FIGI at all — the ISIN repeated in the ticker. All three are stable within the provider, which is what the asset_external_refs namespace makes sufficient.

Also confirmed: the token is read-only on all three accounts — the assumption the whole "dev talks to the live API" decision rests on.

Design notes

The market is resolved in the adapter (venue is provider knowledge) by inverting the existing venues table rather than writing a second copy: two hand-kept mappings would eventually disagree, and the disagreement would surface as one company owning two asset rows.

Quantity is read from Quotation at scale 9 — a property of the wire format, not of the instrument. Inferring it ("shares are whole numbers") is how a fractional lot becomes a rounding error nobody sees; the capture has a 0.88 cash line.

Unresolved positions are counted, not dropped (entity.BrokerSkips), and the one place this adapter guesses — an instrument absent from the catalogue — is counted separately in DefaultedMarket so the guess is never silent. Repair path is uusf.

Verification

make check exit 0. Reverting the bonds load, the liquidity split, the cash exception, or the ticker-derived currency each fails its own test.

Capture-driven tests assert the wire shape; mapping rules are exercised against a synthetic universe, because a real capture cannot carry a catalogue of thousands of rows.

Not in scope

Nothing calls this yet — SyncAccount still refuses a broker account (lab0), and the scheduled sweep filters it out in SQL (c1nz).

🤖 Generated with Claude Code

https://claude.ai/code/session_015TZ3u7wZtv1Dwe24Kz7FPV

…response

The adapter knew four API paths and all four were about quotes, so nothing
could read a position; the instrument catalogue loaded only shares and etfs, so
a bond had no venue to take a market from. GetAccounts, GetPortfolio and Bonds
join them, and entity.BrokerSyncer joins WalletSyncer and ExchangeSyncer as a
third shape — ExchangeBalance is Symbol/Amount/Decimals and loses the three
things a broker line needs: its instrument id, the currency of THAT row, and
the instrument type that decides whether it can be valued at all.

The market comes from the venue, resolved in the adapter because that is
provider knowledge, and by inverting the existing venues table rather than
writing a second copy of it — two hand-kept mappings would eventually disagree,
and the disagreement would surface as one company owning two asset rows.

THE CAPTURE CORRECTED FOUR THINGS, three of which were already written down as
facts. It is committed to testdata/ (sanitised: pseudonymous account ids,
rounded money, blanked names; structure untouched) and its composition matches
the 2026-08-27 measurement exactly — 34 share, 7 currency, 4 etf, 2 bond.

A cash line never states its own currency. currentPrice.currency is "rub" on
every one of them, including the dollars, because that field says what the
position is WORTH IN. Reading the code from there — which this branch did until
the capture landed — books 0.88 dollars as 0.88 roubles, and merges dollars,
roubles and euros into one row. The currency is in the ticker: USD000UTSTOM,
RUB000UTSTOM, EUR_RUB__TOM_CETS. The unit test missed it because it used a
rouble line, where the two agree.

Partial blocking does not exist here. All seven blocked positions carry
blockedLots = 0: the broker says THAT a line is restricted, never how much.
splitByLiquidity still handles a quantity, because the field exists and
ignoring it silently is its own bug, but a test now records that the branch is
unexercised so nobody reads it as observed behaviour.

The portfolio response does carry a venue, in classCode. That makes it the
fallback for an instrument the catalogue does not hold, ahead of inferring one
from the currency: DE0005190003 trades on SPBXM_OTC quoted in euros, which no
currency rule places correctly.

And the id field is not always a FIGI. It is a real one, or a synthetic
T-Invest id (TCS00A1055Y4), or — for the four blocked foreign holdings that
have no FIGI at all — the ISIN, repeated in the ticker. All three are stable
within this provider, which is what the asset_external_refs namespace makes
sufficient.

The token is confirmed read-only on all three accounts, which is the
assumption the whole "dev talks to the live API" decision rests on.

make check exit 0. Reverting the bonds load, the liquidity split, the cash
exception, or the ticker-derived currency each fails its own test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TZ3u7wZtv1Dwe24Kz7FPV
@foxcool foxcool closed this Aug 31, 2026
@foxcool
foxcool deleted the feature/tgrg-broker-read branch August 31, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant