Quant0 commited on
Commit
23c86d9
·
verified ·
1 Parent(s): 68e0797

Create matching.py

Browse files
Files changed (1) hide show
  1. matching.py +210 -0
matching.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # matching.py — Búsqueda y emparejamiento de empresas/tickers
3
+ # ------------------------------------------------------------
4
+ # NOTA PARA AUDITORÍA:
5
+ # - Este módulo encapsula la lógica de:
6
+ # (1) Consulta a Yahoo Finance Search API para obtener candidatos
7
+ # (nombre, ticker) a partir de una cadena de búsqueda.
8
+ # (2) Priorización de resultados con una lista/dict opcional
9
+ # de tickers preferentes (p. ej., los que tienen holdings BTC).
10
+ # (3) Fuzzy matching (rapidfuzz) primero por ticker y luego por nombre.
11
+ # - No tiene efectos secundarios al importarlo.
12
+ # - La salida mantiene la firma usada en tu app: (name, ticker) o (None, None).
13
+ # ============================================================
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Dict, Iterable, List, Optional, Tuple
18
+
19
+ import requests
20
+ from rapidfuzz import process
21
+
22
+
23
+ YF_SEARCH_ENDPOINT = (
24
+ "https://query1.finance.yahoo.com/v1/finance/search"
25
+ )
26
+
27
+
28
+ def _fetch_yahoo_finance_candidates(
29
+ query: str,
30
+ quotes_count: int = 10,
31
+ timeout: float = 15.0,
32
+ user_agent: str = "Mozilla/5.0",
33
+ ) -> List[Tuple[str, str]]:
34
+ """
35
+ Llama a Yahoo Finance Search API y devuelve una lista de candidatos (name, ticker).
36
+
37
+ Parámetros
38
+ ----------
39
+ query : str
40
+ Texto de búsqueda (p. ej., 'MicroStrategy', 'MSTR', 'Tesla').
41
+ quotes_count : int
42
+ Número máximo de resultados (por defecto: 10).
43
+ timeout : float
44
+ Timeout de la petición (segundos).
45
+ user_agent : str
46
+ Cabecera 'User-Agent' para la petición.
47
+
48
+ Devuelve
49
+ --------
50
+ list[tuple[str, str]]
51
+ Lista de pares (name, ticker) en el orden que devuelve la API.
52
+ Si no hay resultados o ocurre un error, devuelve lista vacía.
53
+ """
54
+ q = (query or "").strip()
55
+ if not q:
56
+ return []
57
+
58
+ url = f"{YF_SEARCH_ENDPOINT}?q={q}&quotesCount={quotes_count}&newsCount=0"
59
+ headers = {"User-Agent": user_agent}
60
+
61
+ try:
62
+ r = requests.get(url, headers=headers, timeout=timeout)
63
+ r.raise_for_status()
64
+ data = r.json()
65
+ except Exception:
66
+ return []
67
+
68
+ quotes = data.get("quotes", [])
69
+ results: List[Tuple[str, str]] = []
70
+
71
+ for item in quotes:
72
+ symbol = item.get("symbol")
73
+ if not symbol:
74
+ continue
75
+ # Preferir shortname/longname sobre symbol como nombre visible
76
+ name = item.get("shortname") or item.get("longname") or symbol
77
+ results.append((name, symbol))
78
+
79
+ return results
80
+
81
+
82
+ def _prioritize_with_whitelist(
83
+ results: Iterable[Tuple[str, str]],
84
+ priority_tickers: Optional[Dict[str, object]] = None,
85
+ ) -> Optional[Tuple[str, str]]:
86
+ """
87
+ Devuelve el primer candidato cuyo ticker esté en la lista/dict de prioridad.
88
+ Si no hay prioridad o no hay coincidencias, devuelve None.
89
+ """
90
+ if not priority_tickers:
91
+ return None
92
+
93
+ priority_keys = set(priority_tickers.keys())
94
+ for name, ticker in results:
95
+ if ticker in priority_keys:
96
+ return name, ticker
97
+ return None
98
+
99
+
100
+ def _fuzzy_match_ticker(
101
+ query: str,
102
+ results: Iterable[Tuple[str, str]],
103
+ score_cutoff: int = 80,
104
+ ) -> Optional[Tuple[str, str]]:
105
+ """
106
+ Fuzzy matching por TICKER (mayúsculas). Devuelve el (name, ticker) con
107
+ mejor score si supera el umbral; en caso contrario, None.
108
+ """
109
+ tickers = [sym for _, sym in results]
110
+ if not tickers:
111
+ return None
112
+
113
+ match = process.extractOne(query.upper(), tickers, score_cutoff=score_cutoff)
114
+ if not match:
115
+ return None
116
+
117
+ best_ticker = match[0]
118
+ # Recuperar el 'name' correspondiente
119
+ for name, ticker in results:
120
+ if ticker == best_ticker:
121
+ return name, ticker
122
+ return None
123
+
124
+
125
+ def _fuzzy_match_name(
126
+ query: str,
127
+ results: Iterable[Tuple[str, str]],
128
+ score_cutoff: int = 60,
129
+ ) -> Optional[Tuple[str, str]]:
130
+ """
131
+ Fuzzy matching por NOMBRE visible. Devuelve el (name, ticker) con mejor
132
+ score si supera el umbral; si no, None.
133
+ """
134
+ names = [name for name, _ in results]
135
+ if not names:
136
+ return None
137
+
138
+ match = process.extractOne(query, names, score_cutoff=score_cutoff)
139
+ if not match:
140
+ return None
141
+
142
+ best_name = match[0]
143
+ for name, ticker in results:
144
+ if name == best_name:
145
+ return name, ticker
146
+ return None
147
+
148
+
149
+ def find_company_online(
150
+ query: str,
151
+ priority_tickers: Optional[Dict[str, object]] = None,
152
+ quotes_count: int = 10,
153
+ timeout: float = 15.0,
154
+ ) -> Tuple[Optional[str], Optional[str]]:
155
+ """
156
+ Realiza la búsqueda de una empresa/ticker en Yahoo Finance y aplica
157
+ una estrategia de resolución en cuatro pasos (igual a tu lógica):
158
+
159
+ 1) Si 'priority_tickers' está definido, devuelve el primer candidato
160
+ cuyo ticker esté en esa lista.
161
+ 2) Fuzzy matching por ticker (score >= 80).
162
+ 3) Fuzzy matching por nombre (score >= 60).
163
+ 4) Si todo falla, el primer resultado.
164
+ 5) Si no hay resultados, (None, None).
165
+
166
+ Parámetros
167
+ ----------
168
+ query : str
169
+ Texto a buscar (se recomienda recortar en UI a 6 caracteres como haces).
170
+ priority_tickers : dict[str, Any], opcional
171
+ Diccionario o conjunto de tickers priorizados (p. ej., tickers_to_ids).
172
+ quotes_count : int
173
+ Límite de resultados solicitados a la API de Yahoo Finance.
174
+ timeout : float
175
+ Timeout de la petición a la API.
176
+
177
+ Devuelve
178
+ --------
179
+ (str | None, str | None)
180
+ Tupla (name, ticker) o (None, None).
181
+ """
182
+ # 0) Descargar candidatos
183
+ results = _fetch_yahoo_finance_candidates(
184
+ query=query, quotes_count=quotes_count, timeout=timeout
185
+ )
186
+ if not results:
187
+ return None, None
188
+
189
+ # 1) Priorizar tickers conocidos
190
+ prioritized = _prioritize_with_whitelist(results, priority_tickers)
191
+ if prioritized:
192
+ return prioritized
193
+
194
+ # 2) Fuzzy por ticker
195
+ by_ticker = _fuzzy_match_ticker(query, results, score_cutoff=80)
196
+ if by_ticker:
197
+ return by_ticker
198
+
199
+ # 3) Fuzzy por nombre
200
+ by_name = _fuzzy_match_name(query, results, score_cutoff=60)
201
+ if by_name:
202
+ return by_name
203
+
204
+ # 4) Fallback: primer resultado
205
+ return results[0]
206
+
207
+
208
+ __all__ = [
209
+ "find_company_online",
210
+ ]