diff options
Diffstat (limited to 'src/FundLab.Api/akshare_collector.py')
| -rw-r--r-- | src/FundLab.Api/akshare_collector.py | 102 |
1 files changed, 100 insertions, 2 deletions
diff --git a/src/FundLab.Api/akshare_collector.py b/src/FundLab.Api/akshare_collector.py index 652f32d..62632bf 100644 --- a/src/FundLab.Api/akshare_collector.py +++ b/src/FundLab.Api/akshare_collector.py @@ -207,9 +207,105 @@ def bond_quote(code): } +def first_positive_decimal(*values): + for value in values: + price = decimal_text(value) + if price is not None and Decimal(price) > 0: + return price + return None + + +def stock_price_from_bid_ask(code): + # Single-symbol realtime snapshot (eastmoney). Returns None when the venue + # rejects the symbol or carries no valid last price. + frame = ak.stock_bid_ask_em(symbol=code) + if frame is None or frame.empty: + return None + + for _, row in frame.iterrows(): + item = text(row.get("item")) + if item in ("最新", "最新价"): + return first_positive_decimal(row.get("value")) + + return None + + +def stock_from_snapshot(code): + # Whole-market snapshots differ per provider; each entry is + # (fetch, code column, name column, price column, exchange prefixes). + providers = [ + (ak.stock_zh_a_spot_tx, "code", "name", "zxj", {"sh": 0, "sz": 1, "bj": 2}), + (ak.stock_zh_a_spot_em, "代码", "名称", "最新价", None), + ] + + for fetch, code_key, name_key, price_key, prefixes in providers: + try: + frame = fetch() + except Exception: + continue + + if frame is None or frame.empty: + continue + + candidates = [] + + for _, row in frame.iterrows(): + row_code = text(row.get(code_key)) or "" + if row_code != code and not row_code.lower().endswith(code): + continue + + price = first_positive_decimal(row.get(price_key)) + if price is None: + continue + + prefix = row_code[:2].lower() + rank = prefixes.get(prefix, 3) if prefixes is not None else 0 + candidates.append((rank, row_code, text(row.get(name_key)), price)) + + if candidates: + _, _, name, price = sorted(candidates, key=lambda item: (item[0], item[1]))[0] + return name, price + + return None + + +def stock_quote(code): + code = text(code) + if code is None or not re.fullmatch(r"\d{6}", code): + raise ValueError("stock code must contain exactly six digits") + + name = None + price = None + + try: + price = stock_price_from_bid_ask(code) + except Exception: + price = None + + if price is None: + found = stock_from_snapshot(code) + if found is None: + raise ValueError(f"no valid quote for {code}") + name, price = found + + return { + "schema_version": SCHEMA_VERSION, + "operation": "stock-quote", + "source": "akshare", + "source_revision": source_revision(), + "collected_at": collected_at(), + "instrument": {"code": code}, + "quote": { + "name": name, + "price": price, + "currency": "CNY", + }, + } + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("--operation", choices=("search", "nav", "bond-quote"), required=True) + parser.add_argument("--operation", choices=("search", "nav", "bond-quote", "stock-quote"), required=True) parser.add_argument("--query") parser.add_argument("--code") args = parser.parse_args() @@ -219,8 +315,10 @@ def main(): payload = search(args.query) elif args.operation == "nav": payload = nav(args.code) - else: + elif args.operation == "bond-quote": payload = bond_quote(args.code) + else: + payload = stock_quote(args.code) json.dump(payload, sys.stdout, ensure_ascii=False, separators=(",", ":")) sys.stdout.write("\n") |
