phase B (1/4): CSV parser + InstrumentMap (T212 shortcode → Yahoo ticker)
First two slices of the multi-user roadmap (Phase B). Validates the
core onboarding mechanic against the user's real T212 export before
paying any auth/tenancy tax.
CSV parser (app/services/csv_import.py):
- Header-name matched (survives T212 reordering columns between
exports), tolerant of UTF-8 BOM, dash/N/A/empty markers, thousand-
separator commas, blank rows, zero-quantity stubs, missing Total row.
- Returns ParsedPie(name, positions, invested, value, result) with
derived avg_price + current_price per share in account currency.
- 14 tests covering happy path on the real CSV + 13 edge cases.
InstrumentMap (migration 0006 + app/services/instrument_map.py):
- Catalogue table mapping T212 ticker → Yahoo ticker, populated by
sync_from_t212() against the dev's read-only API key. Manual rows
(manual=True) are protected from auto-overwrite.
- Pure t212_ticker_to_yahoo() handles both suffix forms: single
trailing exchange letter (l/a/p/d/m/s/...) and country code (US,
DE, FR, IT, CA, ...). All 13 of the user's holdings + 15 case-
coverage tests pass.
- Live sync against T212 ingests 17,050 instruments (~2.2% unmappable
on exotic exchanges; can extend the suffix map later).
- resolve_slice() picks the right listing per shortName using a
UK-friendly currency preference (GBX > GBP > EUR > USD). Resolved
correctly for all 13 of the user's positions, including TTE on
Paris vs the NYSE dual-listing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6dac8a2c7f
commit
16e9f5f0cc
7 changed files with 840 additions and 0 deletions
|
|
@ -187,6 +187,33 @@ class Position(Base):
|
|||
snapshot: Mapped[PortfolioSnapshot] = relationship(back_populates="positions")
|
||||
|
||||
|
||||
class InstrumentMap(Base):
|
||||
"""Maps T212's tickers/shortnames to Yahoo Finance tickers so we can
|
||||
refresh prices via Yahoo after a user uploads a T212 pie CSV.
|
||||
|
||||
Synced periodically from T212's /equity/metadata/instruments endpoint
|
||||
via the admin's read-only API key. Each row is one T212 listing.
|
||||
Multiple rows can share a shortName (e.g. SHEL on LSE in GBX vs
|
||||
SHEL on NYSE in USD); the resolver picks the right one per user."""
|
||||
__tablename__ = "instrument_map"
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
t212_ticker: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
t212_shortname: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
yahoo_ticker: Mapped[str | None] = mapped_column(String(32))
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
currency: Mapped[str | None] = mapped_column(String(8))
|
||||
isin: Mapped[str | None] = mapped_column(String(16))
|
||||
instrument_type: Mapped[str | None] = mapped_column(String(16))
|
||||
manual: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
last_verified_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("t212_ticker", name="uq_imap_t212_ticker"),
|
||||
Index("ix_imap_shortname", "t212_shortname"),
|
||||
Index("ix_imap_isin", "isin"),
|
||||
)
|
||||
|
||||
|
||||
class JobRun(Base):
|
||||
"""One row per scheduled-job invocation; powers /api/health + the ops footer."""
|
||||
__tablename__ = "job_runs"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue