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 /// Exchange prefix: "sh", "sz" or "bj". Market: string option /// Price adjustment flag: "none", "qfq" or "hfq". Adjust: string option /// True when the venue reports no trades for the session. Suspended: bool option } /// 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 /// AKShare-backed stock-quote probe. Successful lookups are memoised per code ///; failures are never cached. type AkshareStockQuoteProbe(collector: IMarketDataCollector) = let cache = ConcurrentDictionary(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 Market = payload.Market Adjust = payload.Adjust Suspended = payload.Suspended } cache.[normalized] <- quote Ok quote