Merge PR #1861: recognize digit-leading crypto bases
This commit is contained in:
commit
70a2801426
5 files changed
+93
-9
No files matched your search
@@ -119,7 +119,7 @@ def _normalize_symbol(code: str, market: str) -> str:
|
||||
if market == "crypto":
|
||||
if cleaned.endswith("/USDT"):
|
||||
return cleaned.replace("/", "-")
|
||||
if re.fullmatch(r"[A-Z]{2,}USDT", cleaned):
|
||||
if re.fullmatch(r"(?!\d+USDT)[A-Z0-9]{2,}USDT", cleaned):
|
||||
return f"{cleaned[:-4]}-USDT"
|
||||
return cleaned
|
||||
if market == "hk_equity":
|
||||
|
||||
@@ -119,17 +119,17 @@ _MARKET_PATTERNS = [
|
||||
# Vietnam equities: HOSE (VIC.VN). Tickers are three letters in practice;
|
||||
# the class stays broad to admit fund certificates and ETF codes.
|
||||
(re.compile(r"^[A-Z0-9]+\.VN$", re.I), "vietnam_equity"),
|
||||
(re.compile(r"^[A-Z]+-USDT$", re.I), "crypto"),
|
||||
(re.compile(r"^[A-Z]+/USDT$", re.I), "crypto"),
|
||||
(re.compile(r"^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+-USDT$", re.I), "crypto"),
|
||||
(re.compile(r"^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+/USDT$", re.I), "crypto"),
|
||||
# yfinance's native crypto spelling (BTC-USD, ETH-USD). Distinct from
|
||||
# USDT pairs only in the quote currency; both belong to CryptoEngine.
|
||||
(re.compile(r"^[A-Z]+-USD$", re.I), "crypto"),
|
||||
(re.compile(r"^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+-USD$", re.I), "crypto"),
|
||||
# Concatenated spot pairs (BTCUSDT, ETHUSDC) with no separator. Same
|
||||
# quote-asset table the trade-journal parser uses; without it these fell
|
||||
# through every pattern and got a_share rules (T+1, no shorting) on a
|
||||
# perpetual. Bare metals/FX (XAUUSD) end in USD, not USDT/USDC/BUSD, so
|
||||
# they still reach the forex whitelist below.
|
||||
(re.compile(r"^[A-Z]{2,}(?:USDT|USDC|BUSD)$", re.I), "crypto"),
|
||||
(re.compile(r"^(?!\d+(?:USDT|USDC|BUSD)$)[A-Z0-9]{2,}(?:USDT|USDC|BUSD)$", re.I), "crypto"),
|
||||
# China futures: product+delivery.exchange (e.g. IF2406.CFFEX, rb2410.SHFE)
|
||||
# Tushare suffix spellings (SHF/CZC/CFX/GFE) classify here too.
|
||||
(
|
||||
|
||||
@@ -115,7 +115,9 @@ def _run_card_data_sources(config: Dict[str, Any], loader: Any) -> List[str]:
|
||||
|
||||
# ─── Market detection (lightweight, for signal alignment only) ───
|
||||
|
||||
_CRYPTO_RE = _re.compile(r"^[A-Z]+-USDT$|^[A-Z]+/USDT$", _re.I)
|
||||
_CRYPTO_RE = _re.compile(
|
||||
r"^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+-USDT$|^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+/USDT$", _re.I
|
||||
)
|
||||
# Forex / metals in their explicit Yahoo notations, plus the bare-6-char
|
||||
# whitelist for XAUUSD / XAGUSD / XPTUSD / XPDUSD and G10 currencies. Mirrors
|
||||
# ``backtest.engines._market_hooks._MARKET_PATTERNS`` so the ffill-limit
|
||||
|
||||
@@ -48,8 +48,8 @@ _SOURCE_PATTERNS = [
|
||||
# Korea: KOSPI (005930.KS) / KOSDAQ (247540.KQ), 6-digit codes. Served by
|
||||
# pykrx (KRX public data, no auth); registry falls back to Yahoo/yfinance.
|
||||
(re.compile(r"^\d{6}\.(KS|KQ)$", re.I), "pykrx"),
|
||||
(re.compile(r"^[A-Z]+-USDT$", re.I), "okx"),
|
||||
(re.compile(r"^[A-Z]+/USDT$", re.I), "ccxt"),
|
||||
(re.compile(r"^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+-USDT$", re.I), "okx"),
|
||||
(re.compile(r"^(?=[A-Z0-9]*[A-Z])[A-Z0-9]+/USDT$", re.I), "ccxt"),
|
||||
# Iran: Nobitex IRT-quoted crypto pairs (BTCIRT / BTC-IRT). Toman-
|
||||
# denominated public UDF endpoint, no auth; explicit-source only (not in
|
||||
# the crypto fallback chain — non-IRT pairs would never resolve there).
|
||||
@@ -89,7 +89,7 @@ def detect_source(code: str) -> str:
|
||||
def normalize_crypto_symbol(code: str) -> str:
|
||||
"""Normalize USDT spot aliases without changing the requested quote asset."""
|
||||
cleaned = code.strip().upper()
|
||||
match = re.fullmatch(r"([A-Z]{2,})[-/]?USDT", cleaned)
|
||||
match = re.fullmatch(r"((?!\d+[-/]?USDT)[A-Z0-9]{2,})[-/]?USDT", cleaned)
|
||||
return f"{match.group(1)}-USDT" if match else code
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Crypto bases that start with a digit (1INCH, 1000PEPE) must stay crypto.
|
||||
|
||||
The USDT-pair patterns used ``[A-Z]+`` for the base asset, so ``1INCH-USDT``
|
||||
matched nothing and fell through to the ``a_share`` default: T+1 settlement,
|
||||
no shorting, CNY and the tushare source were applied to a crypto pair.
|
||||
Digit-only codes are equities (A-share / HK) and must be unaffected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from backtest.correlation import _normalize_symbol, infer_market
|
||||
from backtest.engines._market_hooks import _detect_market
|
||||
from backtest.engines.base import _CRYPTO_RE
|
||||
from src.market_data import detect_source, normalize_crypto_symbol
|
||||
|
||||
DIGIT_LEADING_PAIRS = [
|
||||
"1INCH-USDT",
|
||||
"1INCH/USDT",
|
||||
"1INCHUSDT",
|
||||
"1000PEPE-USDT",
|
||||
"1000PEPE/USDT",
|
||||
"1000SHIBUSDT",
|
||||
"1inch-usdt",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", DIGIT_LEADING_PAIRS)
|
||||
def test_digit_leading_base_is_classified_as_crypto(code: str) -> None:
|
||||
assert _detect_market(code.upper()) == "crypto"
|
||||
assert infer_market(code) == "crypto"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", ["1INCH-USDT", "1INCH/USDT", "1000PEPE-USDT", "1000PEPE/USDT"])
|
||||
def test_signal_alignment_regex_accepts_digit_leading_base(code: str) -> None:
|
||||
assert _CRYPTO_RE.match(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "source"),
|
||||
[("1INCH-USDT", "okx"), ("1000PEPE-USDT", "okx"), ("1INCH/USDT", "ccxt")],
|
||||
)
|
||||
def test_digit_leading_base_selects_crypto_source(code: str, source: str) -> None:
|
||||
assert detect_source(code) == source
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "expected"),
|
||||
[
|
||||
("1INCH/USDT", "1INCH-USDT"),
|
||||
("1inch-usdt", "1INCH-USDT"),
|
||||
("1INCHUSDT", "1INCH-USDT"),
|
||||
("1000SHIBUSDT", "1000SHIB-USDT"),
|
||||
("BTCUSDT", "BTC-USDT"),
|
||||
],
|
||||
)
|
||||
def test_digit_leading_base_normalizes_to_dash_pair(code: str, expected: str) -> None:
|
||||
assert normalize_crypto_symbol(code) == expected
|
||||
assert _normalize_symbol(code, "crypto") == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "market"),
|
||||
[
|
||||
("000001.SZ", "a_share"),
|
||||
("600000", "a_share"),
|
||||
("600000.SH", "a_share"),
|
||||
("0700.HK", "hk_equity"),
|
||||
("00700", "hk_equity"),
|
||||
],
|
||||
)
|
||||
def test_digit_only_equity_codes_are_not_captured(code: str, market: str) -> None:
|
||||
assert infer_market(code) == market
|
||||
assert not _CRYPTO_RE.match(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", ["600000USDT", "000001-USDT", "000001/USDT", "000001-USD"])
|
||||
def test_digit_only_base_is_never_crypto(code: str) -> None:
|
||||
assert _detect_market(code) != "crypto"
|
||||
assert not _CRYPTO_RE.match(code)
|
||||
assert normalize_crypto_symbol(code) == code
|
||||
Reference in new issue
Block a user