看股功能更新
This commit is contained in:
122
backend/scripts/backfill_adj_factor.py
Normal file
122
backend/scripts/backfill_adj_factor.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""全量回补历史复权因子(adj_factor 表)。
|
||||
|
||||
用法(在 backend 目录下):
|
||||
uv run python scripts/backfill_adj_factor.py # 从 candles 最早日期回补到今天
|
||||
uv run python scripts/backfill_adj_factor.py --start 20180101
|
||||
uv run python scripts/backfill_adj_factor.py --force # 已有日期也重拉
|
||||
|
||||
- 按交易日逐日拉取全市场因子(pro.adj_factor(trade_date=...)),幂等可断点续跑;
|
||||
- 交易日取自本地 trade_calendar(缓存不到的区间自动刷新一次日历);
|
||||
- Tushare 每分钟限频由 _call_retry 自动等待 62s 重试。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from sqlalchemy import delete, func, insert, select
|
||||
|
||||
from app.db import async_session
|
||||
from app.models import AdjFactor, Candle, TradeCalendar
|
||||
from app.screener.market_sync import _call_retry, _get_pro, _norm_date, _parse_d
|
||||
|
||||
_INTERVAL_MSG = 20 # 每完成 N 个交易日打印一次进度
|
||||
|
||||
|
||||
async def _calendar_dates(start: str, end: str) -> list[str]:
|
||||
"""[start, end] 交易日(升序)。本地日历覆盖不足时直接拉宽范围日历并回写缓存。"""
|
||||
async with async_session() as session:
|
||||
all_cached = set((await session.execute(select(TradeCalendar.trade_date))).scalars().all())
|
||||
cached = sorted(d for d in all_cached if start <= d <= end)
|
||||
if cached and min(cached) <= start:
|
||||
return cached
|
||||
|
||||
# 覆盖不到起点:按需拉宽范围日历(trade_cal 低积分限频 1 次/小时,失败沿用缓存)
|
||||
pro = _get_pro()
|
||||
try:
|
||||
cal = await asyncio.to_thread(
|
||||
_call_retry, pro.trade_cal, exchange="SSE", start_date=start, end_date=end, is_open="1"
|
||||
)
|
||||
dates = sorted(cal["cal_date"].tolist())
|
||||
except Exception as e: # noqa: BLE001
|
||||
if not cached:
|
||||
raise
|
||||
print(f"交易日历拉取受限({str(e)[:100]}),沿用本地缓存")
|
||||
return cached
|
||||
fresh = [d for d in dates if d not in all_cached]
|
||||
if fresh:
|
||||
async with async_session() as session:
|
||||
await session.execute(insert(TradeCalendar), [{"trade_date": d} for d in fresh])
|
||||
await session.commit()
|
||||
return dates
|
||||
|
||||
|
||||
async def _existing_dates() -> set[str]:
|
||||
async with async_session() as session:
|
||||
res = await session.execute(select(func.distinct(AdjFactor.trade_date)))
|
||||
return {_norm_date(r[0]) for r in res}
|
||||
|
||||
|
||||
async def main(start: str, end: str, force: bool) -> None:
|
||||
# 默认起点:candles 最早日线(因子只需覆盖有 K 线的区间)
|
||||
if start is None:
|
||||
async with async_session() as session:
|
||||
first = await session.scalar(select(func.min(Candle.ts)).where(Candle.timeframe == "1d"))
|
||||
start = first.strftime("%Y%m%d") if first else "20050101"
|
||||
if end is None:
|
||||
end = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
dates = await _calendar_dates(start, end)
|
||||
have = set() if force else await _existing_dates()
|
||||
todo = [d for d in dates if d not in have]
|
||||
print(f"区间 {start}~{end} 共 {len(dates)} 个交易日,待回补 {len(todo)} 个(已有 {len(dates) - len(todo)})")
|
||||
if not todo:
|
||||
return
|
||||
|
||||
pro = _get_pro()
|
||||
done = 0
|
||||
for d in todo:
|
||||
time.sleep(0.15) # 轻微控频;分钟级限频由 _call_retry 自动等待重试
|
||||
df = None
|
||||
for attempt in range(5): # 网络抖动(超时/断连)也重试,_call_retry 只兜限频
|
||||
try:
|
||||
df = _call_retry(pro.adj_factor, trade_date=d) # noqa: 线性脚本直接同步调用
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
wait = min(30 * (attempt + 1), 120)
|
||||
print(f" {d} 拉取异常({str(e)[:80]}),{wait}s 后重试 {attempt + 1}/5")
|
||||
time.sleep(wait)
|
||||
if df is None:
|
||||
print(f" {d} 连续 5 次失败,跳过(断点续跑可补)")
|
||||
continue
|
||||
if df is None or df.empty:
|
||||
print(f" {d} 无数据(非交易日或未生成),跳过")
|
||||
continue
|
||||
rows = [
|
||||
{"trade_date": _parse_d(d), "ts_code": r["ts_code"], "adj_factor": float(r["adj_factor"])}
|
||||
for _, r in df.iterrows()
|
||||
]
|
||||
async with async_session() as session:
|
||||
dt = _parse_d(d)
|
||||
await session.execute(delete(AdjFactor).where(AdjFactor.trade_date == dt))
|
||||
await session.execute(insert(AdjFactor), rows)
|
||||
await session.commit()
|
||||
done += 1
|
||||
if done % _INTERVAL_MSG == 0 or done == len(todo):
|
||||
print(f" 进度 {done}/{len(todo)}({d},+{len(rows)} 行)")
|
||||
print(f"回补完成:{done} 个交易日")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser(description="全量回补历史复权因子")
|
||||
ap.add_argument("--start", default=None, help="YYYYMMDD,默认 candles 最早日期")
|
||||
ap.add_argument("--end", default=None, help="YYYYMMDD,默认今天")
|
||||
ap.add_argument("--force", action="store_true", help="已有日期也重拉")
|
||||
a = ap.parse_args()
|
||||
asyncio.run(main(a.start, a.end, a.force))
|
||||
Reference in New Issue
Block a user