summaryrefslogtreecommitdiff
path: root/src/FundLab.Api/StockQuoteProbe.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/FundLab.Api/StockQuoteProbe.fs')
-rw-r--r--src/FundLab.Api/StockQuoteProbe.fs61
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