diff options
Diffstat (limited to 'src/FundLab.Api/akshare_collector.py')
| -rw-r--r-- | src/FundLab.Api/akshare_collector.py | 112 |
1 files changed, 94 insertions, 18 deletions
diff --git a/src/FundLab.Api/akshare_collector.py b/src/FundLab.Api/akshare_collector.py index f9a0f06..2fb837e 100644 --- a/src/FundLab.Api/akshare_collector.py +++ b/src/FundLab.Api/akshare_collector.py @@ -162,14 +162,12 @@ def bond_maturity_date(code): return None -def bond_quote(code): - code = text(code) - if code is None or not re.fullmatch(r"\d{6}", code): - raise ValueError("bond code must contain exactly six digits") - +def bond_quote_from_spot(code): + # Realtime convertible-bond snapshot (sina). Gives name/trade but no date or + # accrued-interest fields; returns None when the code carries no valid price. frame = ak.bond_zh_hs_cov_spot() if frame is None or frame.empty: - raise ValueError(f"no valid quote for {code}") + return None prefix_rank = {"sh": 0, "sz": 1, "bj": 2} candidates = [] @@ -188,25 +186,103 @@ def bond_quote(code): candidates.append((prefix_rank.get(prefix, 3), symbol, text(row.get("name")), price)) if not candidates: - raise ValueError(f"no valid quote for {code}") + return None - _, _, name, price = sorted(candidates, key=lambda item: (item[0], item[1]))[0] + _, symbol, name, price = sorted(candidates, key=lambda item: (item[0], item[1]))[0] + revision = source_revision() return { - "schema_version": SCHEMA_VERSION, - "operation": "bond-quote", "source": "akshare", - "source_revision": source_revision(), - "collected_at": collected_at(), - "instrument": {"code": code}, - "quote": { - "name": name, - "price": price, - "maturity_date": bond_maturity_date(code), - }, + "source_revision": f"{revision}/hs-cov-spot", + "name": name, + "price": price, + "clean_price": price, + "accrued_interest": None, + "date": None, + "maturity_date": bond_maturity_date(code), + "symbol": symbol, } +def bond_quote_from_daily(code): + # Exchange bond daily history (sina). The venue is not derivable from the + # six-digit code alone, so try both prefixes; the first that returns rows + # wins. Gives a settled close and the trade date but no accrued-interest + # breakdown. + for prefix in ("sh", "sz"): + symbol = f"{prefix}{code}" + + try: + frame = ak.bond_zh_hs_daily(symbol=symbol) + except Exception: + continue + + if frame is None or frame.empty or "date" not in frame.columns or "close" not in frame.columns: + continue + + rows = [] + + for _, row in frame.iterrows(): + row_date = date_text(row.get("date")) + close = decimal_text(row.get("close")) + if row_date is None or close is None or Decimal(close) <= 0: + continue + rows.append((row_date, close)) + + if not rows: + continue + + rows.sort(key=lambda item: item[0]) + row_date, close = rows[-1] + revision = source_revision() + + return { + "source": "akshare", + "source_revision": f"{revision}/hs-bond-daily", + "name": None, + "price": close, + "clean_price": close, + "accrued_interest": None, + "date": row_date, + "maturity_date": bond_maturity_date(code), + "symbol": symbol, + } + + return None + + +def bond_quote(code): + code = text(code) + if code is None or not re.fullmatch(r"\d{6}", code): + raise ValueError("bond code must contain exactly six digits") + + for attempt in (bond_quote_from_spot, bond_quote_from_daily): + try: + quote = attempt(code) + except Exception: + quote = None + + if quote is not None: + return { + "schema_version": SCHEMA_VERSION, + "operation": "bond-quote", + "source": quote["source"], + "source_revision": quote["source_revision"], + "collected_at": collected_at(), + "instrument": {"code": code}, + "quote": { + "name": quote["name"], + "price": quote["price"], + "clean_price": quote["clean_price"], + "accrued_interest": quote["accrued_interest"], + "date": quote["date"], + "maturity_date": quote["maturity_date"], + }, + } + + raise ValueError(f"no valid bond quote for {code}") + + def first_positive_decimal(*values): for value in values: price = decimal_text(value) |
