Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions contexts/design/flow/news.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,17 @@ three limited checks:
3. fetch up to 25 unique articles and find the first supported exchange-coded
symbol in every group that has one (100% recall).

Ticker-hint extraction supports one exchange-qualified symbol and parenthesized
comma-separated symbol lists that share a supported exchange prefix. A shared
group ends at a semicolon, conjunction, or closing parenthesis. Additional list
members record a reconstructed `EXCHANGE: SYMBOL` raw value; unsupported
exchanges remain outside the whitelist policy.

The ticker check uses a separate comparison regex rather than the production
extractor. It accepts plain, Markdown-link, and emphasis-wrapped symbols. It
ignores later symbols in a multi-symbol list and exchanges that the extractor
does not support. The script prints PASS/FAIL plus short listing, article,
recognizes comma-separated symbols that share a supported exchange prefix and
ignores exchanges that the extractor does not support. The script prints
PASS/FAIL plus short listing, article,
failure, and recall summaries. It fails when RSS or listing data is invalid,
no sampled article parses, or supported symbols fall below 100% recall. A
parsed sample with no supported symbols reports `SKIP` and still passes.
Expand Down
8 changes: 8 additions & 0 deletions examples/preprocess/02_news_ticker_lists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Extract ticker hints from a shared exchange-prefix list."""

from quantmind.preprocess import extract_exchange_ticker_hints

text = "Example issuer (NYSE: EVEX, EVEXW; B3: EVEB31) announced results."

for hint in extract_exchange_ticker_hints(text):
print(hint)
35 changes: 33 additions & 2 deletions quantmind/preprocess/news.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
r"(?<!\*)\*{1,2}([A-Z][A-Z0-9.-]{0,9})\*{1,2}(?!\*)",
re.IGNORECASE,
)
_SHARED_EXCHANGE_SYMBOL_RE = re.compile(
r"\s*,\s*([A-Z][A-Z0-9.-]{0,9})",
re.IGNORECASE,
)
_EMAIL_PROTECTION_LINK_RE = re.compile(
r"\[\[email protected]\]\(/cdn-cgi/l/email-protection#[^)]+\)"
)
Expand Down Expand Up @@ -401,8 +405,12 @@ def canonicalize_source_url(url: str) -> str:
def extract_exchange_ticker_hints(text: str) -> tuple[NewsTickerHint, ...]:
"""Extract exchange-qualified ticker mentions from PR-style text.

Examples matched include ``(NASDAQ: NVDA)`` and ``NYSE: IBM``. The result is
only a hint; downstream instrument resolution should still validate it.
Examples matched include ``(NASDAQ: NVDA)``, ``NYSE: IBM``, and a
parenthesized comma list such as ``(NYSE: EVEX, EVEXW)``. A shared exchange
applies only to comma-separated symbols before the closing parenthesis;
semicolons and conjunctions end the group. Additional members use a
reconstructed ``EXCHANGE: SYMBOL`` raw value. The result is only a hint;
downstream instrument resolution should still validate it.
Markdown link and emphasis decoration is removed from a scan-only copy so
stored news text and its content hash retain their original representation.
"""
Expand All @@ -425,6 +433,29 @@ def extract_exchange_ticker_hints(text: str) -> tuple[NewsTickerHint, ...]:
raw=match.group(0).strip(),
)
)
if not match.group(0).lstrip().startswith("("):
continue

closing_parenthesis = scan_text.find(")", match.end())
if closing_parenthesis == -1:
continue
suffix = scan_text[match.end() : closing_parenthesis]
position = 0
while continuation := _SHARED_EXCHANGE_SYMBOL_RE.match(
suffix, position
):
symbol = continuation.group(1).upper()
key = (symbol, exchange)
if key not in seen:
seen.add(key)
hints.append(
NewsTickerHint(
symbol=symbol,
exchange=exchange,
raw=f"{exchange}: {symbol}",
)
)
position = continuation.end()
return tuple(hints)


Expand Down
39 changes: 39 additions & 0 deletions tests/preprocess/test_news.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,45 @@ def test_exchange_ticker_hints_ignore_markdown_decoration(self):
expected,
)

def test_exchange_ticker_hints_capture_shared_prefix_comma_lists(self):
cases = (
(
"supported shared prefix",
"(NYSE: EVEX, EVEXW; B3: EVEB31)",
(
("EVEX", "NYSE", "(NYSE: EVEX"),
("EVEXW", "NYSE", "NYSE: EVEXW"),
),
),
("unsupported exchange", "(OTCID: QVCAQ, QVCGQ, QVCPQ)", ()),
(
"conjunction boundary",
"(NYSE: TME and HKEX: 1698)",
(("TME", "NYSE", "(NYSE: TME"),),
),
(
"semicolon boundary TSXV",
"(NASDAQ: VMAR; TSXV: VMAR)",
(("VMAR", "NASDAQ", "(NASDAQ: VMAR"),),
),
(
"semicolon boundary BMV",
"(NYSE: ASR; BMV: ASUR)",
(("ASR", "NYSE", "(NYSE: ASR"),),
),
)

for name, text, expected in cases:
with self.subTest(name=name):
hints = extract_exchange_ticker_hints(text)

self.assertEqual(
tuple(
(hint.symbol, hint.exchange, hint.raw) for hint in hints
),
expected,
)

def test_build_sec_news_identity(self):
self.assertEqual(
build_sec_news_identity(
Expand Down