blob: a41070af204c8f6e390a42a551b87f50e8bed462 (
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
|
namespace FundLab.Api
open System
open System.Collections.Concurrent
open System.Threading
/// A single stock's current quote. Name is optional because a quote venue may
/// not carry it; missing stays missing rather than being filled in.
type StockQuote =
{
Code: string
Name: string option
Price: decimal option
Currency: string
}
/// Read-only probe that returns the current quote for a stock code.
///
/// Like the other market probes there is no synthetic fallback: a failure is
/// surfaced as a MarketDataFailure so the API answers 503 with the reason.
type IStockQuoteProbe =
abstract GetQuote: code: string * CancellationToken -> Result<StockQuote, MarketDataFailure>
/// AKShare-backed stock-quote probe. Successful lookups are memoised per code
///; failures are never cached.
type AkshareStockQuoteProbe(collector: IMarketDataCollector) =
let cache = ConcurrentDictionary<string, StockQuote>(StringComparer.Ordinal)
let isStockCode (value: string) =
not (String.IsNullOrWhiteSpace value)
&& value.Length = 6
&& value |> Seq.forall Char.IsDigit
member _.Clear() = cache.Clear()
interface IStockQuoteProbe with
member _.GetQuote(code, token) =
let normalized = if isNull code then "" else code.Trim()
if not (isStockCode normalized) then
Error(InvalidMarketDataRequest "stock code must contain exactly six digits")
else
match cache.TryGetValue normalized with
| true, cached -> Ok cached
| _ ->
match collector.FetchStockQuote(normalized, token) with
| Error message -> Error(MarketDataCollectorUnavailable message)
| Ok json ->
match MarketData.parseStockQuotePayload json with
| Error message -> Error(InvalidMarketDataPayload message)
| Ok payload ->
let quote =
{
Code = payload.Code
Name = payload.Name
Price = payload.Price
Currency = payload.Currency
}
cache.[normalized] <- quote
Ok quote
|