summaryrefslogtreecommitdiff
path: root/src/FundLab.Web/App.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/FundLab.Web/App.fs')
-rw-r--r--src/FundLab.Web/App.fs143
1 files changed, 138 insertions, 5 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs
index 8eb598e..44130de 100644
--- a/src/FundLab.Web/App.fs
+++ b/src/FundLab.Web/App.fs
@@ -373,6 +373,12 @@ type RawReturns =
points: RawReturnsPoint array
}
+type RawMarketNavDates =
+ {
+ code: string
+ dates: string array
+ }
+
type CreateAttempt =
{
idempotencyKey: string
@@ -709,6 +715,9 @@ module Api =
[<Import("getReturns", "./src/api.js")>]
let getReturns (token: string) (fundId: string) : JS.Promise<RawReturns> = jsNative
+ [<Import("getMarketNavDates", "./src/api.js")>]
+ let getMarketNavDates (token: string) (code: string) : JS.Promise<RawMarketNavDates> = jsNative
+
[<Import("createCapitalDeposit", "./src/api.js")>]
let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative
@@ -1049,6 +1058,11 @@ type Model =
returnsReadSeq: int
returnsInFlight: bool
returns: FundReturns option
+ navDatesReadSeq: int
+ navDatesInFlight: bool
+ navDates: string list
+ navDatesAvailable: bool
+ returnsSelectedDate: string option
planReadSeq: int
planInFlight: bool
planRunSeq: int
@@ -1142,6 +1156,10 @@ type Msg =
| ReturnsReadRequested
| ReturnsReadCompleted of requestId: int * returns: RawReturns
| ReturnsReadFailed of requestId: int * message: string
+ | NavDatesReadRequested
+ | NavDatesReadCompleted of requestId: int * dates: RawMarketNavDates
+ | NavDatesReadFailed of requestId: int * message: string
+ | ReturnsDateChanged of string
| InvestmentPlansReadRequested
| InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array
| InvestmentPlansReadFailed of requestId: int * message: string
@@ -1252,6 +1270,11 @@ let init () =
returnsReadSeq = 0
returnsInFlight = false
returns = None
+ navDatesReadSeq = 0
+ navDatesInFlight = false
+ navDates = []
+ navDatesAvailable = false
+ returnsSelectedDate = None
planReadSeq = 0
planInFlight = false
planRunSeq = 0
@@ -1421,6 +1444,13 @@ let private readReturnsCommand token fundId requestId =
(fun returns -> ReturnsReadCompleted(requestId, returns))
(fun error -> ReturnsReadFailed(requestId, errorText error))
+let private readNavDatesCommand token code requestId =
+ Cmd.OfPromise.either
+ (fun () -> Api.getMarketNavDates token code)
+ ()
+ (fun dates -> NavDatesReadCompleted(requestId, dates))
+ (fun error -> NavDatesReadFailed(requestId, errorText error))
+
let private readInvestmentPlansCommand token fundId requestId =
Cmd.OfPromise.either
(fun () -> Api.getInvestmentPlans token fundId)
@@ -1512,6 +1542,11 @@ let update message model =
returnsReadSeq = model.returnsReadSeq + 1
returnsInFlight = false
returns = None
+ navDatesReadSeq = model.navDatesReadSeq + 1
+ navDatesInFlight = false
+ navDates = []
+ navDatesAvailable = false
+ returnsSelectedDate = None
planReadSeq = model.planReadSeq + 1
planInFlight = false
planRunSeq = model.planRunSeq + 1
@@ -1721,6 +1756,11 @@ let update message model =
returnsReadSeq = model.returnsReadSeq + 1
returnsInFlight = false
returns = None
+ navDatesReadSeq = model.navDatesReadSeq + 1
+ navDatesInFlight = false
+ navDates = []
+ navDatesAvailable = false
+ returnsSelectedDate = None
planReadSeq = model.planReadSeq + 1
planInFlight = false
planRunSeq = model.planRunSeq + 1
@@ -2499,7 +2539,7 @@ let update message model =
returnsInFlight = true
error = None
},
- readReturnsCommand model.token fund.id requestId
+ Cmd.batch [ readReturnsCommand model.token fund.id requestId; Cmd.ofMsg NavDatesReadRequested ]
| Some _ -> { model with error = Some "请输入 API token" }, Cmd.none
| None -> model, Cmd.none
| ReturnsReadCompleted (requestId, returns) ->
@@ -2518,6 +2558,61 @@ let update message model =
{ model with returnsInFlight = false; error = Some message }, Cmd.none
else
model, Cmd.none
+ | NavDatesReadRequested ->
+ match model.createdFund with
+ | Some _ when String.IsNullOrWhiteSpace model.token ->
+ { model with error = Some "请输入 API token" }, Cmd.none
+ | Some _ ->
+ let heldCode =
+ model.positions
+ |> Option.bind (fun positions -> positions.positions |> List.tryHead)
+ |> Option.map (fun position -> position.instrumentCode)
+
+ match heldCode with
+ | Some code ->
+ let requestId = model.navDatesReadSeq + 1
+
+ {
+ model with
+ navDatesReadSeq = requestId
+ navDatesInFlight = true
+ },
+ readNavDatesCommand model.token code requestId
+ | None ->
+ {
+ model with
+ navDates = []
+ navDatesAvailable = false
+ navDatesInFlight = false
+ },
+ Cmd.none
+ | None -> model, Cmd.none
+ | NavDatesReadCompleted (requestId, dates) ->
+ if requestId = model.navDatesReadSeq then
+ let resolved = dates.dates |> Array.toList
+
+ {
+ model with
+ navDates = resolved
+ navDatesAvailable = not (List.isEmpty resolved)
+ navDatesInFlight = false
+ },
+ Cmd.none
+ else
+ model, Cmd.none
+ | NavDatesReadFailed (requestId, _) ->
+ if requestId = model.navDatesReadSeq then
+ {
+ model with
+ navDates = []
+ navDatesAvailable = false
+ navDatesInFlight = false
+ },
+ Cmd.none
+ else
+ model, Cmd.none
+ | ReturnsDateChanged value ->
+ { model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none
| InvestmentPlansReadRequested ->
match model.createdFund with
| Some fund when not (String.IsNullOrWhiteSpace model.token) ->
@@ -3889,9 +3984,15 @@ let private returnsChartView (points: ReturnsPoint list) =
]
let private returnsPanel model dispatch =
- let latest (selector: ReturnsPoint -> string option) =
- model.returns
- |> Option.bind (fun returns -> returns.points |> List.rev |> List.tryPick selector)
+ let visiblePoints =
+ match model.returns with
+ | None -> []
+ | Some returns ->
+ match model.returnsSelectedDate with
+ | Some selected -> returns.points |> List.filter (fun point -> point.date <= selected)
+ | None -> returns.points
+
+ let latest (selector: ReturnsPoint -> string option) = visiblePoints |> List.rev |> List.tryPick selector
let totalAssets = latest (fun point -> point.totalAssets)
let cumulativeReturn = latest (fun point -> point.cumulativeReturn)
@@ -3910,6 +4011,38 @@ let private returnsPanel model dispatch =
Html.span [ prop.className "section-note"; prop.text "Returns - cash-flow adjusted" ]
]
]
+ Html.div [
+ prop.className "returns-date-picker"
+ prop.custom (
+ "data-nav-dates-state",
+ (if model.navDatesAvailable then "available" else "empty")
+ )
+ prop.children [
+ Html.label [
+ prop.className "field-label"
+ prop.children [
+ Html.span "真实净值日期"
+ Html.select [
+ prop.className "text-input returns-date-input"
+ prop.disabled (not model.navDatesAvailable)
+ prop.value (defaultArg model.returnsSelectedDate "")
+ prop.onChange (fun value -> dispatch (ReturnsDateChanged value))
+ prop.children [
+ Html.option [ prop.value ""; prop.text "最新" ]
+ yield!
+ (model.navDates
+ |> List.map (fun date -> Html.option [ prop.value date; prop.text date ]))
+ ]
+ ]
+ ]
+ ]
+ Html.p [
+ prop.className "returns-date-empty"
+ prop.hidden model.navDatesAvailable
+ prop.text "暂无真实净值数据"
+ ]
+ ]
+ ]
match model.returns with
| None ->
Html.p [ prop.className "hint"; prop.text "创建基金后可读取收益曲线。" ]
@@ -3945,7 +4078,7 @@ let private returnsPanel model dispatch =
]
]
]
- returnsChartView returns.points
+ returnsChartView visiblePoints
Html.p [
prop.className "returns-note"
prop.text "蓝线 总资产 / 橙线 单位净值(各自归一化);缺少净值的数据点不连线,也不补零。"