blob: 311dec5da203a5b0ca2cb948e1fe438f690d2619 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
namespace FundLab.Api
open System
open System.Collections.Concurrent
open System.Threading
/// A single bond's current quote. Every descriptive field is optional because
/// the upstream AKShare endpoint may not carry it; missing stays missing rather
/// than being filled with a fabricated value.
type BondQuote =
{
Code: string
SourceRevision: string
Name: string option
Price: decimal option
CleanPrice: decimal option
AccruedInterest: decimal option
Date: DateOnly option
MaturityDate: DateOnly option
}
/// Read-only probe that returns the current quote for a bond code.
///
/// Like INavDateProbe there is no synthetic fallback: a failed probe surfaces as
/// a MarketDataFailure so the API answers 503 with the underlying reason.
type IBondQuoteProbe =
abstract GetQuote: code: string * CancellationToken -> Result<BondQuote, MarketDataFailure>
/// AKShare-backed bond-quote probe. Successful lookups are memoised per bond
/// code (the F# equivalent of functools.lru_cache); failures are never cached.
type AkshareBondQuoteProbe(collector: IMarketDataCollector) =
let cache = ConcurrentDictionary<string, BondQuote>(StringComparer.Ordinal)
let isBondCode (value: string) =
not (String.IsNullOrWhiteSpace value)
&& value.Length = 6
&& value |> Seq.forall Char.IsDigit
member _.Clear() = cache.Clear()
interface IBondQuoteProbe with
member _.GetQuote(code, token) =
let normalized = if isNull code then "" else code.Trim()
if not (isBondCode normalized) then
Error(InvalidMarketDataRequest "bond code must contain exactly six digits")
else
match cache.TryGetValue normalized with
| true, cached -> Ok cached
| _ ->
match collector.FetchBondQuote(normalized, token) with
| Error message -> Error(MarketDataCollectorUnavailable message)
| Ok json ->
match MarketData.parseBondQuotePayload json with
| Error message -> Error(InvalidMarketDataPayload message)
| Ok payload when not (String.Equals(payload.Code, normalized, StringComparison.Ordinal)) ->
Error(
InvalidMarketDataPayload(
sprintf "payload code '%s' does not match requested code '%s'" payload.Code normalized
)
)
| Ok payload ->
let quote =
{
Code = payload.Code
SourceRevision = payload.SourceRevision
Name = payload.Name
Price = payload.Price
CleanPrice = payload.CleanPrice
AccruedInterest = payload.AccruedInterest
Date = payload.Date
MaturityDate = payload.MaturityDate
}
cache.[normalized] <- quote
Ok quote
|