提交
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
@@ -16,6 +17,7 @@ from ..config import settings
|
||||
from ..domain import Bar
|
||||
from ..models import Candle
|
||||
from . import akshare_provider, tushare_provider
|
||||
from .symbols import is_etf_symbol, to_ts_code
|
||||
|
||||
DEFAULT_START = settings.data_default_start or "20200101"
|
||||
|
||||
@@ -74,19 +76,19 @@ async def sync_symbol(
|
||||
# 增量:从缓存最后一根当天开始(重叠一天重新拉取,容忍数据源漏行/盘后修订)
|
||||
start = last_ts.strftime("%Y%m%d")
|
||||
start = start or DEFAULT_START
|
||||
errors: list[str] = []
|
||||
bars: list[Bar] = []
|
||||
used = None
|
||||
|
||||
for name, fn in _providers(source):
|
||||
# ETF 走 Tushare fund_daily(quicksync 镜像可用;与股票同源同控频),
|
||||
# 按ts_code 增量拉取,未收盘当日数据未生成时自然返回空。
|
||||
if is_etf_symbol(code):
|
||||
errors: list[str] = []
|
||||
try:
|
||||
# tushare/akshare 是同步网络 IO,丢到线程池避免阻塞事件循环;
|
||||
# adjust=None -> 不复权(复权在读取时按 adj_factor 换算)
|
||||
bars = await asyncio.to_thread(fn, code, start, end, None)
|
||||
used = name
|
||||
break
|
||||
bars = await asyncio.to_thread(_fetch_etf_daily, code, start, end)
|
||||
used = "tushare"
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append(f"{name}: {e}")
|
||||
errors.append(f"tushare: {e}")
|
||||
bars = []
|
||||
else:
|
||||
bars, used, errors = await _fetch_stock(code, start, end, source)
|
||||
|
||||
if not bars:
|
||||
if last_ts is not None:
|
||||
@@ -112,3 +114,47 @@ async def sync_symbol(
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
return {"symbol": code, "bars": len(bars), "source": used}
|
||||
|
||||
|
||||
def _fetch_etf_daily(code: str, start: str | None, end: str | None) -> list[Bar]:
|
||||
"""Tushare fund_daily 按 ts_code 拉 ETF 日线(同步网络 IO,to_thread 调用)。
|
||||
单位沿用 Tushare:vol 手、amount 千元,此处换算为 股/元。"""
|
||||
from .tushare_provider import get_pro
|
||||
|
||||
pro = get_pro()
|
||||
df = pro.fund_daily(ts_code=to_ts_code(code), start_date=start, end_date=end)
|
||||
if df is None or df.empty:
|
||||
raise RuntimeError(f"Tushare 无数据: {to_ts_code(code)}")
|
||||
df = df.sort_values("trade_date")
|
||||
bars: list[Bar] = []
|
||||
for _, r in df.iterrows():
|
||||
amt = r.get("amount")
|
||||
bars.append(
|
||||
Bar(
|
||||
ts=datetime.strptime(str(r["trade_date"]), "%Y%m%d"),
|
||||
open=float(r["open"]), high=float(r["high"]),
|
||||
low=float(r["low"]), close=float(r["close"]),
|
||||
volume=float(r["vol"]) * 100.0, # 手 -> 份
|
||||
amount=float(amt) * 1000.0 if amt is not None and amt == amt else None, # 千元 -> 元
|
||||
)
|
||||
)
|
||||
return bars
|
||||
|
||||
|
||||
async def _fetch_stock(code: str, start: str, end: str | None, source: str):
|
||||
"""Tushare 主 -> AKShare 兜底拉股票日线(不复权)。"""
|
||||
errors: list[str] = []
|
||||
bars: list[Bar] = []
|
||||
used = None
|
||||
|
||||
for name, fn in _providers(source):
|
||||
try:
|
||||
# tushare/akshare 是同步网络 IO,丢到线程池避免阻塞事件循环;
|
||||
# adjust=None -> 不复权(复权在读取时按 adj_factor 换算)
|
||||
bars = await asyncio.to_thread(fn, code, start, end, None)
|
||||
used = name
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append(f"{name}: {e}")
|
||||
|
||||
return bars, used, errors
|
||||
|
||||
@@ -28,14 +28,12 @@ from .. import cache
|
||||
from ..config import settings
|
||||
|
||||
# (tushare代码, 名称, 地区, 腾讯符号) —— 展示顺序即列表顺序
|
||||
# 标普500 腾讯符号是 s_usINX(不是 s_usSPX);恒生科技是 s_hkHSTECH(不是 HSTECH)
|
||||
# 首页聚焦中美(港股/国际指数在 /indexes 国际指数页);标普500 腾讯符号是 s_usINX(不是 s_usSPX)
|
||||
MARKET_INDEXES: list[tuple[str, str, str, str]] = [
|
||||
("000001.SH", "上证指数", "cn", "s_sh000001"),
|
||||
("399001.SZ", "深证成指", "cn", "s_sz399001"),
|
||||
("399006.SZ", "创业板指", "cn", "s_sz399006"),
|
||||
("000688.SH", "科创50", "cn", "s_sh000688"),
|
||||
("HSI", "恒生指数", "hk", "s_hkHSI"),
|
||||
("HKTECH", "恒生科技", "hk", "s_hkHSTECH"),
|
||||
("DJI", "道琼斯", "us", "s_usDJI"),
|
||||
("IXIC", "纳斯达克", "us", "s_usIXIC"),
|
||||
("SPX", "标普500", "us", "s_usINX"),
|
||||
@@ -49,7 +47,7 @@ _TENCENT_URL = "http://qt.gtimg.cn/q=" + ",".join([*_TENCENT_MAP.values(), _TENC
|
||||
_SPARK_DAYS = 45 # 迷你走势取最近 45 个交易日收盘
|
||||
_HISTORY_DAYS = 150 # 日历日窗口(约 100 个交易日,够取 spark)
|
||||
_CALL_INTERVAL = 0.12 # 顺序调用间隔(秒),对 tushare 控频
|
||||
_EOD_KEY = "market_overview:eod:v2" # v2:与旧整包缓存 v1 的 payload 形状不同,天然隔离
|
||||
_EOD_KEY = "market_overview:eod:v3" # v3:首页列表移除港股(移入国际指数页),与 v2 隔离
|
||||
_AMOUNT_HIST_CAL_DAYS = 190 # 成交额历史的日历日窗口(≈128 交易日)
|
||||
_AMOUNT_HIST_BARS = 120 # 输出的柱数(取尾部)
|
||||
_LIVE_FAIL_TTL = 15.0 # live 层失败负缓存(秒)
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
"""A 股代码归一化。支持 6 位纯数字或带交易所后缀(000001 / 000001.SZ)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
# 场内 ETF 代码前缀(与股票区间不重叠):沪 51/56/58 开头、深 159 开头。
|
||||
# 50/57/16 开头是 LOF,不在此列。
|
||||
_ETF_PREFIXES = ("51", "56", "58", "159")
|
||||
|
||||
|
||||
def plain_code(code: str) -> str:
|
||||
"""000001.SZ -> 000001"""
|
||||
return code.strip().upper().split(".")[0]
|
||||
|
||||
|
||||
def is_etf_symbol(code: str) -> bool:
|
||||
"""按 6 位纯代码前缀判断是否场内 ETF(fetcher 据此路由到东财数据源)。"""
|
||||
c = plain_code(code)
|
||||
return len(c) == 6 and c.startswith(_ETF_PREFIXES)
|
||||
|
||||
|
||||
def to_ts_code(code: str) -> str:
|
||||
"""转 Tushare ts_code(带交易所后缀)。"""
|
||||
c = code.strip().upper()
|
||||
if "." in c:
|
||||
return c
|
||||
c = plain_code(c)
|
||||
# 沪市:60xxxx 主板、68xxxx 科创、9xxxxx B 股
|
||||
if c.startswith(("60", "68", "9")):
|
||||
# 沪市:60xxxx 主板、68xxxx 科创、9xxxxx B 股、5xxxxx 基金(51/56/58 场内 ETF)
|
||||
if c.startswith(("60", "68", "9", "51", "56", "58")):
|
||||
return c + ".SH"
|
||||
# 深市:00xxxx 主板/中小、30xxxx 创业、20xxxx B 股
|
||||
if c.startswith(("00", "30", "20")):
|
||||
# 深市:00xxxx 主板/中小、30xxxx 创业、20xxxx B 股、159xxx 场内 ETF
|
||||
if c.startswith(("00", "30", "20", "159")):
|
||||
return c + ".SZ"
|
||||
# 北交所:8xxxxx / 4xxxxx
|
||||
if c.startswith(("8", "4")):
|
||||
|
||||
Reference in New Issue
Block a user