Files
qlib/backend/app/infrastructure/data_sources/sina.py
T
Simon 2da234220a feat(backend): Phase 1 数据层 — Domain / Provider / Failover 审计 + 持久化 + 同步 CLI
- domain:市场数据实体(Stock / 交易日历 / 日线 / 复权 / 财务含 announce_date)+ Repository 与 MarketDataProvider Protocol
- 数据源:TushareProvider(归一化、重试、鉴权错误归类)、SinaProvider(备用,明确前复权口径与能力边界)、FailoverProvider + SyncLog 审计(禁止静默切换)
- 持久化:SQLAlchemy 2.x Models + Repository 实现(按业务键幂等 upsert、as_of_date 防未来函数过滤)+ Alembic 迁移
- CLI:uv run python -m app.cli.sync {basic|calendar|daily|financial|verify},支持 --resume 断点续传
- 真实 Tushare 验证:stock 5556 / 交易日历 366 / daily+factor 242 / 财务 55;sync_log 审计完整
- 测试:38 passed(domain / provider / failover / repository / 未来函数 / 迁移),ruff clean
2026-09-06 16:59:28 +08:00

102 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""新浪财经 Provider —— 备用数据源。
能力边界(AGENT.md §5.2):
- 新浪日 K 接口返回**前复权**数据,口径与 Tushare 不复权不同,
因此本 Provider 只用于「缺失/不可用时的行情参考与交叉验证」,
不得把结果直接并入不复权主时序库(禁止静默混口径)。
- 新浪不提供复权因子 / 财务指标 → 相应方法抛 DataSourceNotSupported。
"""
from __future__ import annotations
import json
import re
import urllib.parse
import urllib.request
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from app.domain.entities.market import DailyBar
from app.infrastructure.data_sources.errors import (
DataSourceError,
DataSourceNotSupported,
)
_KLINE_JSONP = (
"https://quotes.sina.cn/cn/api/jsonp_v2.php/var%20data=/CN_MarketDataService"
".getKLineData?symbol={sina_symbol}&scale=240&ma=no&datalen={datalen}"
)
def _to_sina_symbol(symbol: str) -> str:
"""600519.SH -> sh600519;000001.SZ -> sz000001。"""
code, _, suffix = symbol.partition(".")
prefix = {"SH": "sh", "SZ": "sz", "BJ": "bj"}.get(suffix.upper(), "sh")
return f"{prefix}{code}"
def _extract_jsonp(payload: str) -> list[dict[str, Any]]:
match = re.search(r"=\s*(\[.*\])\s*$", payload.strip(), flags=re.DOTALL)
if not match:
raise DataSourceError("新浪行情返回格式无法解析")
return json.loads(match.group(1))
class SinaProvider:
"""新浪财经备用数据源(仅日线参考 / 交叉验证)。"""
name = "sina"
def __init__(self, *, timeout: float = 10.0, urlopen=urllib.request.urlopen) -> None:
self._timeout = timeout
self._urlopen = urlopen
def get_daily(self, symbol: str, start: date, end: date, datalen: int = 320) -> list[DailyBar]:
"""拉取前复权日 K(新浪仅支持最近 datalen 个自然日窗口)。"""
url = _KLINE_JSONP.format(sina_symbol=_to_sina_symbol(symbol), datalen=datalen)
try:
with self._urlopen(url, timeout=self._timeout) as resp:
payload = resp.read().decode("utf-8", errors="replace")
except Exception as exc: # noqa: BLE001
raise DataSourceError(f"sina 请求失败: {exc}") from exc
bars: list[DailyBar] = []
for rec in _extract_jsonp(payload):
day = datetime.strptime(rec["day"], "%Y-%m-%d").date()
if day < start or day > end:
continue
bars.append(
DailyBar(
symbol=symbol,
trade_date=day,
open=_d(rec.get("open")),
high=_d(rec.get("high")),
low=_d(rec.get("low")),
close=_d(rec.get("close")),
volume=_d(rec.get("volume")),
)
)
return bars
def get_stock_basic(self):
raise DataSourceNotSupported("新浪不提供股票基础信息列表")
def get_trade_cal(self, start, end):
raise DataSourceNotSupported("新浪不提供交易日历")
def get_adjust_factor(self, symbol, start, end):
raise DataSourceNotSupported("新浪不提供复权因子(返回数据为前复权口径)")
def get_financial(self, symbol):
raise DataSourceNotSupported("新浪不提供财务指标")
def _d(value) -> Decimal | None:
if value is None:
return None
try:
return Decimal(str(value))
except (ValueError, TypeError):
return None