๐ฎ๐น Borsa Italiana Provider (borsa_italiana)
The Borsa Italiana provider fetches financial data from borsaitaliana.it using the borsa-italiana-scraping library. It supports stocks, bonds (including BTP), ETFs listed on Borsa Italiana markets (MTA, MOT, ETFPlus), and funds/SICAVs exposed on Borsa fund-detail pages.
โ๏ธ How it Works
- Identifier: An ISIN code (e.g.,
IT0003128367for ENEL S.p.A.) for listed instruments. - Identifier Types:
ISINfor listed instruments. Mutual funds are priced by a Borsa internal code carried inprovider_params.codice_fondo(see Mutual funds below); the asset identifier is still the real ISIN when the fund page exposes one, otherwise anOTHERidentifier is used. provider_params:languageโ optional ("en"or"it", default"en"). Controls the language of asset names, metadata descriptions, and the provider URL.codice_fondoโ optional Borsa internal fund code (e.g.2FADB602822). When present, current/historical value use the fund NAV path instead of the market API.micโ optional market MIC (e.g.ETLXfor EuroTLX,MOTXfor MOT). Auto-filled from the search result's own link; routes the instrument page (get_asset_url) and metadata to the right market.platformโ optional trading platform (e.g.TLXfor EuroTLX). Required by some markets so the universalsearch/scheda.htmlURL redirects to the real market page.urlโ not a form field (absent fromparams_schema): set automatically to the instrument's own page address โ the site's searchlink, or the pageresolve_urlactually loaded (scheda.url_pagina).get_asset_urland metadata prefer it, so a EuroTLX link keeps working even whenplatformis missing (e.g. an asset created while the site search had the ISIN de-indexed).
๐งญ Market routing (mic / platform)
Borsa Italiana lists the same instrument families across several markets (MTA, MOT, ExtraMOT, ETFplus, SeDeX, MIV, EuroTLX, GEM). Most resolve through the universal search/scheda.html?code={ISIN} URL, but EuroTLX does not โ it requires mic=ETLX&platform=TLX or the URL stays on a dead generic page. The provider takes mic/platform from the site's own search link (never a hardcoded map), stores them in provider_params, and reuses them everywhere:
- Search results carry
provider_params: {language, mic, platform?, url}(url= the site's ownlink) โ so the URL and pricing are correct by construction for any market, present or future. - Dead-result filter: a non-fund result with no parseable
micin its link is skipped (never emit a result whose URL would be the unresolved/search/page). Indices (Indice/Index) are excluded โ they are benchmarks, not purchasable instruments. - History auto-discovery (library โฅ 0.3.0): the chart API only knows XMIL/ETLX; if an ISIN is unknown to XMIL and no exchange is passed, the library discovers the MIC via the site search and retries. This self-heals legacy assets saved before
micwas propagated. UNSUPPORTED_PAGE: when even mic/platform can't resolve a parseable market page (a family we don't handle yet), the provider raisesUNSUPPORTED_PAGEwith a message inviting the user to open a GitHub issue โ instead of a cryptic parse error.
๐ฑ Currency
A listed instrument is priced in its trading currency on Borsa Italiana: EUR for most listings, but e.g. USD for FX-denominated EuroTLX bonds such as the US Treasury US912810TU25. One source serves both sides: the metadata currency is the currency the prices carry.
- Prices (
get_current_value,get_history_value): the currency of thegrafici.borsaitaliana.itchart-API reply (ottieni_prezzo_corrente/ottieni_storico),EURwhen the reply carries none. History goes through_storico_con_mic_retry: default route first, then, onStrumentoNonTrovato, the storedmicas the chart-API exchange. - Metadata and
resolve_url(listed instruments): the currency is not read from the scheda page. ETF/ETC scheda pages often show only the fund's denomination currency (ยซValuta di Denominazione USDยป), while the instrument trades โ and is priced โ in EUR; read off the scheda, it gave USD metadata over EUR prices (e.g. Invesco Physical Gold ETCIE00B579F325, iShares Core MSCI WorldIE00B4L5Y983, iShares Core S&P 500IE00B5BMR087). The module helper_price_api_currency(identifier, mic)asks the chart API instead: the same_storico_con_mic_retrycall, the sameEURdefault. - Currency unknown: when that chart-API call fails (any exception),
fetch_asset_metadataleavescurrencyunset in the returnedFAAssetPatchItem(patch contract: present, even ifNoneโ update/blank; absent โ keep), so a metadata refresh keeps the stored asset currency instead of blanking it.resolve_urlrows carrycurrency: None. - Search: rows for listed instruments carry
currency: Noneโ thecercapayload has no currency; the metadata fetch fills it. - Funds (
codice_fondo): unchanged โ the currency comes from the fund page (DatiFondo.valuta, defaultEUR).
Note (library โฅ 0.3.2): the scheda parser no longer takes the denomination row for the trading currency โ ยซValutaยป / ยซCurrencyยป match only as an exact label โ and exposes the denomination separately as SchedaStrumento.valuta_denominazione, which the plugin does not use. The fix still matters for current prices: ottieni_prezzo_corrente falls back to scraping the scheda when its chart-API read fails.
๐ฐ Current Value (get_current_value)
- Funds (when
provider_params.codice_fondois set): returns the fund NAV only if the published NAV is dated today. A fund NAV is published once per day with a lag, so exposing a stale NAV as the "current" value would misstate the portfolio. When the NAV date โ today, the provider raisesNO_DATA: no price is stored for today, and valuation carries the latest observation forward through the unified price resolver (backend/app/services/financial_math/price_marks.py) โ the last NAV point, or a more recent trade-derived mark (LAST_TRADE_PRICE). There is no separate last-buy fallback. - Listed instruments: uses
ottieni_prezzo_corrente(isin, mic=โฆ, platform=โฆ)from the scraping library. If it raisesStrumentoNonRisolto/StrumentoNonTrovatoand amicis stored, it retries once withexchange=mic(EuroTLX answers under its own exchange code, equal to the MIC). - Returns
FACurrentValuewith price, date, currency, and source ("Borsa Italiana").
๐ Historical Data (get_history_value)
- Funds (when
provider_params.codice_fondois set): the fund page exposes only the latest NAV (no series), dated to the day it actually refers to (never today). The provider returns a single price point at that real NAV date; the core backward-fills the gaps up to the requested range. Out-of-range NAV dates yield no points. - Listed instruments: uses
ottieni_storico(isin, periodo)from thegrafici.borsaitaliana.itJSON API.- Returns full OHLCV data (open, high, low, close, volume) for each trading day.
- Period selection: The API uses fixed windows (
1M,3M,6M,1Y,3Y,5Y,MAX). The provider automatically selects the smallest period that covers the requestedstart_date..end_daterange. - Results are filtered in-memory to the exact requested date range.
- The core handles gap filling (weekends, holidays) โ the plugin returns only actual trading days.
๐งพ Mutual funds (NAV by internal code)
Mutual funds / SICAVs are not on the XMIL market API, so ottieni_storico / ottieni_prezzo_corrente cannot price them. Their only public NAV source is the fund detail page /borsa/fondi/dettaglio/{code}.html, addressed by a Borsa internal code (2FADBโฆ) that is not the ISIN. The scraping library exposes:
ottieni_dati_fondo(codice, sessione)โ fetch NAV, currency, NAV date (dd/mm/yy), name, and the real ISIN extracted from the page (DatiFondo).DatiFondoalso carries the descriptive sections scraped from the page โcaratteristiche,societa_gestione,costi(each alabel โ valuedict,N.D.placeholders dropped) โ consumed byfetch_asset_metadatato enrich the fund description.ottieni_dati_fondo_da_url(url, sessione)/estrai_codice_da_url(url)โ the URL variant used byresolve_url.
The provider captures the internal code into provider_params.codice_fondo at asset creation (from search or resolve_url); pricing afterwards uses that stored code with no external search. Price fetches never hit any web search โ that invariant is what keeps automated syncs deterministic.
๐ Search (search)
- Uses
cerca(query)โ the internal JSON search engine of borsaitaliana.it. - Searches across all instrument types: stocks, bonds, ETFs, ETC/ETN, and funds.
- Fund names are indexed with Borsa abbreviations (for example
Obbligaz.instead ofObbligazionaria), so the provider retries common full-word variants with abbreviated terms. - Funds:
cercareturns the Borsa internal code in theisinfield (not a real ISIN). The provider fetches each fund page once per code (in-search cache) viaottieni_dati_fondoto recover the real ISIN, sets it as theidentifier(ISIN), and carries the internal code inprovider_params.codice_fondo. If the page can't be fetched or no ISIN is present, it falls back to the code as anOTHERidentifier โ still priceable by NAV. - Dual-language results: emits two entries per instrument (๐ฎ๐น Italiano + ๐ฌ๐ง English) with flag emojis in
display_name. Each result carriesprovider_params: {language: "en"|"it"}so the user's selection is propagated on assignment. - Result ordering: results are grouped by the cerca-level identifier (the internal code for funds, the real ISIN otherwise), preserving first-seen order, and within each group Italian is emitted before English. This keeps sibling funds โ whose internal codes may differ by a single letter โ from interleaving across languages. Grouping also de-duplicates per
(identifier, language). - Last-resort external fallback: when
cercareturns nothing, the search orchestration calls the LibreFolioweb_link_finderstack to turn the query into a candidate Borsa page URL, thenresolve_urls it. The caller can passhints(report-extracted ISIN + names) which drive a two-stage "stringone" query and an identifier post-filter that narrows the resolved siblings to the exact ISIN when it is known โ all handled centrally, see Asset Search & Link-Finder. Best-effort, interactive-only, never on price fetches.
๐ Metadata (fetch_asset_metadata)
- Listed instruments (ISIN path): uses
ottieni_scheda(isin, lingua)โ scrapes the instrument detail page in the configured language, routed by the storedmic/platform. If that raisesStrumentoNonRisoltoandprovider_params.urlis stored, the scheda is read straight from that page (url_diretto). - Extracts:
- Name: from
<h1>tag on the page, appended with language flag emoji (e.g.,"ENEL S.p.A. ๐ฌ๐ง"). - Type: mapped from instrument type field (e.g.,
obbligazioneโ BOND,azioneโ STOCK,etfโ ETF).etc/etnalso maps to ETF, so ETFs, ETCs and ETNs all arrive as plainETFโ the frontend provider-data comparison treats a stored ETF subtype (e.g.ETF_STOCK) as agreeing with it (isFamilyOnlyProposalinfrontend/src/lib/utils/assetTypes.ts). Fund labels (fondi comuni,fondo chiuso,closed-end fund, โฆ) โ FUND; otherobbligazioneโฆ/bondโฆlabels fall back to BOND by prefix; unknown labels โ no type. - Currency: from the chart API that prices the asset, not from the scheda; left unset when unknown โ see Currency.
- Description: assembled from page description, market, issuer, maturity date, coupon rate, structure, tipology, coupon frequency.
- Ticker: if available on the page (mainly for stocks).
- Geographic Area: inferred from issuer name via
_ISSUER_TO_COUNTRY(exact match, then substring), e.g. "Republic of Italy" โITA, "United States of America" โUSA. - Sector: inferred from
settore(stocks) ortipologia(bonds) fields. Bondtipologiagoes through_TIPOLOGIA_TO_SECTOR(EN + IT labels): government paper โGovernment Bonds, corporate โCorporate Bonds, supranational โFinancials; an unmapped tipologia yields no sector. The mapping only shapes the metadata offered: values already stored on an asset change only through the provider-data comparison.
- Name: from
- Bond-specific fields available in the raw data:
cedola_annua,scadenza,emittente,rendimento_lordo,struttura_bond,frequenza_cedola. - Funds (when
provider_params.codice_fondois set): funds are not on the XMIL scheda, so metadata is built from the fund detail page by internal code instead. Theshort_descriptionis assembled from the fund name, the real ISIN, and the non-N.D.entries scraped from the page's Caratteristiche, Societร di Gestione and Costi sections (e.g.โฆ | ISIN: LU2178929613 | Classe: P | Grado di Rischio: 3 | Categoria Assogestioni: Bilanciati | Costi โ Gestione: 1.3 โฆ). The internal code is persisted inidentifier_otheras a JSON-list entry (["2FADBโฆ"]). The section fields are read defensively, so metadata degrades to just name + ISIN if the installed scraping library predates the enrichedDatiFondo.
๐ get_asset_url
Precedence, first match wins:
- Funds with
provider_params.codice_fondoโhttps://www.borsaitaliana.it/borsa/fondi/dettaglio/{codice_fondo}.html?lang={language}, the NAV page keyed by the internal fund code (the generic scheda page does not resolve a fund by ISIN). - A stored
provider_params.urlโ that page, withlang={language}appended when it is missing. It is a working page by construction. - Otherwise โ
https://www.borsaitaliana.it/borsa/search/scheda.html?code={ISIN}&lang={language}, plus&mic={mic}and&platform={platform}when stored. Without them the bare URL still resolves for MTA, MOT, ETFplus, SeDeX and MIV, but not for EuroTLX.
The lang parameter follows the user's provider_params.language selection (default en).
๐ resolve_url (inverse of get_asset_url)
The provider opts into the generic URL โ search-item capability:
resolvable_url_domains = ["borsaitaliana.it"]โsupports_url_resolutionisTrue.resolve_url(url)recognises two public page families:- Fund detail pages (
/borsa/fondi/dettaglio/{code}.html) โ fetches the page and returns the full canonical set a normal search would emit for that fund: the IT + EN pair (Italian first, with flag indisplay_name), each{identifier: <ISIN from page> or code, identifier_type, display_name, currency, type: "FUND", provider_params: {codice_fondo, language}}. - Stock / bond / ETF scheda pages (
โฆ/scheda/{ISIN}[-{MIC}].html) โ returns the same IT + EN canonical set, priced by ISIN. The rows'currencycomes from one chart-API call shared by every row (Noneif it fails), not from the scheda.
- Fund detail pages (
It is only a different entry point โ the orchestration flattens the list and de-dupes by (identifier, language). Anything that is not a recognisable Borsa page (off-domain, no extractable fund code or ISIN path) returns None. Best-effort: fetch/parse errors return None, never raise.
This lets an externally discovered page URL (e.g. found via web_link_finder, or pasted by the user in a future UI) be turned into a ready-to-create asset with the correct pricing params.
๐ web_link_finder (last-resort external search)
Central reference
The link-finder is generic. This section keeps only the Borsa-Italiana-relevant summary โ
the full contract (three layers, hints, identifier filter, config, invariants) lives on
Asset Search & Link-Finder, which is the source of truth.
backend/app/services/web_link_finder.py is a LibreFolio-level module (deliberately not inside the scraping library, so the choice of external engine stays a LibreFolio concern). It turns a free-text query/ISIN into candidate provider-domain URLs, used only at search time as a last resort when a provider's on-site search yields nothing.
- Best-effort: any failure (rate-limit, anomaly page, network/parse error) yields
[]and is never fatal โ the search always completes and emitsdone. - Async-safe: sync I/O runs in
asyncio.to_thread; short timeout, small TTL cache, domain-filtered hits, structured logs. - Pluggable engine: default
DdgsEnginewraps theddgsmetasearch library (bing/brave/google/DuckDuckGo/โฆ aggregated viabackend="auto") with anApiKeyEngineseam for a paid API (Brave/Bing/SerpAPI) later;searxngreserved for a deferred Fase B. - Config (all optional env vars):
LIBREFOLIO_WEB_LINK_FINDER_ENABLED(1/0, default1),_ENGINE(ddgs|apikey),_API_KEY,_DDGS_REGION(defaultwt-wt),_DDGS_BACKEND(defaultauto),_TIMEOUT(s),_MAX.
Invariant
External search is only ever hit during interactive asset search. Price fetches (frequent, automated) must never call web_link_finder โ once an asset exists it is priced by its stored provider_params (a fund's codice_fondo), not by search.
๐ Technical Details
HTTP Session
The provider maintains a shared Sessione instance across all calls:
- WAF handling: The library manages Imperva WAF cookies automatically.
- JWT token: Extracted from the interactive chart page (required for the
grafici.borsaitaliana.itAPI). - Rate limiting: Built-in minimum pause between requests (0.5s default).
- Shutdown: The session is closed via
shutdown()at app teardown.
params_schema (Dynamic UI Form)
The provider exposes optional parameters via params_schema:
| Key | Type | Options | Default | Description |
|---|---|---|---|---|
language |
select |
en (๐ฌ๐ง English), it (๐ฎ๐น Italiano) |
en |
Language for names and metadata |
codice_fondo |
string |
โ | โ | Borsa internal fund code (e.g. 2FADB602822); when set, current/history use the fund NAV path |
mic |
string |
โ | โ | Market MIC (auto-filled from search, e.g. ETLX for EuroTLX); routes the instrument page and metadata |
platform |
string |
โ | โ | Trading platform (auto-filled from search, e.g. TLX for EuroTLX); needed by some markets to resolve the page |
Uses option_labels for human-readable display in the frontend dropdown. Each field also carries a short label โ the form caption (Language, Fund internal code, Market MIC, Platform) โ and a longer description shown as its tooltip; the frontend translates both under assets.providerParams.borsa_italiana.
Error Handling
| Library Exception | Mapped Error Code | Meaning |
|---|---|---|
StrumentoNonRisolto |
UNSUPPORTED_PAGE |
Instrument found but its market page can't be resolved/read (unhandled market family) โ user is invited to open a GitHub issue |
StrumentoNonTrovato |
NOT_FOUND |
ISIN not recognized |
DatiNonDisponibili |
NO_DATA |
No data available (market closed, delisted, or empty window for an illiquid instrument) |
RicercaNonDisponibile |
FETCH_ERROR |
Search endpoint down |
BorsaItalianaErrore |
FETCH_ERROR |
Generic library error |
Note (library โฅ 0.3.0): an empty history window (e.g. an illiquid ExtraMOT bond over 1M) now maps to NO_DATA, never NOT_FOUND โ the library distinguishes "unknown instrument" from "known but no trades in the window" via a MAX-period probe.
Dependencies
Transitive: httpx, beautifulsoup4, lxml.
๐งช Test Cases
| Identifier | provider_params |
Description |
|---|---|---|
IT0003128367 |
โ | ENEL S.p.A. (stock, MTA) |
LU2178929613 |
{codice_fondo: "2FADB602822"} |
Eurizon Next 2.0 Alloc. Divers. 40 P (fund, NAV by code) |
US912810TU25 |
{mic: "ETLX", platform: "TLX"} |
US T-Bond 4.375% Aug43 (EuroTLX bond, USD-denominated, issuer โ USA) |
Search test query: "ENEL" (listed) / "EURIZON NEXT 2.0 DIVERSIFICATO 40 P" (fund by report name).
resolve_url test: https://www.borsaitaliana.it/borsa/fondi/dettaglio/2FADB602822.html โ IT + EN fund search-items with ISIN LU2178929613 + codice_fondo.
๐ Related Documentation
- ๐๏ธ Asset Architecture โ Sync pipeline and caching
- ๐ Asset Search & Link-Finder โ The generic search stack this provider implements
- ๐ Asset Providers Overview โ All providers comparison
- ๐ Asset Plugin Guide โ How to create a new provider