diff options
Diffstat (limited to 'src/FundLab.Api/App.fs')
| -rw-r--r-- | src/FundLab.Api/App.fs | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs index b559ac2..34b4f2d 100644 --- a/src/FundLab.Api/App.fs +++ b/src/FundLab.Api/App.fs @@ -224,6 +224,39 @@ type RebalanceExecutionResponse = outcomes: {| instrumentCode: string; action: string; amount: string; status: string; orderId: string option; pendingReason: string option |} list } +type RebalanceExecutionRecordResponse = + { + planId: Guid + runDate: string + instrumentCode: string + action: string + amount: string + status: string + orderId: string option + pendingReason: string option + executedAt: string + } + +type RebalanceWeightRowResponse = + { + instrumentCode: string + targetPercent: string + currentValue: string + currentPercent: string + action: string + deltaAmount: string + deltaUnits: string option + } + +type RebalancePreviewResponse = + { + planId: Guid + runDate: string + availableCash: string + equity: string + rows: RebalanceWeightRowResponse list + } + type CapitalDepositResponse = { id: Guid @@ -484,6 +517,45 @@ module App = |}) } + let private rebalanceExecutionRecordResponse (record: RebalanceExecutionRecord) : RebalanceExecutionRecordResponse = + { + planId = record.PlanId + runDate = dateText record.RunDate + instrumentCode = record.InstrumentCode + action = record.Action + amount = cashText record.Amount + status = record.Status + orderId = record.OrderId |> Option.map (fun id -> id.ToString("D")) + pendingReason = record.PendingReason + executedAt = timestampText record.ExecutedAt + } + + let private rebalanceActionText (action: RebalancePolicy.RebalanceAction) = + match action with + | RebalancePolicy.Buy -> "buy" + | RebalancePolicy.Sell -> "sell" + | RebalancePolicy.Hold -> "hold" + + let private rebalanceWeightRowResponse (row: RebalancePolicy.RebalanceWeightRow) : RebalanceWeightRowResponse = + { + instrumentCode = row.InstrumentCode + targetPercent = cashText row.TargetPercent + currentValue = cashText row.CurrentValue + currentPercent = cashText row.CurrentPercent + action = rebalanceActionText row.Action + deltaAmount = cashText row.DeltaAmount + deltaUnits = row.DeltaUnits |> Option.map decimalText + } + + let private rebalancePreviewResponse (preview: RebalancePreview) : RebalancePreviewResponse = + { + planId = preview.PlanId + runDate = dateText preview.RunDate + availableCash = cashText preview.AvailableCash + equity = cashText preview.Equity + rows = preview.Rows |> List.map rebalanceWeightRowResponse + } + let private errorResponse status error message : HttpHandler = setStatusCode status >=> json ({ @@ -1437,6 +1509,41 @@ module App = json (rebalanceExecutionResponse result) next ctx with _ -> errorResponse 500 "PERSISTENCE_ERROR" "rebalance execution failed" next ctx + + let private getRebalanceExecutions (repository: FundRepository) (fundIdText: string) : HttpHandler = + fun next ctx -> + match Guid.TryParse fundIdText with + | false, _ -> errorResponse 400 "INVALID_FUND_ID" "fund id must be a UUID" next ctx + | true, fundId -> + try + match repository.GetFund fundId with + | None -> errorResponse 404 "FUND_NOT_FOUND" "fund was not found" next ctx + | Some _ -> + let records = repository.GetRebalanceExecutions fundId + json (records |> List.map rebalanceExecutionRecordResponse) next ctx + with _ -> + errorResponse 500 "PERSISTENCE_ERROR" "rebalance execution persistence failed" next ctx + + let private previewRebalancePlan (repository: FundRepository) (fundIdText: string) (planIdText: string) : HttpHandler = + fun next ctx -> + match Guid.TryParse fundIdText, Guid.TryParse planIdText with + | (false, _), _ + | _, (false, _) -> + errorResponse 400 "INVALID_REBALANCE_REQUEST" "fund id and plan id must be UUIDs" next ctx + | (true, fundId), (true, planId) -> + try + match repository.GetFund fundId with + | None -> errorResponse 404 "FUND_NOT_FOUND" "fund was not found" next ctx + | Some _ -> + if repository.GetRebalancePlans fundId |> List.exists (fun plan -> plan.Id = planId) |> not then + errorResponse 404 "PLAN_NOT_FOUND" "rebalance plan does not belong to this fund" next ctx + else + match repository.PreviewRebalancePlan planId with + | Error message -> errorResponse 400 "INVALID_REBALANCE_REQUEST" message next ctx + | Ok preview -> json (rebalancePreviewResponse preview) next ctx + with _ -> + errorResponse 500 "PERSISTENCE_ERROR" "rebalance preview failed" next ctx + let private marketDataError (failure: MarketDataFailure) : HttpHandler = let status, error, message = match failure with @@ -1547,6 +1654,8 @@ module App = POST >=> routef "/funds/%s/rebalance/plans" (createRebalancePlan repository) GET >=> routef "/funds/%s/rebalance/plans" (getRebalancePlans repository) POST >=> routef "/funds/%s/rebalance/plans/%s/execute" (fun (fundId, planId) -> executeRebalancePlan repository fundId planId) + GET >=> routef "/funds/%s/rebalance/plans/%s/preview" (fun (fundId, planId) -> previewRebalancePlan repository fundId planId) + GET >=> routef "/funds/%s/rebalance/executions" (getRebalanceExecutions repository) POST >=> routef "/funds/%s/dividends" (createDividend repository) GET >=> routef "/funds/%s/dividends" (getDividends repository) GET >=> routef "/funds/%s/returns" (getFundReturns repository) |
