summaryrefslogtreecommitdiff
path: root/src/FundLab.Api/akshare_collector.py
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 03:25:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 03:25:00 +0800
commit7dd0e3f42105569b6e6ec02aeeec9b5a8bbd9dd3 (patch)
treed7374c1b4bb5308374c3386ac3768ac7209a6440 /src/FundLab.Api/akshare_collector.py
parent8c01adf05f1428bf2ada49574b25529c064da884 (diff)
downloadfund-lab-7dd0e3f42105569b6e6ec02aeeec9b5a8bbd9dd3.tar.gz
Add real AKShare stock daily history probe slice (3d-19)
Diffstat (limited to 'src/FundLab.Api/akshare_collector.py')
-rw-r--r--src/FundLab.Api/akshare_collector.py88
1 files changed, 86 insertions, 2 deletions
diff --git a/src/FundLab.Api/akshare_collector.py b/src/FundLab.Api/akshare_collector.py
index 62632bf..f9a0f06 100644
--- a/src/FundLab.Api/akshare_collector.py
+++ b/src/FundLab.Api/akshare_collector.py
@@ -269,6 +269,87 @@ def stock_from_snapshot(code):
return None
+def stock_daily_observations(frame, date_key, close_key, volume_key, amount_key):
+ rows = []
+
+ for _, row in frame.iterrows():
+ row_date = date_text(row.get(date_key))
+ close = decimal_text(row.get(close_key))
+ if row_date is None or close is None or Decimal(close) <= 0:
+ continue
+
+ rows.append(
+ {
+ "date": row_date,
+ "close": close,
+ "volume": decimal_text(row.get(volume_key)),
+ "amount": decimal_text(row.get(amount_key)),
+ }
+ )
+
+ rows.sort(key=lambda item: item["date"])
+ return rows
+
+
+def stock_market_symbol(code):
+ if code[0] in ("6", "9"):
+ return f"sh{code}"
+ if code[0] in ("0", "3", "2"):
+ return f"sz{code}"
+ if code[0] in ("4", "8"):
+ return f"bj{code}"
+ return f"sh{code}"
+
+
+def stock_daily(code, days_text):
+ code = text(code)
+ if code is None or not re.fullmatch(r"\d{6}", code):
+ raise ValueError("stock code must contain exactly six digits")
+
+ if days_text is None:
+ days = 5
+ else:
+ try:
+ days = int(days_text)
+ except (TypeError, ValueError):
+ raise ValueError("days must be an integer between 1 and 30")
+
+ if days < 1 or days > 30:
+ raise ValueError("days must be an integer between 1 and 30")
+
+ symbol = stock_market_symbol(code)
+ providers = [
+ ((ak.stock_zh_a_hist, {"symbol": code, "period": "daily", "adjust": ""}), "日期", "收盘", "成交量", "成交额"),
+ ((ak.stock_zh_a_daily, {"symbol": symbol, "adjust": ""}), "date", "close", "volume", "amount"),
+ ((ak.stock_zh_a_hist_tx, {"symbol": symbol}), "date", "close", "volume", "amount"),
+ ]
+
+ for (fetch, kwargs), date_key, close_key, volume_key, amount_key in providers:
+ try:
+ frame = fetch(**kwargs)
+ except Exception:
+ continue
+
+ if frame is None or frame.empty:
+ continue
+
+ observations = stock_daily_observations(frame, date_key, close_key, volume_key, amount_key)
+ if not observations:
+ continue
+
+ return {
+ "schema_version": SCHEMA_VERSION,
+ "operation": "stock-daily",
+ "source": "akshare",
+ "source_revision": source_revision(),
+ "collected_at": collected_at(),
+ "instrument": {"code": code},
+ "observations": observations[-days:],
+ }
+
+ raise ValueError(f"no valid daily history for {code}")
+
+
def stock_quote(code):
code = text(code)
if code is None or not re.fullmatch(r"\d{6}", code):
@@ -305,9 +386,10 @@ def stock_quote(code):
def main():
parser = argparse.ArgumentParser()
- parser.add_argument("--operation", choices=("search", "nav", "bond-quote", "stock-quote"), required=True)
+ parser.add_argument("--operation", choices=("search", "nav", "bond-quote", "stock-quote", "stock-daily"), required=True)
parser.add_argument("--query")
parser.add_argument("--code")
+ parser.add_argument("--days")
args = parser.parse_args()
try:
@@ -317,8 +399,10 @@ def main():
payload = nav(args.code)
elif args.operation == "bond-quote":
payload = bond_quote(args.code)
- else:
+ elif args.operation == "stock-quote":
payload = stock_quote(args.code)
+ else:
+ payload = stock_daily(args.code, args.days)
json.dump(payload, sys.stdout, ensure_ascii=False, separators=(",", ":"))
sys.stdout.write("\n")