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).
|
Follow-up review found a second-order currency issue in my earlier guidance: allowing a USD-quoted |
… layer routes (#1565) #1575 taught identity and symbol search the .BA suffix. It was the third market to arrive this way. .L (#1206) and .VN had also landed in the backtest's _MARKET_PATTERNS / _MARKET_CURRENCY with no canonical-symbol scan, no venue and no currency in grounding. Symbol search labelled every result for India, Korea, the UK, Vietnam and Argentina "global". The two tables had nothing tying them to the one the backtest prices from. - identity: .L / .VN beside .BA in the scan, venue (lse / hose) and currency (GBP / VND). GBP matches the UK loaders' contract, which admits only a declared GBP/GBp quote and returns GBP. - symbol search: _MARKET_BY_SUFFIX, which the Eastmoney path already used, now has a row for every routed suffix, and the Yahoo path reads it instead of hand-written branches: in / kr / ca / ar / uk / vn. - test_market_identity_parity makes the backtest's currency table the list of markets. Each market needs a sample row checked against _detect_market, the identity scan, venue, currency (== _MARKET_CURRENCY) and the search label. Run against main's two files, it fails for ar, india, kr, uk and vietnam. Removed from #1575: the "ar" branch and aliases in candidate_market. That function is read only at identity.py:724, to match explicit market constraints. _extract_market_constraints produces only cn / hk / us, so an "ar" candidate was never compared to anything. Constraint words for more markets would be a separate change, and it fails closed on a mis-scoped constraint.
…d ARS (#1576) #1576 kept Yahoo's declared quote currency on every frame. Probing what it declares for .BA showed the case the ar_equity market (#1543) had missed. BYMA lists an issuer in pesos and again in dollars. Yahoo declares GGAL.BA / YPFD.BA / GOOGL.BA in ARS, and SPYD.BA / GGALD.BA / AAPLD.BA in USD (13.37 / 4.20 / 17.53 on 2026-09-24). The engine books every .BA line in one static ARS pool, so a dollar line was priced as pesos. The trailing D is not a rule: YPFD.BA is a peso line. This is the LSE contract (#1206) on a second venue. normalize_declared_quote_ currency admits a .BA frame only when it is declared ARS and still sends .L through the GBp/GBP normalizer. Every other declared currency is recorded as #1576 intended. The Yahoo loader calls it for every frame. The yfinance loader reads the declared currency for .L and .BA only (a metadata request each) and passes it through the same function. The ar_equity chain is yahoo -> yfinance -> local, so a dollar line can no longer reach the pool through a fallback. Grounding, same commit: - evidence.py: a market-data record takes the currency the provenance declares and falls back to the suffix. The answer must name the record's currency, so the suffix guess was the one enforced before. - policies.py: GBP / VND / ARS aliases (£, ₫, AR$, 英镑, 越南盾, 阿根廷比索). The previous commit gave .L / .VN / .BA a currency. Without the aliases an answer writing £1.17 took a correction round for not naming GBP. Mutants each fail: no ARS check (4 tests), yfinance not probing .BA (3), the record ignoring provenance (1), no aliases (4).
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)