summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/FundLab.Api/akshare_collector.py43
-rw-r--r--tests/FundLab.Api.Tests/BondQuoteProbeTests.fs27
2 files changed, 61 insertions, 9 deletions
diff --git a/src/FundLab.Api/akshare_collector.py b/src/FundLab.Api/akshare_collector.py
index 6543923..652f32d 100644
--- a/src/FundLab.Api/akshare_collector.py
+++ b/src/FundLab.Api/akshare_collector.py
@@ -150,22 +150,47 @@ def nav(code):
}
+def bond_maturity_date(code):
+ # Maturity is a best-effort enrichment: the quote endpoint has no expiry
+ # date, so a failure here only means maturity_date stays null.
+ try:
+ frame = ak.bond_zh_cov_info(symbol=code, indicator="基本信息")
+ if frame is None or frame.empty:
+ return None
+ return date_text(frame.iloc[0].get("EXPIRE_DATE"))
+ except Exception:
+ 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")
- frame = ak.bond_zh_cov_info(symbol=code, indicator="基本信息")
+ frame = ak.bond_zh_hs_cov_spot()
if frame is None or frame.empty:
- raise ValueError(f"AKShare returned no bond info for {code}")
+ raise ValueError(f"no valid quote for {code}")
+
+ prefix_rank = {"sh": 0, "sz": 1, "bj": 2}
+ candidates = []
+
+ for _, row in frame.iterrows():
+ symbol = text(row.get("symbol")) or ""
+ row_code = text(row.get("code")) or ""
+ if row_code != code and not symbol.endswith(code):
+ continue
+
+ price = decimal_text(row.get("trade"))
+ if price is None or Decimal(price) <= 0:
+ continue
+
+ prefix = symbol[:2].lower()
+ candidates.append((prefix_rank.get(prefix, 3), symbol, text(row.get("name")), price))
- row = frame.iloc[0]
- name = text(row.get("SECURITY_NAME_ABBR"))
- price = decimal_text(row.get("CURRENT_BOND_PRICE"))
- maturity_date = date_text(row.get("EXPIRE_DATE"))
+ if not candidates:
+ raise ValueError(f"no valid quote for {code}")
- if name is None and price is None and maturity_date is None:
- raise ValueError(f"AKShare returned no usable bond quote fields for {code}")
+ _, _, name, price = sorted(candidates, key=lambda item: (item[0], item[1]))[0]
return {
"schema_version": SCHEMA_VERSION,
@@ -177,7 +202,7 @@ def bond_quote(code):
"quote": {
"name": name,
"price": price,
- "maturity_date": maturity_date,
+ "maturity_date": bond_maturity_date(code),
},
}
diff --git a/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs b/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs
index a94bf58..442ed7c 100644
--- a/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs
+++ b/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs
@@ -70,6 +70,33 @@ type BondQuoteProbeTests(fixture: PostgresFixture) =
| Error failure -> failwithf "expected a bond quote, got %A" failure
[<Fact>]
+ member _.``probe parses a spot-shaped payload where maturity is unknown``() =
+ let json = BondQuoteProbeSupport.payload "110075" (Some "南航转债") (Some "106.368") None
+
+ let probe =
+ AkshareBondQuoteProbe(BondQuoteProbeSupport.collectorReturning (fun () -> Ok json)) :> IBondQuoteProbe
+
+ match probe.GetQuote("110075", CancellationToken.None) with
+ | Ok quote ->
+ Assert.Equal("110075", quote.Code)
+ Assert.Equal<string option>(Some "南航转债", quote.Name)
+ Assert.Equal<decimal option>(Some 106.368m, quote.Price)
+ Assert.Equal<DateOnly option>(None, quote.MaturityDate)
+ | Error failure -> failwithf "expected a bond quote, got %A" failure
+
+ [<Fact>]
+ member _.``probe surfaces a no valid quote collector failure``() =
+ let probe =
+ AkshareBondQuoteProbe(
+ BondQuoteProbeSupport.collectorReturning (fun () -> Error "AKShare collector failed: no valid quote for 999999")
+ )
+ :> IBondQuoteProbe
+
+ match probe.GetQuote("999999", CancellationToken.None) with
+ | Error(MarketDataCollectorUnavailable message) -> Assert.Contains("no valid quote for 999999", message)
+ | other -> failwithf "expected MarketDataCollectorUnavailable, got %A" other
+
+ [<Fact>]
member _.``probe rejects a malformed bond code without touching the collector``() =
let probe =
AkshareBondQuoteProbe(BondQuoteProbeSupport.collectorReturning (fun () -> failwith "collector should not be called"))