fix(dataflows): map crypto to StockTwits/Reddit sentiment symbols

- crypto reached StockTwits as Yahoo's BTC-USD (404) instead of BTC.X, and Reddit
  searched the dashed pair that barely matches; both now resolve the base via a
  shared crypto_base() helper, restoring crypto sentiment
- also fixes a StockTwits resilience test class that pytest never collected #1113
This commit is contained in:
Yijia-Xiao
2026-07-05 14:29:07 +00:00
parent daf1da9c35
commit a102afa090
6 changed files with 114 additions and 13 deletions

View File

@@ -190,3 +190,25 @@ class TestFormatterHandlesRssPosts:
assert "1234↑" in out
assert "56c" in out
assert "via RSS" not in out
@pytest.mark.unit
class TestCryptoSearchTerm:
"""A crypto pair (BTC-USD) barely matches Reddit text; search the base (#1113)."""
def _captured_ticker(self, ticker):
seen = {}
def fake_fetch(t, sub, limit, timeout):
seen["ticker"] = t
return []
with patch.object(reddit, "_fetch_subreddit", side_effect=fake_fetch):
reddit.fetch_reddit_posts(ticker, subreddits=("stocks",), inter_request_delay=0)
return seen["ticker"]
def test_crypto_pair_searches_base(self):
assert self._captured_ticker("BTC-USD") == "BTC"
def test_equity_passes_through(self):
assert self._captured_ticker("NVDA") == "NVDA"

View File

@@ -1,5 +1,9 @@
"""StockTwits fetch degrades (never raises) on transport errors, including the
http.client chunked-transfer exceptions that are not OSErrors (#1024)."""
"""StockTwits fetch: transport-error resilience (#1024) and crypto symbol
mapping (#1113).
StockTwits lists crypto under ``<BASE>.X`` (Yahoo's ``BTC-USD`` 404s), and any
transport error must degrade to a placeholder rather than raise.
"""
from __future__ import annotations
@@ -26,7 +30,7 @@ def _raise(exc):
@pytest.mark.unit
class StockTwitsResilienceTests:
class TestStockTwitsResilience:
@pytest.mark.parametrize(
"exc",
[
@@ -40,3 +44,34 @@ class StockTwitsResilienceTests:
out = stocktwits.fetch_stocktwits_messages("NVDA")
assert "unavailable" in out.lower()
assert out.startswith("<stocktwits unavailable")
@pytest.mark.unit
class TestStockTwitsCryptoSymbols:
@pytest.mark.parametrize(
("ticker", "expected"),
[
("BTC-USD", "BTC.X"),
("eth-usd", "ETH.X"),
("SOL-USD", "SOL.X"),
("BTCUSD", "BTC.X"), # undashed broker form
("BTC-USDT", "BTC.X"), # stablecoin quote
("AMD", "AMD"),
("BRK-B", "BRK-B"), # dashed class share: untouched
("GOLD", "GOLD"), # real equity (aliases elsewhere): untouched here
("XYZ-USD", "XYZ-USD"), # unknown base: not treated as crypto
],
)
def test_symbol_mapping(self, ticker, expected):
assert stocktwits._stocktwits_symbol(ticker) == expected
def test_crypto_pair_requests_dot_x_endpoint(self):
seen = {}
def fake_urlopen(req, timeout=None):
seen["url"] = req.full_url
raise TimeoutError("stop after capturing the URL")
with patch.object(stocktwits, "urlopen", side_effect=fake_urlopen):
stocktwits.fetch_stocktwits_messages("BTC-USD")
assert "/symbol/BTC.X.json" in seen["url"]

View File

@@ -6,6 +6,7 @@ import pytest
from tradingagents.dataflows.symbol_utils import (
NoMarketDataError,
crypto_base,
is_yahoo_safe,
normalize_symbol,
)
@@ -77,5 +78,25 @@ class TestIsYahooSafe(unittest.TestCase):
self.assertFalse(is_yahoo_safe(sym))
@pytest.mark.unit
class TestCryptoBase(unittest.TestCase):
def test_resolves_known_crypto_forms(self):
for raw in ("BTC-USD", "BTCUSD", "btc-usdt", "BTC-USDC", "BTCUSD+"):
self.assertEqual(crypto_base(raw), "BTC")
self.assertEqual(crypto_base("ETH-USD"), "ETH")
self.assertEqual(crypto_base("sol-usd"), "SOL")
def test_non_crypto_returns_none(self):
# Plain equities, class shares, and real tickers that alias elsewhere
# (GOLD -> gold future on the Yahoo path) must NOT read as crypto.
for raw in ("AAPL", "BRK-B", "GOLD", "XYZ-USD", "EURUSD", "", None):
self.assertIsNone(crypto_base(raw))
def test_agrees_with_normalize_symbol(self):
# crypto_base is the shared primitive behind the -USD normalization.
self.assertEqual(normalize_symbol("BTCUSD"), "BTC-USD")
self.assertEqual(crypto_base("BTCUSD"), "BTC")
if __name__ == "__main__":
unittest.main()