feat(data): v2 数据下载(SSH模式 STOCK_MOUNT+rsync) + launchd定时 + 文档
- v1 data_platform 脚本复制到 v2/scripts/data_platform/ 维护 - daily_all_update.py: 路径用 STOCK_MOUNT env(SSH模式) + STOCK_LIMIT(验证) - run_daily_update.sh: rsync拉NAS→本地 + v1增量 + 推NAS(SSH key免密,不挂载) - launchd com.sanguo.data-update 每日15:30(替代crontab,macOS FDA限制) - 验证: STOCK_LIMIT=2 updated=2 records=24 拉到当天 - 文档 docs/deployment/data-download.md
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""多源降级管理器 - 日线(akshare→腾讯) + 实时(新浪→东财→腾讯)"""
|
||||
import pandas as pd
|
||||
import urllib.request
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FallbackManager:
|
||||
def __init__(self):
|
||||
self._source_used = ""
|
||||
|
||||
def get_source_used(self) -> str:
|
||||
return self._source_used
|
||||
|
||||
def get_daily(self, symbol: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||||
"""获取日线数据,降级链:akshare → 腾讯K线"""
|
||||
# 1. akshare
|
||||
try:
|
||||
df = self._fetch_akshare_daily(symbol, start_date, end_date)
|
||||
if df is not None and not df.empty:
|
||||
self._source_used = "akshare"
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.warning(f"akshare日线失败 {symbol}: {e}")
|
||||
|
||||
# 2. 腾讯K线
|
||||
try:
|
||||
df = self._fetch_tencent_daily(symbol, start_date, end_date)
|
||||
if df is not None and not df.empty:
|
||||
self._source_used = "tencent_kline"
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.warning(f"腾讯K线失败 {symbol}: {e}")
|
||||
|
||||
raise RuntimeError(f"所有日线数据源失败: {symbol} {start_date}~{end_date}")
|
||||
|
||||
def get_realtime(self, symbol: str) -> dict:
|
||||
"""获取实时行情,降级链:新浪→东财→腾讯"""
|
||||
from realtime import get_realtime_quote
|
||||
result = get_realtime_quote(symbol)
|
||||
self._source_used = result.get("source", "unknown")
|
||||
return result
|
||||
|
||||
def _fetch_akshare_daily(self, symbol: str, start_date: str, end_date: str) -> Optional[pd.DataFrame]:
|
||||
import akshare as ak
|
||||
code = symbol.replace("SH", "").replace("SZ", "").replace("sh", "").replace("sz", "")
|
||||
s = start_date.replace("-", "")
|
||||
e = end_date.replace("-", "")
|
||||
df = ak.stock_zh_a_hist(symbol=code, period="daily", start_date=s, end_date=e, adjust="")
|
||||
if df is None or df.empty:
|
||||
return None
|
||||
df = df.rename(columns={"日期": "date", "开盘": "open", "收盘": "close",
|
||||
"最高": "high", "最低": "low", "成交量": "volume",
|
||||
"成交额": "amount"})
|
||||
df["date"] = pd.to_datetime(df["date"]).dt.strftime("%Y-%m-%d")
|
||||
for c in ["open", "high", "low", "close", "volume", "amount"]:
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0)
|
||||
return df[["date", "open", "high", "low", "close", "volume", "amount"]]
|
||||
|
||||
def _fetch_tencent_daily(self, symbol: str, start_date: str, end_date: str) -> Optional[pd.DataFrame]:
|
||||
"""腾讯K线API获取日线"""
|
||||
code = symbol.replace("SH", "").replace("SZ", "").replace("sh", "").replace("sz", "")
|
||||
if code.startswith(("6", "5", "1")):
|
||||
prefix = "sh"
|
||||
else:
|
||||
prefix = "sz"
|
||||
tq_symbol = f"{prefix}{code}"
|
||||
|
||||
days = (datetime.strptime(end_date, "%Y-%m-%d") - datetime.strptime(start_date, "%Y-%m-%d")).days + 10
|
||||
url = f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={tq_symbol},day,{start_date},,{days},"
|
||||
try:
|
||||
import urllib.request, json as _json
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with opener.open(req, timeout=10) as r:
|
||||
resp = _json.loads(r.read())
|
||||
d = resp.get("data")
|
||||
if not isinstance(d, dict):
|
||||
return None
|
||||
klines = d.get(tq_symbol, {}).get("day", [])
|
||||
if not klines:
|
||||
return None
|
||||
df = pd.DataFrame(klines)
|
||||
ncols = len(df.columns)
|
||||
if ncols >= 7:
|
||||
df.columns = ["date", "open", "close", "high", "low", "volume", "amount"][:ncols]
|
||||
else:
|
||||
df.columns = ["date", "open", "close", "high", "low", "volume"][:ncols]
|
||||
if "amount" not in df.columns:
|
||||
df["amount"] = 0.0
|
||||
for c in ["open", "close", "high", "low", "volume", "amount"]:
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0)
|
||||
df["date"] = pd.to_datetime(df["date"]).dt.strftime("%Y-%m-%d")
|
||||
mask = (df["date"] >= start_date) & (df["date"] <= end_date)
|
||||
return df.loc[mask, ["date", "open", "high", "low", "close", "volume", "amount"]]
|
||||
except Exception as e:
|
||||
logger.warning(f"腾讯K线请求失败: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user