看股功能更新

This commit is contained in:
2026-08-16 00:05:26 +08:00
parent 9cce670b74
commit fc86fe0674
28 changed files with 3823 additions and 96 deletions

View 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))