diff options
Diffstat (limited to 'src/FundLab.Api/akshare_collector.py')
| -rw-r--r-- | src/FundLab.Api/akshare_collector.py | 88 |
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") |
