Remove Tiingo spread monitor (indicative two-tier feed, unvalidatable)

Tiingo's free /tiingo/fx/top publishes only a two-tier indicative
spread for gold ($0.10 liquid hours / $0.70 rollover+news; verified on
273 production samples + live probes) — mid ± a posted increment, not
executable depth, so it can never validate the $0.30 backtest cost
assumption. Removed one day after shipping: tiingo_provider.py, the
sampler/quote plumbing in telegram_bot.py, the channel_spread_detail
config knob and tiingo_api_key resolver, and Module 14's Tiingo tests
(221 -> 210). CLAUDE.md documents the verdict: do not re-add a spread
display from Tiingo's free FX feed.

Claude-Session: https://claude.ai/code/session_01BoZKi67Ct6QTeiFJmybXSw
This commit is contained in:
Harwinder Sekhon
2026-07-24 10:56:55 -04:00
parent 3a95a37a0c
commit 7a810c4a4c
6 changed files with 21 additions and 571 deletions
+5 -231
View File
@@ -25,7 +25,7 @@ sys.path.insert(0, os.path.dirname(__file__))
import json
import app_config
for _k in ("TELEGRAM_TOKEN", "TELEGRAM_CHAT_ID", "TELEGRAM_CHANNEL_ID",
"TWELVE_DATA_API_KEY", "TIINGO_API_KEY", "TIINGO"):
"TWELVE_DATA_API_KEY"):
os.environ.pop(_k, None)
_TEST_APP = app_config.AppConfig(
provider_settings={"twelve_data": {"api_key": "test-key"}}
@@ -2751,7 +2751,7 @@ def _write_json(payload) -> str:
_FULL_CFG = {
"telegram": {"token": "t0k", "chat_id": "111", "channel_id": "-100222",
"alert_min_quality": "B", "channel_spread_detail": True},
"alert_min_quality": "B"},
"data_providers": {"active": "twelve_data",
"twelve_data": {"api_key": "json-key"}},
"scanning": {"pairs": ["XAU/USD"], "timeframes": ["M15", "H1"]},
@@ -2767,7 +2767,6 @@ def _():
assert app.telegram_chat_id == "111"
assert app.telegram_channel_id == "-100222"
assert app.alert_min_quality == "B"
assert app.channel_spread_detail is True
assert app.active_provider == "twelve_data"
assert app.provider_settings["twelve_data"]["api_key"] == "json-key"
assert app.scan_pairs == [XAUUSD]
@@ -2821,7 +2820,6 @@ def _():
{"data_providers": {"active": "polygon",
"twelve_data": {"api_key": "k"}}}, # no block
{"telegram": {"alert_min_quality": "Z"}}, # bad quality
{"telegram": {"channel_spread_detail": "yes"}}, # not a bool
{"log_level": "CHATTY"}, # bad level
]
for payload in bad_payloads:
@@ -3004,135 +3002,11 @@ def _():
# ══════════════════════════════════════════════════════════════════════
# MODULE 14: tiingo_provider.py (bid/ask spread monitor)
# MODULE 14: .env loader + telegram_bot resilience
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 14: tiingo_provider.py ═══")
print("\n═══ Module 14: .env loader + telegram_bot resilience ═══")
from tiingo_provider import (
Quote, TiingoQuoteProvider, WIDE_SPREAD_MULTIPLE, log_quote,
tiingo_ticker,
)
from app_config import load_dotenv, tiingo_api_key
def _tiingo_response(payload):
resp = MagicMock()
resp.json.return_value = payload
resp.raise_for_status.return_value = None
return resp
_TIINGO_ROW = [{
"ticker": "xauusd",
"quoteTimestamp": "2026-07-22T12:00:00+00:00",
"bidPrice": 2400.10, "bidSize": 1000000.0,
"askPrice": 2400.40, "askSize": 1000000.0,
"midPrice": 2400.25,
}]
@test("tiingo: ticker mapping XAU/USD → xauusd")
def _():
assert tiingo_ticker("XAU/USD") == "xauusd"
assert tiingo_ticker("EUR/USD") == "eurusd"
@test("tiingo: quote parsed — spread = ask − bid, canonical symbol")
def _():
provider = TiingoQuoteProvider("k")
with patch("tiingo_provider.requests.get",
return_value=_tiingo_response(_TIINGO_ROW)) as mock_get:
q = provider.get_quote("XAU/USD")
assert q is not None
assert q.symbol == "XAU/USD"
assert abs(q.bid - 2400.10) < 1e-9 and abs(q.ask - 2400.40) < 1e-9
assert abs(q.spread - 0.30) < 1e-9
assert abs(q.mid - 2400.25) < 1e-9
assert q.quote_time is not None and q.quote_time.tzinfo is not None
assert q.age_seconds() < 5
params = mock_get.call_args.kwargs["params"]
assert params["tickers"] == "xauusd" and params["token"] == "k"
@test("tiingo: mid falls back to (bid+ask)/2 when midPrice missing")
def _():
row = [dict(_TIINGO_ROW[0], midPrice=None)]
provider = TiingoQuoteProvider("k")
with patch("tiingo_provider.requests.get",
return_value=_tiingo_response(row)):
q = provider.get_quote("XAU/USD")
assert q is not None and abs(q.mid - 2400.25) < 1e-9
@test("tiingo: TTL cache dedupes calls; ttl=0 refetches")
def _():
provider = TiingoQuoteProvider("k")
with patch("tiingo_provider.requests.get",
return_value=_tiingo_response(_TIINGO_ROW)) as mock_get:
provider.get_quote("XAU/USD")
provider.get_quote("XAU/USD")
assert mock_get.call_count == 1, "second call within TTL must hit cache"
provider = TiingoQuoteProvider("k", quote_ttl=0.0)
with patch("tiingo_provider.requests.get",
return_value=_tiingo_response(_TIINGO_ROW)) as mock_get:
provider.get_quote("XAU/USD")
provider.get_quote("XAU/USD")
assert mock_get.call_count == 2
@test("tiingo: every failure path returns None (never raises)")
def _():
provider = TiingoQuoteProvider("k", quote_ttl=0.0)
# network error
with patch("tiingo_provider.requests.get",
side_effect=Exception("boom")):
assert provider.get_quote("XAU/USD") is None
# HTTP error
resp = _tiingo_response(_TIINGO_ROW)
resp.raise_for_status.side_effect = Exception("401")
with patch("tiingo_provider.requests.get", return_value=resp):
assert provider.get_quote("XAU/USD") is None
# empty / non-list / null-price payloads
for payload in ([], {"detail": "unknown ticker"},
[dict(_TIINGO_ROW[0], bidPrice=None)],
[dict(_TIINGO_ROW[0], askPrice=None)]):
with patch("tiingo_provider.requests.get",
return_value=_tiingo_response(payload)):
assert provider.get_quote("XAU/USD") is None, \
f"payload {payload!r} must yield None"
@test("tiingo: log_quote writes header once then appends rows")
def _():
q = Quote(symbol=XAUUSD, bid=2400.10, ask=2400.40, mid=2400.25,
spread=0.30, quote_time=None, fetched_at=time.time())
with tempfile.TemporaryDirectory() as td:
path = os.path.join(td, "sub", "spread_log.csv")
log_quote(q, path)
log_quote(q, path)
with open(path, encoding="utf-8") as fh:
lines = fh.read().strip().splitlines()
assert lines[0] == "ts_utc,symbol,bid,ask,mid,spread"
assert len(lines) == 3, "header + one row per log_quote call"
cells = lines[1].split(",")
assert cells[1] == XAUUSD and float(cells[5]) == 0.30
@test("app_config: tiingo_api_key precedence env → env-short → json → ''")
def _():
app = AppConfig(provider_settings={
"twelve_data": {"api_key": "td"},
"tiingo": {"api_key": "json-tiingo"},
})
with patch.dict(os.environ, {}, clear=True):
assert tiingo_api_key(app) == "json-tiingo"
with patch.dict(os.environ, {"TIINGO": "short-env"}, clear=True):
assert tiingo_api_key(app) == "short-env"
with patch.dict(os.environ, {"TIINGO": "short-env",
"TIINGO_API_KEY": "long-env"}, clear=True):
assert tiingo_api_key(app) == "long-env"
with patch.dict(os.environ, {}, clear=True):
assert tiingo_api_key(AppConfig()) == "", "no key anywhere = disabled"
from app_config import load_dotenv
@test("app_config: load_dotenv parses lines, never overrides real env")
@@ -3163,90 +3037,6 @@ def _():
load_dotenv("/nonexistent/.env") # missing file is a silent no-op
@test("telegram_bot: _spread_lines fresh/wide/stale/absent")
def _():
import telegram_bot as tb
try:
# fresh, normal spread → bid/ask line only
tb._last_quotes[XAUUSD] = Quote(
symbol=XAUUSD, bid=2400.10, ask=2400.40, mid=2400.25,
spread=0.30, quote_time=None, fetched_at=time.time())
lines = tb._spread_lines(XAUUSD)
assert len(lines) == 1 and "Bid/Ask" in lines[0]
assert "spread $0.30" in lines[0]
# wide spread (≥ 2× the $0.30 assumption) → warning appended
tb._last_quotes[XAUUSD] = Quote(
symbol=XAUUSD, bid=2400.10, ask=2401.00, mid=2400.55,
spread=0.90, quote_time=None, fetched_at=time.time())
lines = tb._spread_lines(XAUUSD)
assert len(lines) == 2 and "Wide spread" in lines[1]
assert "3.0×" in lines[1]
# stale quote → nothing
tb._last_quotes[XAUUSD] = Quote(
symbol=XAUUSD, bid=2400.10, ask=2400.40, mid=2400.25,
spread=0.30, quote_time=None,
fetched_at=time.time() - tb._QUOTE_MAX_AGE - 1)
assert tb._spread_lines(XAUUSD) == []
# no quote at all → nothing
tb._last_quotes.pop(XAUUSD, None)
assert tb._spread_lines(XAUUSD) == []
finally:
tb._last_quotes.pop(XAUUSD, None)
@test("telegram_bot: channel variant never leaks Tiingo numbers")
def _():
# Tiingo free tier is internal-use-only: the subscriber channel may
# see a number-free caution, never bid/ask/spread values.
import telegram_bot as tb
try:
# normal spread → channel variant shows nothing at all
tb._last_quotes[XAUUSD] = Quote(
symbol=XAUUSD, bid=2400.10, ask=2400.40, mid=2400.25,
spread=0.30, quote_time=None, fetched_at=time.time())
assert tb._spread_lines(XAUUSD, include_quote=False) == []
# wide spread → one caution line with no prices in it
tb._last_quotes[XAUUSD] = Quote(
symbol=XAUUSD, bid=2400.10, ask=2401.00, mid=2400.55,
spread=0.90, quote_time=None, fetched_at=time.time())
lines = tb._spread_lines(XAUUSD, include_quote=False)
assert len(lines) == 1 and "unusually wide" in lines[0]
assert "2400" not in lines[0] and "0.90" not in lines[0] \
and "$" not in lines[0], "channel line must carry no data values"
# format_signal_message plumbs the flag through
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {
Timeframe.H1: make_ohlcv(300, seed=42),
Timeframe.M15: make_ohlcv(300, seed=43),
Timeframe.H4: make_ohlcv(300, seed=44),
}
report = agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False)
owner_msg = tb.format_signal_message(report, spread_detail=True)
channel_msg = tb.format_signal_message(report, spread_detail=False)
assert "Bid/Ask" in owner_msg
assert "Bid/Ask" not in channel_msg
finally:
tb._last_quotes.pop(XAUUSD, None)
@test("telegram_bot: _spread_detail_for routes by chat + internal flag")
def _():
import telegram_bot as tb
saved_channel, saved_flag = tb.CHANNEL_ID, tb._CHANNEL_SPREAD_DETAIL
try:
tb.CHANNEL_ID = -100222
tb._CHANNEL_SPREAD_DETAIL = False # default: channel is public
assert tb._spread_detail_for(111) is True, "owner DM gets raw quotes"
assert tb._spread_detail_for(-100222) is False, \
"public channel must get the number-free variant"
tb._CHANNEL_SPREAD_DETAIL = True # internal-testing channel
assert tb._spread_detail_for(-100222) is True
assert tb._spread_detail_for(111) is True
finally:
tb.CHANNEL_ID, tb._CHANNEL_SPREAD_DETAIL = saved_channel, saved_flag
@test("telegram_bot: log rotation is 3-hourly with 30-day retention")
def _():
import logging.handlers
@@ -3282,22 +3072,6 @@ def _():
tb._autoscan_chats.clear()
@test("telegram_bot: no Tiingo key → provider off, message unchanged")
def _():
import telegram_bot as tb
assert tb._tiingo is None, "hermetic env has no Tiingo key"
config = PlatformConfig()
agg = SignalAggregator(config)
dfs = {
Timeframe.H1: make_ohlcv(300, seed=42),
Timeframe.M15: make_ohlcv(300, seed=43),
Timeframe.H4: make_ohlcv(300, seed=44),
}
report = agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False)
msg = tb.format_signal_message(report)
assert "Bid/Ask" not in msg and "Wide spread" not in msg
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════