diff options
Diffstat (limited to 'src/FundLab.Api/StockQuoteProbe.fs')
| -rw-r--r-- | src/FundLab.Api/StockQuoteProbe.fs | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/src/FundLab.Api/StockQuoteProbe.fs b/src/FundLab.Api/StockQuoteProbe.fs new file mode 100644 index 0000000..a41070a --- /dev/null +++ b/src/FundLab.Api/StockQuoteProbe.fs @@ -0,0 +1,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 |
