Aller au contenu principal

Référence d'implémentation d'un fournisseur personnalisé

Les étapes autour d'un nouveau fournisseur (enregistrement, unités, canary, tests, couverture) se trouvent dans le guide. Cette page présente le code.

Squelette​

"""MySite: fundamentals read from https://mysite.example."""

import logging
from typing import Optional

from selectolax.parser import HTMLParser

from financials.models import FinancialMetrics
from financials.numbers import parse_number
from financials.providers.base import BaseFinancialProvider

logger = logging.getLogger(__name__)


class MySiteProvider(BaseFinancialProvider):
name = "MySite"
timeout = 10.0

SEARCH_URL = "https://mysite.example/api/search"
BASE_URL = "https://mysite.example"

async def get_financials(self, ticker: str) -> Optional[FinancialMetrics]:
"""``ticker`` is the search term chosen by the runner (mapping, ISIN or ticker)."""
async with self._session() as client: # cookies kept between the two requests
found = await client.get(self.SEARCH_URL, params={"q": ticker})
if found.status_code != 200 or not found.json().get("results"):
return None
path = found.json()["results"][0]["url"]
page = await client.get(self.BASE_URL + path)
if page.status_code != 200:
return None
metrics = self._parse_page(HTMLParser(page.text), ticker)
metrics.provider_url = self.BASE_URL + path
return metrics

def _parse_page(self, parser: HTMLParser, ticker: str) -> FinancialMetrics:
metrics = FinancialMetrics(ticker=ticker)
for row in parser.css("table.key-figures tr"):
label = row.css_first("th")
value = row.css_first("td")
if not label or not value:
continue
text = value.text(strip=True)
match label.text(strip=True):
case "P/E":
metrics.pe_ratio = parse_number(text, decimal=".")
case "Dividend yield":
metrics.dividend_yield = parse_number(text, decimal=".") # "3.45 %" -> 3.45
case "ISIN":
metrics.isin = text
return metrics

Ici, dividend_yield est un pourcentage affiché (3.45) : déclarez-le pour MySite dans monitoring/units.py.

FinancialMetrics​

Champs de financials/models.py : revenue, ebitda, net_income, eps, payout_ratio, dividend_yield, debt_to_equity, isin, pe_ratio, profit_margin, operating_margin, esg_score, risk_level, morningstar_rating, eligibility, piotroski_score, beneish_m_score, roic, gf_score, provider_url, ticker.

Utilitaires de BaseFinancialProvider​

UtilitaireUsage
await self._get(url, headers=, params=)Texte d'une page, ou None
await self._get_json(url, ...) / await self._post_json(url, ...)JSON décodé, ou None
async with self._session() as clientPlusieurs requêtes partageant les cookies (client.get, client.post)
self._get_headers(extra)En-têtes de type navigateur avec un User-Agent tournant
self._safe_float(value), self._safe_int(value)Conversions tolérantes

Attributs de classe : name, timeout (secondes), max_retries (3), retry_delay (1 s, doublé à chaque tentative), _semaphore (une limite de concurrence propre au fournisseur).

Chaque requête suit la même politique : les erreurs réseau et les statuts 429, 500, 502, 503, 504 sont retentés avec une pause croissante ; 400, 401, 403, 404 et 410 sont définitifs ; un Retry-After supérieur à 10 secondes est plafonné. Au plus FONREX_PROVIDER_MAX_CONCURRENCY requêtes (4) s'exécutent en même temps par fournisseur, et FONREX_PROXY_URL, s'il est défini, les fait passer par un proxy.

Nombres​

from financials.numbers import find_number, parse_number

parse_number("1 234,5 M€", decimal=",") # 1234500000.0
parse_number("(12.3)", decimal=".") # -12.3 (accounting parentheses)
parse_number("80,95 Md", decimal=",") # 80950000000.0
find_number("Dividend yield: 3.45 % (2025)") # 3.45

decimal est le séparateur décimal de la page : , (la valeur par défaut de parse_number) pour les pages françaises, . pour les pages anglaises. Un texte qui contient plus qu'un nombre est rejeté plutôt qu'à moitié lu.