feat(data): raw真实价数据源(task#79)—raw_dir+dir_key路由+新浪源重下

根因: daily_dir mixed-adjust(hfq bulk+akshare raw tail)致3-30 -94%假跌。
方案(Linus三问简化单raw, 除权留分期项#3):
- datareader read_parquet_daily/15min 加 dir_key 参数
- data_source iter_bars/fetch_day: adjust=raw→raw_dir(缺配置报错防混源), qfq→daily_dir
- engine PaperEngine 默认 adjust=raw
- config 加 raw_dir; scripts/raw_redownload.py 新浪源adjust='' 直连+单线程限速
- 验证: 浦发606行close 6.5/14.6 mean10.08 0跳变, 撮合成交价9.71-10.25真实
- 测试9/9+trader全量108/108通过
This commit is contained in:
2026-07-07 22:19:11 +08:00
parent fc39b549cf
commit 1ed7b72aca
10 changed files with 366 additions and 43 deletions
+6 -1
View File
@@ -245,7 +245,12 @@ class SourceHealthMonitor:
# ======================== 数据源:BaoStock ========================
def fetch_baostock_daily(code: str, start_date: str, end_date: str) -> Optional[pd.DataFrame]:
"""BaoStock日线:全量历史,无反爬,amount真实,T+1延迟"""
"""BaoStock日线:全量历史,无反爬,amount真实,T+1延迟
注意:adjustflag="2"=qfq;但 Mac/容器无 baostockHAS_BAOSTOCK=False)时不调用,
实际走 akshare fallbackadjust=""=raw)→ daily_dir 可能 mixed adjusttask #79 根因)。
干净单一 raw 见 raw_redownload.py → raw_dir。
"""
if not HAS_BAOSTOCK:
return None
bs_code = code_to_baostock(code)
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""raw 日线重下(task #79):akshare 新浪源 adjust="" 真实价。
根因:daily_dir 历史数据是 mixed-adjusthfq bulk + akshare raw tail 拼接),
3-30 类单日 -94% 假跌。本脚本从头重下**单一 raw** 到 raw_dir,与 daily_dir 同构
{prefix}{symbol}_daily.parquet,按 year 分目录),datareader 直接读。
**约束(用户反馈,见 memory/feedback-data-download-constraints**
- 直连不走代理(unset proxy env + NO_PROXY=*
- 单线程 + SLEEP 间隔限速(不并发猛打,防数据源封 IP)
用法:
# 验证单只/几只
python3 raw_redownload.py --symbols 600000,000001 --start 2024-01-01
# 全市场(读 STOCK_LIST csv,后台跑)
python3 raw_redownload.py --all --start 2024-01-01
env:
RAW_DIR 本地 raw 根(默认 /tmp/stock_dl/A股数据/日线数据/raw
SLEEP 每只请求间隔秒(默认 1.0,限速防封)
STOCK_LIST stock_basic_info csv 路径(--all 读,默认拉到本地的 csv)
"""
import argparse
import csv
import logging
import os
import sys
import time
# 直连:进程级 unset 代理(用户约束)
for _k in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"]:
os.environ.pop(_k, None)
os.environ["NO_PROXY"] = "*"
os.environ["no_proxy"] = "*"
import pandas as pd # noqa: E402
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("raw_redl")
RAW_DIR = os.environ.get("RAW_DIR", "/tmp/stock_dl/A股数据/日线数据/raw")
SLEEP = float(os.environ.get("SLEEP", "1.0"))
DEFAULT_STOCK_LIST = "/tmp/stock_dl/A股数据/stock_info/stock_basic_info_raw_20260326_113530.csv"
def prefix_for(code: str) -> str:
"""sh/sz 前缀(与 datareader.guess_exchange 一致)。"""
return "sh" if code.startswith(("60", "68", "51", "56", "58")) else "sz"
def download_one(ak, code: str, start: str, end: str):
"""新浪源拉 raw 日线,返回 (df, None) 或 (None, err)。"""
sym = f"{prefix_for(code)}{code}"
try:
df = ak.stock_zh_a_daily(
symbol=sym,
start_date=start.replace("-", ""),
end_date=end.replace("-", ""),
adjust="", # raw 不复权
)
except Exception as e: # noqa: BLE001
return None, f"{type(e).__name__}: {str(e)[:100]}"
if df is None or df.empty:
return None, "empty"
df["date"] = pd.to_datetime(df["date"])
df["year"] = df["date"].dt.year
return df, None
def save_one(code: str, df) -> int:
"""按 year 分组写 parquet(与 daily_dir 同构),返回写入行数。"""
n = 0
for year, g in df.groupby("year"):
ydir = os.path.join(RAW_DIR, str(int(year)))
os.makedirs(ydir, exist_ok=True)
out = os.path.join(ydir, f"{prefix_for(code)}{code}_daily.parquet")
g.drop(columns=["year"]).to_parquet(out)
n += len(g)
return n
def load_all_codes(stock_list: str) -> list[str]:
"""从 stock_basic_info csv 读代码列表(容错列名)。"""
codes = []
with open(stock_list, encoding="utf-8", errors="replace") as f:
reader = csv.DictReader(f)
for row in reader:
for k in ("code", "symbol", "ts_code", "代码", "股票代码"):
if k in row and row[k]:
c = str(row[k]).strip().split(".")[0]
if c.isdigit() and len(c) == 6:
codes.append(c)
break
return codes
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", help="逗号分隔代码,如 600000,000001")
ap.add_argument("--all", action="store_true", help="全市场(读 STOCK_LIST csv")
ap.add_argument("--start", default="2024-01-01")
ap.add_argument("--end", default=None, help="默认今天")
args = ap.parse_args()
end = args.end or time.strftime("%Y-%m-%d")
if args.symbols:
codes = [c.strip() for c in args.symbols.split(",") if c.strip()]
elif args.all:
sl = os.environ.get("STOCK_LIST", DEFAULT_STOCK_LIST)
if not os.path.exists(sl):
log.error("STOCK_LIST 不存在: %s(先跑 run_daily_update.sh 拉取)", sl)
sys.exit(1)
codes = load_all_codes(sl)
log.info("--all 从 %s 读到 %d", sl, len(codes))
else:
ap.error("需指定 --symbols 或 --all")
import akshare as ak
import warnings
warnings.filterwarnings("ignore")
ok = fail = rows = 0
for i, code in enumerate(codes, 1):
df, err = download_one(ak, code, args.start, end)
if df is None:
fail += 1
log.warning("[%d/%d] %s FAIL %s", i, len(codes), code, err)
else:
try:
n = save_one(code, df)
ok += 1
rows += n
log.info("[%d/%d] %s ok %d rows", i, len(codes), code, n)
except Exception as e: # noqa: BLE001
fail += 1
log.error("[%d/%d] %s SAVE FAIL %s", i, len(codes), code, e)
time.sleep(SLEEP) # 限速(用户约束)
log.info("=== 完成: ok=%d fail=%d rows=%draw_dir=%s ===", ok, fail, rows, RAW_DIR)
if __name__ == "__main__":
main()