feat: phase 1 — data downloader, backtrader runner, SMA strategy
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""Unified data downloader: akshare primary, baostock fallback."""
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
import pandas as pd
|
||||
import akshare as ak
|
||||
import baostock as bs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BAOSTOCK_FREQ_MAP = {"d": "d", "w": "w", "m": "m"} # baostock only supports daily
|
||||
|
||||
|
||||
def _download_akshare(symbol: str, start: str, end: str, adjust: str = "qfq") -> Optional[pd.DataFrame]:
|
||||
"""Download daily bars from akshare. Returns DataFrame with OHLCV columns."""
|
||||
try:
|
||||
# symbol format: 'sh600000' in akshare stock_zh_a_hist expects raw code like '600000'
|
||||
# strip exchange prefix for akshare
|
||||
raw = symbol.replace("sh", "").replace("sz", "")
|
||||
df = ak.stock_zh_a_hist(
|
||||
symbol=raw,
|
||||
period="daily",
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
adjust=adjust,
|
||||
)
|
||||
if df is None or df.empty:
|
||||
return None
|
||||
# Standardize columns
|
||||
col_map = {
|
||||
"日期": "date",
|
||||
"开盘": "open",
|
||||
"最高": "high",
|
||||
"最低": "low",
|
||||
"收盘": "close",
|
||||
"成交量": "volume",
|
||||
"成交额": "amount",
|
||||
}
|
||||
df = df.rename(columns=col_map)
|
||||
df["symbol"] = symbol
|
||||
return df[["symbol", "date", "open", "high", "low", "close", "volume", "amount"]]
|
||||
except Exception as e:
|
||||
logger.warning(f"akshare download failed for {symbol}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _download_baostock(symbol: str, start: str, end: str, frequency: str = "d") -> Optional[pd.DataFrame]:
|
||||
"""Download daily bars from baostock as fallback."""
|
||||
try:
|
||||
bs.login()
|
||||
# baostock format: sh.600000
|
||||
code = f"{symbol[:2]}.{symbol[2:]}"
|
||||
rs = bs.query_history_k_data_plus(
|
||||
code=code,
|
||||
fields="date,open,high,low,close,volume,amount",
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
frequency=frequency,
|
||||
adjustflag="2", # qfq
|
||||
)
|
||||
if rs.error_code != "0":
|
||||
logger.warning(f"baostock error for {symbol}: {rs.error_msg}")
|
||||
return None
|
||||
data_list = []
|
||||
while rs.next():
|
||||
data_list.append(rs.get_row_data())
|
||||
bs.logout()
|
||||
if not data_list:
|
||||
return None
|
||||
df = pd.DataFrame(data_list, columns=["date", "open", "high", "low", "close", "volume", "amount"])
|
||||
df[["open", "high", "low", "close", "volume", "amount"]] = df[
|
||||
["open", "high", "low", "close", "volume", "amount"]
|
||||
].astype(float)
|
||||
df["symbol"] = symbol
|
||||
return df[["symbol", "date", "open", "high", "low", "close", "volume", "amount"]]
|
||||
except Exception as e:
|
||||
logger.warning(f"baostock download failed for {symbol}: {e}")
|
||||
try:
|
||||
bs.logout()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def download_daily(
|
||||
symbol: str,
|
||||
start: str,
|
||||
end: str,
|
||||
adjust: str = "qfq",
|
||||
source: str = "auto",
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Download daily OHLCV data. Tries akshare first, falls back to baostock.
|
||||
|
||||
Args:
|
||||
symbol: Stock symbol like 'sh600000' or 'sz000001'
|
||||
start: Start date 'YYYY-MM-DD'
|
||||
end: End date 'YYYY-MM-DD'
|
||||
adjust: 'qfq' (forward-adjusted), 'hfq' (backward), '' (none)
|
||||
source: 'auto' (akshare then baostock fallback), 'akshare' only,
|
||||
or 'baostock' only
|
||||
|
||||
Returns:
|
||||
DataFrame with columns: symbol, date, open, high, low, close, volume, amount
|
||||
"""
|
||||
df = None
|
||||
if source in ("akshare", "auto"):
|
||||
df = _download_akshare(symbol, start, end, adjust)
|
||||
if df is None and source in ("baostock", "auto"):
|
||||
df = _download_baostock(symbol, start, end)
|
||||
|
||||
if df is None or df.empty:
|
||||
raise RuntimeError(f"Failed to download data for {symbol} from {start} to {end}")
|
||||
|
||||
df["date"] = pd.to_datetime(df["date"])
|
||||
df = df.sort_values("date").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def download_batch(
|
||||
symbols: list[str],
|
||||
start: str,
|
||||
end: str,
|
||||
adjust: str = "qfq",
|
||||
) -> dict[str, pd.DataFrame]:
|
||||
"""Download daily data for multiple symbols. Returns {symbol: DataFrame}."""
|
||||
results = {}
|
||||
for sym in symbols:
|
||||
try:
|
||||
results[sym] = download_daily(sym, start, end, adjust)
|
||||
logger.info(f"Downloaded {sym}: {len(results[sym])} bars")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed {sym}: {e}")
|
||||
return results
|
||||
Reference in New Issue
Block a user