diff options
Diffstat (limited to 'src/FundLab.Web/App.fs')
| -rw-r--r-- | src/FundLab.Web/App.fs | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs index 8b8f3b9..62bb20e 100644 --- a/src/FundLab.Web/App.fs +++ b/src/FundLab.Web/App.fs @@ -481,6 +481,30 @@ type RawBondPositions = positions: RawBondPosition array } +type RawValuationPosition = + { + instrumentCode: string + name: obj + assetClass: string + quantity: string + price: obj + priceSource: obj + marketValue: obj + status: string + } + +type RawFundValuation = + { + fundId: string + currency: string + cash: string + positionsMarketValue: string + portfolioValue: string + pricedPositions: int + unavailablePositions: int + positions: RawValuationPosition array + } + type CreateAttempt = { idempotencyKey: string @@ -869,6 +893,29 @@ type BondPositionView = lastTradedAt: string } +type ValuationPositionView = + { + instrumentCode: string + name: string option + assetClass: string + quantity: string + price: string option + priceSource: string option + marketValue: string option + status: string + } + +type FundValuationView = + { + currency: string + cash: string + positionsMarketValue: string + portfolioValue: string + pricedPositions: int + unavailablePositions: int + positions: ValuationPositionView list + } + type BondTradeAttempt = { idempotencyKey: string @@ -946,6 +993,9 @@ module Api = [<Import("getBondPositions", "./src/api.js")>] let getBondPositions (token: string) (fundId: string) : JS.Promise<RawBondPositions> = jsNative + [<Import("getFundValuation", "./src/api.js")>] + let getFundValuation (token: string) (fundId: string) : JS.Promise<RawFundValuation> = jsNative + [<Import("createCapitalDeposit", "./src/api.js")>] let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative @@ -1285,6 +1335,29 @@ module Api = lastTradedAt = raw.lastTradedAt } + let decodeValuationPosition (raw: RawValuationPosition) : ValuationPositionView = + { + instrumentCode = raw.instrumentCode + name = decodeOptionalText raw.name + assetClass = raw.assetClass + quantity = raw.quantity + price = decodeOptionalText raw.price + priceSource = decodeOptionalText raw.priceSource + marketValue = decodeOptionalText raw.marketValue + status = raw.status + } + + let decodeFundValuation (raw: RawFundValuation) : FundValuationView = + { + currency = raw.currency + cash = raw.cash + positionsMarketValue = raw.positionsMarketValue + portfolioValue = raw.portfolioValue + pricedPositions = raw.pricedPositions + unavailablePositions = raw.unavailablePositions + positions = raw.positions |> Array.toList |> List.map decodeValuationPosition + } + type Model = { token: string @@ -1402,6 +1475,10 @@ type Model = bondPositionsInFlight: bool bondPositions: BondPositionView list bondPositionsMessage: string option + valuationReadSeq: int + valuationInFlight: bool + valuation: FundValuationView option + valuationMessage: string option returnsSelectedDate: string option planReadSeq: int planInFlight: bool @@ -1527,6 +1604,9 @@ type Msg = | BondPositionsReadRequested | BondPositionsReadCompleted of requestId: int * fundId: string * positions: RawBondPositions | BondPositionsReadFailed of requestId: int * fundId: string * message: string + | ValuationReadRequested + | ValuationReadCompleted of requestId: int * fundId: string * valuation: RawFundValuation + | ValuationReadFailed of requestId: int * fundId: string * message: string | ReturnsDateChanged of string | InvestmentPlansReadRequested | InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array @@ -1689,6 +1769,10 @@ let init () = bondPositionsInFlight = false bondPositions = [] bondPositionsMessage = None + valuationReadSeq = 0 + valuationInFlight = false + valuation = None + valuationMessage = None returnsSelectedDate = None planReadSeq = 0 planInFlight = false @@ -1922,6 +2006,13 @@ let private readBondPositionsCommand token fundId requestId = (fun positions -> BondPositionsReadCompleted(requestId, fundId, positions)) (fun error -> BondPositionsReadFailed(requestId, fundId, errorText error)) +let private readValuationCommand token fundId requestId = + Cmd.OfPromise.either + (fun () -> Api.getFundValuation token fundId) + () + (fun valuation -> ValuationReadCompleted(requestId, fundId, valuation)) + (fun error -> ValuationReadFailed(requestId, fundId, errorText error)) + let private readInvestmentPlansCommand token fundId requestId = Cmd.OfPromise.either (fun () -> Api.getInvestmentPlans token fundId) @@ -3476,6 +3567,43 @@ let update message model = Cmd.none else model, Cmd.none + | ValuationReadRequested -> + match model.createdFund with + | Some fund when not (String.IsNullOrWhiteSpace model.token) -> + let requestId = model.valuationReadSeq + 1 + + { + model with + valuationReadSeq = requestId + valuationInFlight = true + valuationMessage = None + }, + readValuationCommand model.token fund.id requestId + | _ -> model, Cmd.none + | ValuationReadCompleted (requestId, fundId, valuation) -> + if requestId = model.valuationReadSeq + && (match model.createdFund with Some fund -> fund.id = fundId | None -> false) then + { + model with + valuation = Some(Api.decodeFundValuation valuation) + valuationInFlight = false + valuationMessage = None + }, + Cmd.none + else + model, Cmd.none + | ValuationReadFailed (requestId, fundId, message) -> + if requestId = model.valuationReadSeq + && (match model.createdFund with Some fund -> fund.id = fundId | None -> false) then + { + model with + valuation = None + valuationInFlight = false + valuationMessage = Some(sprintf "组合估值不可用 — %s" message) + }, + Cmd.none + else + model, Cmd.none | ReturnsDateChanged value -> { model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none | InvestmentPlansReadRequested -> @@ -5462,6 +5590,65 @@ let private stocksPanel model dispatch = prop.hidden (model.stockPositionsMessage.IsNone) prop.text (model.stockPositionsMessage |> Option.defaultValue "") ] + Html.div [ + prop.className "fund-valuation" + prop.children [ + Html.p [ prop.className "returns-subheading"; prop.text "组合估值" ] + Html.div [ + prop.className "search-row" + prop.children [ + Html.button [ + prop.className "secondary-action valuation-refresh-action" + prop.disabled model.valuationInFlight + prop.onClick (fun _ -> dispatch ValuationReadRequested) + prop.text ((if model.valuationInFlight then "估值中..." else "刷新估值"): string) + ] + ] + ] + match model.valuation with + | Some valuation -> + Html.div [ + prop.className "valuation-content" + prop.children ( + [ Html.div [ + prop.className "metric-strip valuation-metrics" + prop.children [ + bondQuoteMetric "现金" (Some valuation.cash) + bondQuoteMetric "持仓市值" (Some valuation.positionsMarketValue) + bondQuoteMetric "组合合计" (Some valuation.portfolioValue) + bondQuoteMetric "已定价/缺失" (Some(sprintf "%d / %d" valuation.pricedPositions valuation.unavailablePositions)) + ] + ] ] + @ (valuation.positions + |> List.map (fun position -> + Html.div [ + prop.className ( + if position.status = "unavailable" then + "valuation-row valuation-position-row valuation-unavailable" + else + "valuation-row valuation-position-row" + ) + prop.children [ + Html.span [ prop.className "fund-detail-label"; prop.text position.instrumentCode ] + Html.span [ prop.className "fund-detail-value"; prop.text (position.name |> Option.defaultValue "—") ] + Html.span [ prop.className "fund-detail-value"; prop.text position.quantity ] + Html.span [ + prop.className "fund-detail-value" + prop.text (position.marketValue |> Option.defaultValue "缺失") + ] + Html.span [ prop.className "fund-detail-value"; prop.text (position.priceSource |> Option.defaultValue "—") ] + ] + ])) + ) + ] + | None -> Html.p [ prop.className "hint"; prop.text "刷新后显示现金/持仓市值/组合合计;缺失行情标的不补零,标记为缺失。" ] + Html.p [ + prop.className "valuation-unavailable" + prop.hidden (model.valuationMessage.IsNone) + prop.text (model.valuationMessage |> Option.defaultValue "") + ] + ] + ] ] ] |
