feat(market-data): UK (LSE .L) equity market support - #1206
Conversation
test_all_expected_markets_present asserted the exact FALLBACK_CHAINS key set, so adding uk_equity (PR HKUDS#1206) failed CI on Python 3.11 and 3.14. Add uk_equity to the expected set and a uk chain-composition test mirroring the canada/vietnam ones.
Journals carrying VOD.L/HSBA.L/DCC.IL trades were inferred as 'other' (no .L/.IL branch), degrading shadow analysis to NaN price features and 'Other' rule labels. Add .L/.IL -> 'uk' inference, map 'uk' to uk_equity in the shadow extractor's registry key map, and label it 'UK equity' in rule translation (HKUDS#1206).
…after reads succeed Reviewer blocker (HKUDS#1206) — two holes in the preemptive halt sweep: 1. flatten read paths assumed call_tool returns a list; the MCP adapter returns {"status": "error", ...} envelopes instead of raising, so a failed open-orders/positions read crashed on string keys (AttributeError: 'str' object has no attribute 'get') AFTER the sweep latch was persisted — a restart then suppressed the kill action for that halt episode forever. _read_broker_state now rejects envelopes, exceptions, and non-list results as structured report["errors"] items (phase read_open_orders / read_positions); non-dict entries are skipped with an error instead of iterated. 2. _run_preemptive_sweep persisting FLATTEN_FIRED before running the sweep. The latch is now written only after broker-state reads succeeded (nothing happened that must not be repeated); raising sweeps and side-effect failures still latch (non-idempotent, §8.5). 3. sweep_latch episode precedence: the newest tripped sentinel (broker or global) now wins the episode lookup, so a stale per-broker latch no longer suppresses a sweep for a newer global HALT (halt_flag_set treats the global as authoritative); the record accumulates episodes so clearing one halt never re-fires an older, already-swept episode. Tests: adapter-envelope regression for both read paths, non-list/invalid read rejection, newer-global rearm + latch persistence, runner restart-replay on read failure (reviewer repro). 390 affected tests pass. Also removes remaining LSE/ISE wording that contradicted the .L-only scope (registry, base, yahoo_client, statements, profile, skill doc) and pre-existing unused imports flagged by ruff.
|
Thanks — the UK work itself reads well, and the parts I checked (SDRT on the purchase side, GBp→GBP normalisation at the loader boundary, I can't merge it as it stands, because it isn't only UK support. The branch is up to date with
The second one changes the kill switch's no-retry rule. It may well be the right change, but it cannot be reviewed inside a market-data PR, and the diff that would carry it is titled "UK equity market support". The underlying point is legitimate and I want the fix. I reproduced both gaps against current
So: please rebase onto |
0cc74df to
2efd822
Compare
test_all_expected_markets_present asserted the exact FALLBACK_CHAINS key set, so adding uk_equity (PR HKUDS#1206) failed CI on Python 3.11 and 3.14. Add uk_equity to the expected set and a uk chain-composition test mirroring the canada/vietnam ones.
Journals carrying VOD.L/HSBA.L/DCC.IL trades were inferred as 'other' (no .L/.IL branch), degrading shadow analysis to NaN price features and 'Other' rule labels. Add .L/.IL -> 'uk' inference, map 'uk' to uk_equity in the shadow extractor's registry key map, and label it 'UK equity' in rule translation (HKUDS#1206).
|
Thank you for doing the split — the four live commits are gone, the scope is LSE-only, and CI is 9/9. I also want to note the I cannot merge it yet, because the yfinance path infers the currency from the ticker suffix: if is_gbp_pence_symbol(symbol): # ^[A-Z0-9&.\-]+\.L$
normalized, _ = scale_pence_to_currency(normalized, "GBp")
So a backtest on The direct-Yahoo path in this same PR already does the right thing: it passes the chart meta's real Worth a test with a |
UK equities (LSE .L, ISE .IL) previously fell through _SOURCE_PATTERNS to the tushare default, were classified as a_share, and routed down the China fallback chain — ending in _unresolved after several seconds of network attempts, indistinguishable from a dead ticker. Add UK as a first-class market with parity to Canada/US across the routing surface: - market_data.py: _SOURCE_PATTERNS maps .L/.IL -> yahoo - _market_hooks.py: _MARKET_PATTERNS -> uk_equity, currency GBP, _detect_submarket -> uk - registry.py: FALLBACK_CHAINS uk_equity: [yahoo, yfinance, local] - runner.py: _MARKET_TO_SOURCE uk_equity -> yahoo; GlobalEquity set literals include uk_equity (prevents silent CryptoEngine routing) - composite.py: dispatch uk_equity -> GlobalEquityEngine(market=uk) Verified: VOD.L / SHEL.L / HSBA.L fetch 15 real LSE bars each under source=auto (served by yfinance after Yahoo fallback); engine dispatch, currency (GBP), and submarket (uk) all resolve. 109 tests pass.
Add comprehensive UK market coverage and close a volume-unit parity gap found while testing: - test_uk_equity_support.py: 17 tests across source detection, market classification, submarket, GBP currency, fallback chain, and backtest engine routing (GlobalEquityEngine market=uk, never CryptoEngine). - test_uk_backtest_smoke.py: end-to-end engine run on in-memory LSE bars, mirroring the India/Korea/Vietnam smoke convention; verifies fills, whole-share sizing, and the zero-commission generic equity path. - yahoo/yfinance loaders: declare volume_units["uk_equity"] = "shares" (was null/undeclared, so UK volume would read as unknown unit and break the provenance contract from HKUDS#1062/HKUDS#1065). 165 related tests pass; 21 new UK tests pass.
test_all_expected_markets_present asserted the exact FALLBACK_CHAINS key set, so adding uk_equity (PR HKUDS#1206) failed CI on Python 3.11 and 3.14. Add uk_equity to the expected set and a uk chain-composition test mirroring the canada/vietnam ones.
UK Main Market equities carry Stamp Duty Reserve Tax: 0.5% of consideration charged to the buyer, rounded to the nearest penny with an exact half-penny rounding UP (FA86/S99(13)). The generic GlobalEquity path charged zero, so a UK backtest ignored a real exchange-mandated cost. Add market='uk' support to GlobalEquityEngine: - uk_stamp_tax config key (default 0.005); uk_slippage defaults to slippage_us - calc_commission charges SDRT on purchase fills only (direction > 0, including buying to cover a short); sells pay nothing — unlike HK's bilateral stamp tax - Decimal ROUND_HALF_UP for the penny rounding: float round() is banker's and miscounts exact half-pennies (1.005 -> 1.00), which the HMRC manual requires to round up (13.455 -> 13.46) - _direction param renamed to direction (was reserved; UK uses it) Exemptions (UCITS ETFs, eligible AIM shares, gilts, new issues) are documented as the caller's concern — the engine applies the statutory Main Market rate. Verified against the HMRC manual's worked examples: 13.4547 -> 13.45, 13.455 -> 13.46. 172 related tests pass.
…loader Yahoo serves .L (always) and .IL (GBp-quoted) equities in pence: VOD.L was ~117p -> the engine priced a £1.17 share at £117, a 100x notional error on positions and SDRT. Normalize OHLC ÷100 when the source quotes GBp, at the loader boundary so every consumer (MCP tool, backtests, indicators) sees GBP values matching code_currency/engine currency: - yahoo_client.get_chart now returns (rows, currency) from chart meta - yahoo_loader: gate .L/.IL (they were silently skipped -> yfinance always served UK), scale when meta says GBp, declare price_units - yfinance_loader: scale .L/.IL via shared suffix rule (no meta there) - base: scale_pence_to_currency + is_gbp_pence_symbol helpers; bump loader cache version v4->v5 so pre-normalization pence parquet files are never served again (HKUDS#1062 precedent) - market_data provenance: currency_conversion reports 'GBp->GBP (÷100)' for UK from the serving loader's price_units; 'none' elsewhere Verified live: VOD.L close now 1.1715 GBP (was 117.15), HSBA.L 15.18 GBP (was 1514.6); notional and SDRT follow. 245 affected tests pass. Known follow-up: Yahoo-meta EUR-quoted .IL names (e.g. Irish corporates) pass through unmodified on the yahoo path, but the yfinance fallback's suffix rule would scale them; those symbols fetch no data today.
Journals carrying VOD.L/HSBA.L/DCC.IL trades were inferred as 'other' (no .L/.IL branch), degrading shadow analysis to NaN price features and 'Other' rule labels. Add .L/.IL -> 'uk' inference, map 'uk' to uk_equity in the shadow extractor's registry key map, and label it 'UK equity' in rule translation (HKUDS#1206).
…ahoo
UK had no fundamentals surface: .L/.IL symbols were rejected by the
statements tool (suffix error), while get_fundamentals stays US-only by
contract (SEC XBRL PIT panels; Yahoo has no filed-date PIT history, so
that boundary stays honest and loud). Add the feasible parity:
- _classify_market: .L/.IL -> uk
- _fetch_yahoo_statement: income/balance/cashflow from Yahoo quoteSummary
statement histories (crumb-gated client already exists); financialData +
defaultKeyStatistics for indicators; {raw, fmt, longFmt} flattened to
plain numbers; newest-first; capped periods
- Tool description/params + suffix error mention .L/.IL
- 4 UK tests (routing, flattening, indicators, failure envelope); live
verified VOD.L income 4 periods (netIncome -4.17B), balance 4, indicators 1
Yahoo histories are annual-only; a quarter request returns the annual
periods (documented degradation, matches Eastmoney's graceful fallback).
Adversarial review found the UK branch silently returned ANNUAL periods
for period='quarter' (an explicit 'del period'), whose docstring claimed
this 'matches Eastmoney' — false: Eastmoney fetches actual quarter
reports. Yahoo quoteSummary carries incomeStatementHistoryQuarterly /
balanceSheetHistoryQuarterly / cashflowStatementHistoryQuarterly for
LSE/ISE tickers, verified live for VOD.L (4 quarterly periods; Q3'25
revenue 10.45B). Map (statement, cadence) -> module + key so a quarter
request returns real quarters; quarterly endDate is {raw, fmt} and
flattens to int like every other value.
Adversarial round-trip review exposed a real bug: base.py's close callsite passes the POSITION side (before.direction), so closing a long (direction=1) was charged 0.5% SDRT on the SALE, and covering a short (direction=-1) skipped the charge on an actual PURCHASE. The unit tests only ever fed ideal trade-side values, so the round-trip path was never exercised. Trade side is direction on opens (1=buy-long, -1=short) and -direction on closes (1=close-long is a sale, -1=cover-short is a buy). Charge SDRT only when trade_is_buy, i.e. direction>0 if is_open else direction<0. HK (bilateral, sign-independent), Canada (rate * notional) and US (zero) are unaffected. Added close-path assertions to the smoke test.
get_stock_profile sends the ticker verbatim to Yahoo, so VOD.L already fetched its full profile — but _market_for classified everything non-HK as 'us', so a UK profile was reported as market='us'. Extend the classifier (.L/.IL -> uk), update the tool description and the parameter help to advertise the supported suffixes. Verified live: VOD.L profile sections all render; label now 'uk'.
…ll ref Review caught the two user-facing doc surfaces still claiming US/HK-only while the code fixes were already in: the MCP wrapper's get_stock_profile docstring (published to MCP clients) and the yfinance skill reference (tool_get_stock_profile.md — loaded into agent context: description, ticker help, market enum us|hk|uk, market-label rule, example list). Both now advertise .L/.IL; 7 profile tests pass.
Upstream's regression test (ad79b62) hardcoded the pre-UK 12-market set; this PR adds uk_equity as a 13th market, so the set must grow. Also pins uk_equity's default order (yahoo) and env var name.
User decision: 'just uk lse'. Empirical evidence backs it — 20+ .IL tickers (DCC, BIRG, AIBG, KRY, VOD, SHEL, HSBA, ULVR, BATS, DGE, RIO) all 404 at Yahoo; the surviving Irish surface is .IR/EUR, which is a separate market class (EUR currency, unverified Irish tax rules). Remove .IL from routing, market detection, submarket, loader gating, GBp pence detection, journal inference, statements/profile classify, MCP + skill docs, and tests (which now assert .IL is NOT supported). LSE .L remains the sole UK surface, live-verified.
Drops the residual LSE/ISE and UK/Irish wording in registry, base, yahoo_client, financial-statements, stock-profile and the yfinance skill reference to match the .L-only scope settled on HKUDS#1205.
… suffix Reviewer finding: the yfinance fallback inferred the quote currency from the .L ticker suffix and ÷100'd EVERY LSE line. But .L is not uniformly GBp — VUSA.L prices in GBP, VUSD.L in USD (Yahoo history_metadata, checked 2026-08-29): a backtest through the yfinance fallback would scale every absolute quantity (initial-capital sizing, lot rounding, min-notional checks, cross-market composite weights) while returns survive. The direct-Yahoo path already passes the chart meta's real currency into scale_pence_to_currency; the yfinance path now reads the same field via Ticker.history_metadata['currency'] and scales ONLY on GBp/p. A missing currency fails closed: no scale (never assume pence from the suffix). Rebased onto origin/main (e90b6c6, merged HKUDS#1244 era). Tests: GBP-quoted .L (VUSA.L) and USD-quoted .L (VUSD.L) pass through unscaled; absent currency unscaled (fail-closed); GBp still ÷100s (VOD.L).
2efd822 to
91aa401
Compare
…osed offline The earlier yfinance-currency commit made _declared_currency a real network call (yf.Ticker.history_metadata). The two pre-existing scaling tests mocked only _download_history, so CI (no/slow yfinance access) either failed or behaved nondeterministically — VOD.L could come back unscaled (fail-closed) or the symbol could be dropped. - Mock _declared_currency -> 'GBp' in test_fetch_scales_lse_pence_to_gbp and test_fetch_scales_other_lse_names: deterministic, offline-safe. - New test_fetch_declared_currency_failure_is_fail_closed: currency lookup returns None (the production fail-closed path) -> prices pass through unscaled and no exception escapes. 8/8 tests pass offline (no yfinance network access).
Review fix: yfinance currency (91aa401)
Finding: the yfinance fallback inferred currency from the
.Lsuffix and ÷100'd every LSE line — but.Lis not uniformly GBp. Verified against Yahooget_history_metadata()['currency'](2026-08-29): VOD.L=GBp, HSBA.L=GBp, IUSA.L=GBp, SGLN.L=GBp, VUSA.L=GBP, VUSD.L=USD. A backtest via the yfinance path would corrupt every absolute quantity (initial-capital sizing, lot rounding, min-notional, cross-market composite weights) while returns survive.Fix: the yfinance path now reads Yahoo's declared currency (
Ticker.history_metadata['currency'], same field the direct-Yahoo path already passes toscale_pence_to_currency) and scales only on GBp/p. A missing currency fails closed — "do not scale", never "assume pence" from the suffix. GBp names (VOD.L) still ÷100.Tests (in
test_yfinance_uk_scale.py): GBP-quoted.L(VUSA.L) unscaled; USD-quoted.L(VUSD.L) unscaled; absent currency unscaled (fail-closed); GBp still scaled.Branch rebased onto
origin/main(e90b6c6c); 16+1 commits.References
Scope
UK LSE (
.L) equity market support per #1205. Full parity: routing, classification, settlement currency, price units, engine dispatch, statutory SDRT, plus the journal/shadow and financial-statements downstream consumers. (Irish ISE.ILexplicitly out of scope.)What this adds
UK equities (LSE
.L) currently fall through_SOURCE_PATTERNSto thetusharedefault, get classifieda_share, and die on the China fallback chain with a misleading_unresolved(several seconds of network attempts, indistinguishable from a dead ticker). This makes UK a first-class market with full parity to Canada/US:_SOURCE_PATTERNS:.L→yahoo;FALLBACK_CHAINS:uk_equity: [yahoo, yfinance, local]_MARKET_PATTERNS→uk_equity;_detect_submarket→ukcode_currency→GBP(wasUNKNOWN:uk_equity)GlobalEquityEngine(market="uk"); never silent CryptoEngine fallthrough (2x set literals in runner, +_MARKET_TO_SOURCE)Decimal ROUND_HALF_UP; config keyuk_stamp_tax; trade-side detection is is_open-aware (opens: direction>0=buy; closes: -direction — closing a long is a sale, covering a short is a purchase, matching base.py's position-side close path); validated against HMRC manual worked examples (13.4547->13.45, 13.455->13.46)uk_equity: shares(was null/undeclared, breaking the fix(data): declare per-market volume units in market-data provenance #1065/[Bug] A-share volume unit inconsistency across fallback chain (lots vs shares, 100x) and missing unit metadata #1062 provenance contract)currency == "GBp"on the Yahoo path, suffix rule on the yfinance fallback) so every consumer sees GBP values matchingcode_currency; provenance declarescurrency_conversion: "GBp->GBP (/100)"; loader cache version bumped v4->v5 (pre-normalization parquet never served again, [Bug] A-share volume unit inconsistency across fallback chain (lots vs shares, 100x) and missing unit metadata #1062 precedent)_infer_market_from_symbol:.L→"uk"(was"other", silently degrading shadow-analysis price features to NaN); shadow_MARKET_KEY_MAP/labels mapuk→uk_equity,"UK equity"Verified
VOD.Lclose now GBP 1.1715 (was 117.15),HSBA.LGBP 15.184 (was 1514.6) — yahoo directKnown scope notes
.IR/EUR (BIRG.IR serves EUR data); routing it requires a separate market class (EUR currency, unverified Irish tax rules) — intentionally out of scope. This PR covers LSE.LonlyFollow-ups (not included)
MARKET_SPECShas no London session; missing market fails loud, never silent)