This commit is contained in:
2026-09-09 11:35:02 +08:00
parent bc1c72d558
commit d656c05b3d
35 changed files with 711 additions and 3194 deletions

View File

@@ -20,7 +20,7 @@ from __future__ import annotations
import asyncio
import time
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from sqlalchemy import delete, func, select, text
from sqlalchemy.dialects.postgresql import insert as pg_insert
@@ -29,6 +29,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from .. import cache
from ..config import settings
from . import etf_provider
from .sync_utils import call_retry, get_pro_lazy, utcnow
# 进程内单例任务状态uvicorn 单进程场景够用)
_state: dict = {
@@ -46,34 +47,6 @@ _lock = asyncio.Lock()
_BATCH = 3000 # upsert 分批行数asyncpg 单语句参数上限 3276610 列/行)
# fund_daily 返回全市场基金 ~2100 行,一天一批远小于上限
# 频率超限特征(等待 62s 重试一次;与 screener.market_sync._call_retry 同款语义)
_RATE_MARKS = ("频率超限", "每分钟")
def _call_retry(fn, *args, **kwargs):
"""同步调用 tushare 接口;「每分钟」级频率超限等 62s 重试一次。"""
try:
return fn(*args, **kwargs)
except Exception as e: # noqa: BLE001
msg = str(e)
if any(m in msg for m in _RATE_MARKS) and "小时" not in msg:
time.sleep(62)
return fn(*args, **kwargs)
raise
def _get_pro():
"""token 检查 + 返回 pro api 客户端(同步对象,调用需 to_thread 包裹)。"""
if not settings.tushare_token:
raise RuntimeError("未配置 TUSHARE_TOKEN无法同步 ETF 日线backend/.env")
from .tushare_provider import get_pro
return get_pro()
def _utcnow() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
def _parse_d(s: str) -> datetime:
return datetime.strptime(str(s), "%Y%m%d")
@@ -85,7 +58,7 @@ async def _sync_spot(session: AsyncSession) -> int:
async with etf_provider.new_client() as client:
rows = await etf_provider.fetch_etf_spot(client)
now = _utcnow()
now = utcnow()
stmt = pg_insert(EtfBasic).values([{**r, "updated_at": now} for r in rows])
stmt = stmt.on_conflict_do_update(
index_elements=["ts_code"],
@@ -108,7 +81,7 @@ async def _sync_spot(session: AsyncSession) -> int:
def _fetch_day_sync(pro, d: str) -> list[dict]:
"""拉某交易日全市场场内基金日线fund_daily未生成的日期返回空"""
time.sleep(settings.screener_sync_interval)
df = _call_retry(pro.fund_daily, trade_date=d)
df = call_retry(pro.fund_daily, trade_date=d)
if df is None or df.empty:
return []
rows = []
@@ -128,7 +101,7 @@ def _fetch_day_sync(pro, d: str) -> list[dict]:
def _fetch_symbol_sync(pro, ts_code: str, start: str | None, end: str | None) -> list[dict]:
"""按 ts_code 增量/全量拉单只 ETF 日线start=None 即上市以来全量)。"""
time.sleep(settings.screener_sync_interval)
df = _call_retry(pro.fund_daily, ts_code=ts_code, start_date=start, end_date=end)
df = call_retry(pro.fund_daily, ts_code=ts_code, start_date=start, end_date=end)
if df is None or df.empty:
return []
df = df.sort_values("trade_date")
@@ -209,7 +182,7 @@ async def _run_sync(full: bool) -> None:
from ..models import Candle, EtfBasic, TradeCalendar
try:
pro = await asyncio.to_thread(_get_pro)
pro = await asyncio.to_thread(get_pro_lazy)
# 1) 快照 -> etf_basic
_state["step"] = "正在拉取 ETF 列表"