resolver: exact base-symbol match for ticker queries, whole-word name matching for name queries
Browse files- src/portfolio.py +8 -1
- tests/test_validator.py +2 -1
src/portfolio.py
CHANGED
|
@@ -109,10 +109,17 @@ def resolve_symbol(query: str) -> dict:
|
|
| 109 |
return m["av"].endswith(".FRK") or (m["av"].endswith(".LON") and m["av"][0] == "0")
|
| 110 |
def _otc(m):
|
| 111 |
return m["region"] == "United States" and len(m["av"]) == 5 and m["av"][-1] in "YF"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
home = [m for m in matches if m["region"] != "United States" and not _secondary(m)]
|
| 113 |
us_primary = [m for m in matches if m["region"] == "United States" and not _otc(m)]
|
| 114 |
rest = [m for m in matches if m not in home and m not in us_primary]
|
| 115 |
-
us_shaped = _TICKER_RE.fullmatch(qU) and "." not in qU
|
| 116 |
ordered = (us_primary + home) if us_shaped else (home + us_primary)
|
| 117 |
if not us_shaped:
|
| 118 |
ordered += rest # a US-shaped dead ticker must not land on a Frankfurt line — fall to the LLM
|
|
|
|
| 109 |
return m["av"].endswith(".FRK") or (m["av"].endswith(".LON") and m["av"][0] == "0")
|
| 110 |
def _otc(m):
|
| 111 |
return m["region"] == "United States" and len(m["av"]) == 5 and m["av"][-1] in "YF"
|
| 112 |
+
us_shaped = bool(_TICKER_RE.fullmatch(qU) and "." not in qU)
|
| 113 |
+
if _TICKER_RE.fullmatch(qU): # a ticker query must match the listing's base symbol EXACTLY
|
| 114 |
+
base_q = qU.rsplit(".", 1)[0]
|
| 115 |
+
matches = [m for m in matches if m["av"].rsplit(".", 1)[0].replace(".", "-").upper() == base_q]
|
| 116 |
+
else: # a name query: every token must appear as a whole word in the name
|
| 117 |
+
toks = [t for t in re.split(r"[^a-z0-9]+", q.lower()) if len(t) > 1]
|
| 118 |
+
matches = [m for m in matches
|
| 119 |
+
if all(re.search(r"\b" + re.escape(t) + r"\b", m["name"].lower()) for t in toks)]
|
| 120 |
home = [m for m in matches if m["region"] != "United States" and not _secondary(m)]
|
| 121 |
us_primary = [m for m in matches if m["region"] == "United States" and not _otc(m)]
|
| 122 |
rest = [m for m in matches if m not in home and m not in us_primary]
|
|
|
|
| 123 |
ordered = (us_primary + home) if us_shaped else (home + us_primary)
|
| 124 |
if not us_shaped:
|
| 125 |
ordered += rest # a US-shaped dead ticker must not land on a Frankfurt line — fall to the LLM
|
tests/test_validator.py
CHANGED
|
@@ -132,4 +132,5 @@ def test_resolver_demotes_secondary_venues(monkeypatch):
|
|
| 132 |
monkeypatch.setattr(PF, "resolve_isin", lambda i: [])
|
| 133 |
monkeypatch.setattr(PF, "call_llm", lambda *a, **k: '{"ticker": "XYZ", "name": "Block"}')
|
| 134 |
assert PF.resolve_symbol("SQ")["symbol"] == "XYZ" # not the Frankfurt line
|
| 135 |
-
assert PF.resolve_symbol("Block
|
|
|
|
|
|
| 132 |
monkeypatch.setattr(PF, "resolve_isin", lambda i: [])
|
| 133 |
monkeypatch.setattr(PF, "call_llm", lambda *a, **k: '{"ticker": "XYZ", "name": "Block"}')
|
| 134 |
assert PF.resolve_symbol("SQ")["symbol"] == "XYZ" # not the Frankfurt line
|
| 135 |
+
assert PF.resolve_symbol("Block Inc")["symbol"] == "SQ3.F" # name tokens all present → the only listing found
|
| 136 |
+
assert PF.resolve_symbol("Block payments")["symbol"] == "XYZ" # 'payments' absent from the name → LLM path
|