8563e4ea53
Signed-off-by: Dinger <quantdinger@gmail.com>
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""
|
||
中国A股数据源 — 多层 fallback
|
||
|
||
有 TWELVE_DATA_API_KEY:
|
||
所有周期 → Twelve Data(主) → 腾讯日/周线 → yfinance → AkShare
|
||
|
||
无 API Key:
|
||
分钟/小时 → yfinance → AkShare
|
||
日/周线 → 腾讯 fqkline → yfinance → AkShare
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Dict, List, Any, Optional
|
||
|
||
from app.data_sources.base import BaseDataSource
|
||
from app.data_sources.tencent import normalize_cn_code, fetch_quote, parse_quote_to_ticker, fetch_kline, tencent_kline_rows_to_dicts
|
||
from app.data_sources.asia_stock_kline import (
|
||
normalize_chart_timeframe,
|
||
fetch_twelvedata_klines,
|
||
fetch_yfinance_klines,
|
||
fetch_akshare_minute_klines,
|
||
fetch_akshare_weekly_klines,
|
||
)
|
||
from app.utils.logger import get_logger
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
class CNStockDataSource(BaseDataSource):
|
||
"""A股数据源(TwelveData + Tencent + yfinance + AkShare)"""
|
||
|
||
name = "CNStock/multi-source"
|
||
|
||
def get_ticker(self, symbol: str) -> Dict[str, Any]:
|
||
code = normalize_cn_code(symbol)
|
||
parts = fetch_quote(code)
|
||
if not parts:
|
||
return {"last": 0, "symbol": code}
|
||
t = parse_quote_to_ticker(parts)
|
||
return {
|
||
"last": t.get("last", 0),
|
||
"change": t.get("change", 0),
|
||
"changePercent": t.get("changePercent", 0),
|
||
"high": t.get("high", 0),
|
||
"low": t.get("low", 0),
|
||
"open": t.get("open", 0),
|
||
"previousClose": t.get("previousClose", 0),
|
||
"name": t.get("name", ""),
|
||
"symbol": code,
|
||
}
|
||
|
||
def get_kline(
|
||
self,
|
||
symbol: str,
|
||
timeframe: str,
|
||
limit: int,
|
||
before_time: Optional[int] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
code = normalize_cn_code(symbol)
|
||
tf = normalize_chart_timeframe(timeframe)
|
||
lim = max(int(limit or 300), 1)
|
||
|
||
# Tier 1: Twelve Data (paid, most reliable)
|
||
rows = fetch_twelvedata_klines(
|
||
is_hk=False, tencent_code=code, timeframe=tf, limit=lim, before_time=before_time
|
||
)
|
||
if rows:
|
||
return self.filter_and_limit(rows, limit=lim, before_time=before_time)
|
||
|
||
# Tier 2: Tencent for daily/weekly (fast, free)
|
||
if tf in ("1D", "1W"):
|
||
tf_map = {"1D": "day", "1W": "week"}
|
||
period = tf_map.get(tf, "day")
|
||
raw_rows = fetch_kline(code, period=period, count=lim, adj="qfq")
|
||
out = tencent_kline_rows_to_dicts(raw_rows)
|
||
if out:
|
||
return self.filter_and_limit(out, limit=lim, before_time=before_time)
|
||
|
||
# Tier 3: yfinance (works when Yahoo not rate-limited)
|
||
rows = fetch_yfinance_klines(
|
||
is_hk=False, tencent_code=code, timeframe=tf, limit=lim, before_time=before_time
|
||
)
|
||
if rows:
|
||
return self.filter_and_limit(rows, limit=lim, before_time=before_time)
|
||
|
||
# Tier 4: AkShare (fragile overseas, last resort)
|
||
if tf in ("1m", "5m", "15m", "30m", "1H", "4H"):
|
||
rows = fetch_akshare_minute_klines(
|
||
is_hk=False, tencent_code=code, timeframe=tf, limit=lim, before_time=before_time
|
||
)
|
||
elif tf == "1W":
|
||
rows = fetch_akshare_weekly_klines(
|
||
is_hk=False, tencent_code=code, limit=lim, before_time=before_time
|
||
)
|
||
else:
|
||
rows = []
|
||
|
||
return self.filter_and_limit(rows, limit=lim, before_time=before_time)
|