summaryrefslogtreecommitdiff
path: root/src/FundLab.Web/App.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 03:25:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 03:25:00 +0800
commit7dd0e3f42105569b6e6ec02aeeec9b5a8bbd9dd3 (patch)
treed7374c1b4bb5308374c3386ac3768ac7209a6440 /src/FundLab.Web/App.fs
parent8c01adf05f1428bf2ada49574b25529c064da884 (diff)
downloadfund-lab-7dd0e3f42105569b6e6ec02aeeec9b5a8bbd9dd3.tar.gz
Add real AKShare stock daily history probe slice (3d-19)
Diffstat (limited to 'src/FundLab.Web/App.fs')
-rw-r--r--src/FundLab.Web/App.fs120
1 files changed, 120 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs
index 9b4bed9..7a0e56c 100644
--- a/src/FundLab.Web/App.fs
+++ b/src/FundLab.Web/App.fs
@@ -408,6 +408,20 @@ type RawStockQuote =
currency: string
}
+type RawStockDailyObservation =
+ {
+ date: string
+ close: string
+ volume: obj
+ amount: obj
+ }
+
+type RawStockDaily =
+ {
+ code: string
+ observations: RawStockDailyObservation array
+ }
+
type CreateAttempt =
{
idempotencyKey: string
@@ -724,6 +738,14 @@ type StockQuoteView =
currency: string
}
+type StockDailyBar =
+ {
+ date: string
+ close: string
+ volume: string option
+ amount: string option
+ }
+
module Api =
[<Import("searchInstruments", "./src/api.js")>]
let searchInstruments (token: string) (query: string) : JS.Promise<RawSearchResponse> = jsNative
@@ -779,6 +801,9 @@ module Api =
[<Import("getStockQuote", "./src/api.js")>]
let getStockQuote (token: string) (code: string) : JS.Promise<RawStockQuote> = jsNative
+ [<Import("getStockDaily", "./src/api.js")>]
+ let getStockDaily (token: string) (code: string) (days: int) : JS.Promise<RawStockDaily> = jsNative
+
[<Import("createCapitalDeposit", "./src/api.js")>]
let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative
@@ -1067,6 +1092,14 @@ module Api =
currency = raw.currency
}
+ let decodeStockDailyBar (raw: RawStockDailyObservation) : StockDailyBar =
+ {
+ date = raw.date
+ close = raw.close
+ volume = decodeOptionalText raw.volume
+ amount = decodeOptionalText raw.amount
+ }
+
type Model =
{
token: string
@@ -1160,6 +1193,10 @@ type Model =
stockQuoteInFlight: bool
stockQuote: StockQuoteView option
stockQuoteMessage: string option
+ stockDailySeq: int
+ stockDailyInFlight: bool
+ stockDaily: StockDailyBar list
+ stockDailyMessage: string option
returnsSelectedDate: string option
planReadSeq: int
planInFlight: bool
@@ -1268,6 +1305,9 @@ type Msg =
| StockQuoteRequested
| StockQuoteCompleted of requestId: int * quote: RawStockQuote
| StockQuoteFailed of requestId: int * message: string
+ | StockDailyRequested
+ | StockDailyCompleted of requestId: int * daily: RawStockDaily
+ | StockDailyFailed of requestId: int * message: string
| ReturnsDateChanged of string
| InvestmentPlansReadRequested
| InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array
@@ -1397,6 +1437,10 @@ let init () =
stockQuoteInFlight = false
stockQuote = None
stockQuoteMessage = None
+ stockDailySeq = 0
+ stockDailyInFlight = false
+ stockDaily = []
+ stockDailyMessage = None
returnsSelectedDate = None
planReadSeq = 0
planInFlight = false
@@ -1595,6 +1639,13 @@ let private readStockQuoteCommand token code requestId =
(fun quote -> StockQuoteCompleted(requestId, quote))
(fun error -> StockQuoteFailed(requestId, errorText error))
+let private readStockDailyCommand token code days requestId =
+ Cmd.OfPromise.either
+ (fun () -> Api.getStockDaily token code days)
+ ()
+ (fun daily -> StockDailyCompleted(requestId, daily))
+ (fun error -> StockDailyFailed(requestId, errorText error))
+
let private readInvestmentPlansCommand token fundId requestId =
Cmd.OfPromise.either
(fun () -> Api.getInvestmentPlans token fundId)
@@ -2909,6 +2960,48 @@ let update message model =
Cmd.none
else
model, Cmd.none
+ | StockDailyRequested ->
+ let code = model.stockCode.Trim()
+
+ if String.IsNullOrWhiteSpace model.token then
+ { model with stockDailyMessage = Some "请输入 API token"; stockDaily = [] }, Cmd.none
+ elif String.IsNullOrWhiteSpace code then
+ { model with stockDailyMessage = Some "请输入六位股票代码"; stockDaily = [] }, Cmd.none
+ else
+ let requestId = model.stockDailySeq + 1
+
+ {
+ model with
+ stockDailySeq = requestId
+ stockDailyInFlight = true
+ stockDailyMessage = None
+ stockDaily = []
+ },
+ readStockDailyCommand model.token code 5 requestId
+ | StockDailyCompleted (requestId, daily) ->
+ if requestId = model.stockDailySeq then
+ let rows = daily.observations |> Array.toList |> List.map Api.decodeStockDailyBar
+
+ {
+ model with
+ stockDaily = rows
+ stockDailyMessage = (if List.isEmpty rows then Some "股票日线不可用 — 暂无数据" else None)
+ stockDailyInFlight = false
+ },
+ Cmd.none
+ else
+ model, Cmd.none
+ | StockDailyFailed (requestId, message) ->
+ if requestId = model.stockDailySeq then
+ {
+ model with
+ stockDaily = []
+ stockDailyMessage = Some(sprintf "股票日线不可用 — %s" message)
+ stockDailyInFlight = false
+ },
+ Cmd.none
+ else
+ model, Cmd.none
| ReturnsDateChanged value ->
{ model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none
| InvestmentPlansReadRequested ->
@@ -4709,6 +4802,12 @@ let private stocksPanel model dispatch =
prop.onClick (fun _ -> dispatch StockQuoteRequested)
prop.text ((if model.stockQuoteInFlight then "查询中..." else "查询行情"): string)
]
+ Html.button [
+ prop.className "secondary-action stock-daily-action"
+ prop.disabled model.stockDailyInFlight
+ prop.onClick (fun _ -> dispatch StockDailyRequested)
+ prop.text ((if model.stockDailyInFlight then "读取中..." else "查询近5日日线"): string)
+ ]
]
]
Html.p [
@@ -4733,6 +4832,27 @@ let private stocksPanel model dispatch =
]
| None ->
Html.p [ prop.className "hint"; prop.text "查询后显示名称/价格/币种;缺失字段以「—」表示。" ]
+ Html.div [
+ prop.className "stock-daily"
+ prop.hidden (List.isEmpty model.stockDaily)
+ prop.children (
+ Html.p [ prop.className "returns-subheading"; prop.text "近 5 个交易日收盘" ]
+ :: (model.stockDaily
+ |> List.map (fun bar ->
+ Html.div [
+ prop.className "order-row stock-daily-row"
+ prop.children [
+ Html.span [ prop.className "fund-detail-label"; prop.text bar.date ]
+ Html.span [ prop.className "fund-detail-value"; prop.text bar.close ]
+ ]
+ ]))
+ )
+ ]
+ Html.p [
+ prop.className "stock-daily-unavailable"
+ prop.hidden (model.stockDailyMessage.IsNone)
+ prop.text (model.stockDailyMessage |> Option.defaultValue "")
+ ]
]
]