Merge PR #1543: feat: add Yahoo market-data routing for Argentina .BA

This commit is contained in:
Haozhe Wu committed 2026-09-23 11:41:47 +08:00
commit 5397f230ae
12 files changed
+75 -8

No files matched your search

+5 -1
View File
@@ -63,6 +63,9 @@ _MARKET_PATTERNS = [
# Canada equities: Toronto Stock Exchange (TD.TO) and TSX Venture
# (PNG.V). Yahoo carries both suffixes verbatim.
(re.compile(r"^[A-Z0-9&.\-]+\.(TO|V)$", re.I), "ca_equity"),
# Argentina: BYMA listings use Yahoo's canonical .BA suffix. Keep this
# as a distinct market so ARS can never be mixed with USD/CNY accounting.
(re.compile(r"^[A-Z0-9&.\-]+\.BA$", re.I), "ar_equity"),
# UK equities: London Stock Exchange (VOD.L, SHEL.L). Yahoo carries the
# suffix verbatim.
(re.compile(r"^[A-Z0-9&.\-]+\.L$", re.I), "uk_equity"),
@@ -164,6 +167,7 @@ _MARKET_CURRENCY = {
"india_equity": "INR",
"kr_equity": "KRW",
"ca_equity": "CAD",
"ar_equity": "ARS",
"uk_equity": "GBP",
"vietnam_equity": "VND",
# Every crypto pattern in _MARKET_PATTERNS is USDT-quoted, and USDT is
@@ -236,7 +240,7 @@ def _detect_market(code: str) -> str:
Returns:
Market type (a_share/us_equity/hk_equity/india_equity/kr_equity/
ca_equity/crypto/futures/forex).
ca_equity/ar_equity/crypto/futures/forex).
Bare 1-5 letter alphabetic tickers resolve to ``us_equity``;
bare 6-letter codes that start with a precious-metal or G10
currency code (whitelist) resolve to ``forex``; concatenated
+5
View File
@@ -50,6 +50,11 @@ def _build_rule_engines(config: dict, codes: List[str]) -> Dict[str, BaseEngine]
elif market == "vietnam_equity":
from backtest.engines.vietnam_equity import VietnamEquityEngine
engines["vietnam_equity"] = VietnamEquityEngine(config)
elif market == "ar_equity":
raise ValueError(
"Argentina .BA market data is supported, but Argentina backtest "
"execution rules are not modeled yet"
)
elif market == "ca_equity":
from backtest.engines.global_equity import GlobalEquityEngine
engines["ca_equity"] = GlobalEquityEngine(config, market="ca")
+2
View File
@@ -220,6 +220,8 @@ FALLBACK_CHAINS: dict[str, list[str]] = {
"kr_equity": ["pykrx", "yahoo", "yfinance", "local"],
# TSX (.TO) / TSX Venture (.V): direct Yahoo first, SDK fallback second.
"ca_equity": ["yahoo", "yfinance", "local"],
# Argentina (BYMA .BA): Yahoo public chart first, yfinance fallback.
"ar_equity": ["yahoo", "yfinance", "local"],
# UK (LSE .L): direct Yahoo first, SDK fallback second.
"uk_equity": ["yahoo", "yfinance", "local"],
# Vietnam (.VN): Yahoo lists HOSE only — HNX and UPCOM are unsupported,
+2 -2
View File
@@ -64,7 +64,7 @@ def _is_supported(code: str) -> bool:
return True
return upper.endswith(
(
".US", ".HK", ".NS", ".BO", ".KS", ".KQ", ".TO", ".V", ".VN",
".US", ".HK", ".NS", ".BO", ".KS", ".KQ", ".TO", ".V", ".VN", ".BA",
".L", "=F", "=X",
)
)
@@ -192,7 +192,7 @@ class DataLoader:
name = "yahoo"
markets = {
"us_equity", "hk_equity", "india_equity", "kr_equity", "ca_equity",
"vietnam_equity", "uk_equity",
"vietnam_equity", "uk_equity", "ar_equity",
}
# Yahoo chart volume is single shares for US/HK equities
# (HKUDS/Vibe-Trading#1062; HK verified 2026-08-11, 00700.HK ratio 1.00
+1 -1
View File
@@ -255,7 +255,7 @@ class DataLoader:
name = "yfinance"
markets = {
"us_equity", "hk_equity", "india_equity", "kr_equity", "ca_equity",
"vietnam_equity", "uk_equity", "crypto",
"vietnam_equity", "uk_equity", "ar_equity", "crypto",
}
# yfinance volume is single shares for US/HK equities
# (HKUDS/Vibe-Trading#1062; HK verified 2026-08-11, 00700.HK ratio 1.00
+10
View File
@@ -849,6 +849,7 @@ _MARKET_TO_SOURCE = {
"india_equity": "yahoo",
"kr_equity": "pykrx",
"ca_equity": "yahoo",
"ar_equity": "yahoo",
"uk_equity": "yahoo",
"vietnam_equity": "yahoo",
"crypto": "okx",
@@ -1531,6 +1532,15 @@ def _create_market_engine(source: str, config: dict, codes: List[str]):
if "vietnam_equity" in markets:
from backtest.engines.vietnam_equity import VietnamEquityEngine
return VietnamEquityEngine(config)
# Argentina market-data routing is supported, but BYMA execution rules
# are not modeled yet. Fail closed instead of silently applying US/crypto
# commissions, lot sizes, settlement, or short-selling assumptions.
if "ar_equity" in markets:
raise ValueError(
"Argentina .BA market data is supported, but Argentina backtest "
"execution rules are not modeled yet"
)
# Index symbols (^SPX, ^FTSE, ...) — priced like a US/global-listed
# instrument (GlobalEquityEngine, US rules) and never the China/crypto
# default the source-based fallback would pick.
+2
View File
@@ -28,6 +28,8 @@ _SOURCE_PATTERNS = [
(re.compile(r"^[A-Z0-9&.\-]+\.(NS|BO)$", re.I), "yahoo"),
# Canada: Toronto Stock Exchange (TD.TO) / TSX Venture (PNG.V).
(re.compile(r"^[A-Z0-9&.\-]+\.(TO|V)$", re.I), "yahoo"),
# Argentina: BYMA equities and CEDEARs use Yahoo's canonical .BA suffix.
(re.compile(r"^[A-Z0-9&.\-]+\.BA$", re.I), "yahoo"),
# UK: London Stock Exchange (VOD.L, SHEL.L). Yahoo serves the suffix
# verbatim; without this they fell through to the tushare default and were
# routed to China-market loaders that cannot resolve them.
+5 -2
View File
@@ -232,6 +232,8 @@ def _market_for(ticker: str) -> str:
return "hk"
if upper.endswith(".L"):
return "uk"
if upper.endswith(".BA"):
return "ar"
return "us"
@@ -240,7 +242,8 @@ class StockProfileTool(BaseTool):
name = "get_stock_profile"
description = (
"Fetch a read-only company profile for a US, Hong Kong, or UK (LSE .L) "
"Fetch a read-only company profile for a US, Hong Kong, UK (LSE .L), or "
"Argentina (BYMA .BA) "
"listing from Yahoo Finance: valuation key statistics, analyst price "
"targets and earnings/revenue estimates, institutional and insider "
"ownership, and the analyst recommendation trend. Use this for "
@@ -254,7 +257,7 @@ class StockProfileTool(BaseTool):
"ticker": {
"type": "string",
"description": (
"US, HK, or UK (LSE .L) symbol. US uses a bare or .US suffix "
"US, HK, UK (LSE .L), or Argentina (BYMA .BA) symbol. US uses a bare or .US suffix "
"(AAPL or AAPL.US); HK uses a zero-padded .HK code "
"(00700.HK); UK uses the .L suffix (VOD.L)."
),
@@ -0,0 +1,37 @@
"""Regression tests for Yahoo-backed Argentina (.BA) market routing.
These tests are deterministic and do not make network requests. They pin the
routing contract separately from Yahoo data availability.
"""
from __future__ import annotations
import pytest
from backtest.engines._market_hooks import _detect_market, code_currency
from backtest.loaders.registry import FALLBACK_CHAINS
from backtest.loaders.yahoo_loader import _is_supported as yahoo_is_supported
from backtest.loaders.yfinance_loader import _to_yfinance_symbol
from backtest.runner import _create_market_engine, _detect_source
from src.market_data import detect_source
from src.tools.stock_profile_tool import _market_for
@pytest.mark.parametrize("symbol", ["GGAL.BA", "PAMP.BA", "GOOGL.BA"])
def test_ba_symbols_share_one_market_data_contract(symbol: str) -> None:
assert _detect_market(symbol) == "ar_equity"
assert code_currency(symbol) == "ARS"
assert detect_source(symbol) == "yahoo"
assert _detect_source(symbol) == "yahoo"
assert yahoo_is_supported(symbol)
assert _to_yfinance_symbol(symbol) == symbol
assert _market_for(symbol) == "ar"
def test_argentina_fallback_chain_is_yahoo_first() -> None:
assert FALLBACK_CHAINS["ar_equity"] == ["yahoo", "yfinance", "local"]
def test_argentina_backtest_fails_closed_until_execution_rules_exist() -> None:
with pytest.raises(ValueError, match="Argentina backtest execution rules are not modeled"):
_create_market_engine("yahoo", {}, ["GGAL.BA"])
+4
View File
@@ -202,6 +202,7 @@ class TestFallbackChains:
"india_equity",
"kr_equity",
"ca_equity",
"ar_equity",
"uk_equity",
"vietnam_equity",
"crypto",
@@ -216,6 +217,9 @@ class TestFallbackChains:
def test_canada_chain_uses_only_compatible_sources(self) -> None:
assert FALLBACK_CHAINS["ca_equity"] == ["yahoo", "yfinance", "local"]
def test_argentina_chain_uses_only_compatible_sources(self) -> None:
assert FALLBACK_CHAINS["ar_equity"] == ["yahoo", "yfinance", "local"]
def test_vietnam_chain_uses_only_compatible_sources(self) -> None:
assert FALLBACK_CHAINS["vietnam_equity"] == ["yahoo", "yfinance", "local"]
+1 -1
View File
@@ -690,7 +690,7 @@ def test_get_data_source_settings_lists_default_source_orders(
orders = {entry["market"]: entry for entry in entries}
assert set(orders) == {
"a_share", "us_equity", "hk_equity", "india_equity", "kr_equity",
"ca_equity", "vietnam_equity", "uk_equity", "crypto", "futures",
"ca_equity", "ar_equity", "vietnam_equity", "uk_equity", "crypto", "futures",
"fund", "macro", "forex", "index",
}
a_share = orders["a_share"]
+1 -1
View File
@@ -390,7 +390,7 @@ class TestLoaderMetadata:
assert loader.name == "yahoo"
assert loader.markets == {
"us_equity", "hk_equity", "india_equity", "kr_equity", "ca_equity",
"vietnam_equity", "uk_equity",
"vietnam_equity", "uk_equity", "ar_equity",
}
assert loader.requires_auth is False
assert loader.is_available() is True