提交
This commit is contained in:
@@ -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 单语句参数上限 32766,10 列/行)
|
||||
# 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 列表"
|
||||
|
||||
Reference in New Issue
Block a user