FSharpChart wraps .NET 4.0 charting control. Here is Don’s entry about it to get started and download the assembly with examples.
However, FSharpChart is still a control that needs WinForms or WPF to work. What about some Matlab-esque type of functionality, where a simple function call would do the job without having to worry about much wiring?
An example (full sources available here) is a simple risk minimization model, where given a portfolio of assets and their expected performance, we must choose the weights for each asset so that standard deviation (risk) of the portfolio is minimal. The model and its solution are described, for instance, here. It is easy enough to implement it using Math.Net package.
module RiskMinimizationFormulation = open System open MathNet.Numerics.FSharp open MathNet.Numerics.LinearAlgebra.Double open modelling.shared type RiskMinimization(expected : Vector, correlations : Matrix, stdDeviations : Vector) = do if correlations.RowCount <> correlations.ColumnCount then invalidArg "variances" "expected square matrix" if expected.Count <> correlations.RowCount then invalidArg "expected" "expectations vector must have the same length as variance matrix dimensions" let variances = correlations |> Matrix.mapi (fun i j value -> let mutable v = value if correlations.[i,j] = 0.0 then v <- correlations.[j, i] stdDeviations.[i] * stdDeviations.[j] * v) let n = expected.Count let i = vector (List.init n (fun i -> 1.)) let variancesInv = variances.Inverse() let a = i * variancesInv * i let b = i * variancesInv * expected let c = expected * variancesInv * expected let denom = 1. / (a * c - b*b) let g = denom * (variancesInv * (c * i - b * expected)) let h = denom * (variancesInv * (a * expected - b * i)) member rm.ComputeOptimal (expectation : float) = g + h * expectation
We compute all the auxiliary values at instantiation and the function of interest is ComputeOptimal, that given an expectation produces a vector of weights.
Suppose we were to chart weights of our portfolio for a range of expectations. Let’s say expectations are values 0.05..0.12..0.05 ([5%, 12%], with a step of 0.5%). Our charting function could look something like this:
member rm.ChartOptimalWeights (expectations : float list) (names : string seq) = let data = matrix(expectations |> List.map (fun v -> (rm.ComputeOptimal v |> Vector.toList))) let plotData = seq {for i in 0 .. data.ColumnCount - 1 -> data.Column(i).ToArray() |> Array.toList} Charting.Plot( "Line", plotData, plotX = (expectations |> List.map(fun v -> v * 100.)), xTitle = "Expected Return", yTitle = "Asset Weight", xLimits = (5., 12.), yLimits = (-2.0, 3.0), seriesNames = names, title = "Risk Minimization Model")
We call ComputeOptimal for each of the values in range and get a vector in return. We then construct a matrix out of these vectors, making each vector a row of the new matrix. Then our series are in the columns of the resulting matrix. For instance, for a test case with the range of expectations described above and a portfolio of four assets, we get a 4 x 24 matrix, where each row represents a portfolio. We want to chart the dynamic of each asset weight as function of expectation, so our series actually are in the columns of this matrix.
The function of interest is Charting.Plot. This function wraps the necessary functionality of FSharpChart in a general way to produce a chart of given type:
type Charting () = static member private createChartOfType ((chartType : string), name, (y : seq<#IConvertible * #IConvertible>)) = let innerTps = FSharpType.GetTupleElements(y.GetType().GetGenericArguments().[0]) findAndCreateChart chartType innerTps name y static member private createChartOfType ((chartType : string), name, (y : seq<#IConvertible>)) = let innerTp = y.GetType().GetGenericArguments().[0] findAndCreateChart chartType [|innerTp|] name y static member Plot ( chartType : string, plotY : #seq<#IConvertible> seq, ? plotX : #seq<#IConvertible>, ? seriesNames : string seq, ? title : string, ? xTitle : string, ? yTitle : string, ? xLimits : float * float, ? yLimits : float * float, ? margin : float32 * float32 * float32 * float32) = let marg = defaultArg margin (4.0f, 12.0f, 4.0f, 4.0f) let chartTitle = defaultArg title "Chart" let xTitle = defaultArg xTitle String.Empty let yTitle = defaultArg yTitle String.Empty let chartNames = defaultArg seriesNames (plotY |> Seq.mapi(fun i v -> "Series " + i.ToString())) if (chartNames |> Seq.length) <> (plotY |> Seq.length) then invalidArg "names" "not of the right length" // zip up the relevant information together // x-values go with every y-series values in a tuple // series names gets zipped with every sequence of (x, y) tuples: (name, seq(x, seq(y))) let mutable chart = match plotX with |Some plotX -> let plot = plotY |> Seq.map(fun s -> List.zip plotX s) |> Seq.zip chartNames FSharpChart.Combine ([for p in plot -> Charting.createChartOfType (chartType, (fst p), (snd p))]) | None -> let plot = plotY |> Seq.zip chartNames FSharpChart.Combine ([for p in plot -> Charting.createChartOfType (chartType, (fst p), (snd p))]) //add x and y limits chart <- match xLimits with | Some (xMin, xMax) -> FSharpChart.WithArea.AxisX(Minimum = xMin, Maximum= xMax, MajorGrid = Grid(LineColor = Color.LightGray)) chart | None -> FSharpChart.WithArea.AxisX(MajorGrid = Grid(LineColor = Color.LightGray)) chart chart <- match yLimits with | Some (yMin, yMax) -> FSharpChart.WithArea.AxisY(Minimum = yMin, Maximum= yMax, MajorGrid = Grid(LineColor = Color.LightGray)) chart | None -> FSharpChart.WithArea.AxisY(MajorGrid = Grid(LineColor = Color.LightGray)) chart //... and margin |> FSharpChart.WithMargin marg //set the titles chart.Area.AxisX.Title <- xTitle chart.Area.AxisY.Title <- yTitle //add legend chart <- FSharpChart.WithLegend(InsideArea = false, Alignment = StringAlignment.Center, Docking = Docking.Top) chart //add title chart.Title <- StyleHelper.Title(chartTitle, FontSize = 10.0f, FontStyle = FontStyle.Bold) //create the form createForm chart
There is not much going on – just setting some chart parameters. There are only two things of interest:
FSharpChart.Combine ([for p in plot -> Charting.createChartOfType (chartType, (fst p), (snd p))])
This call will create a combined chart (if necessary – the combination of only one series) of any type, by calling one of two createChartType overrides. One override looks for chart with X and Y series, the other – for just Y series. For instance:
static member private createChartOfType ((chartType : string), name, (y : seq<#IConvertible * #IConvertible>)) = let innerTps = FSharpType.GetTupleElements(y.GetType().GetGenericArguments().[0]) findAndCreateChart chartType innerTps name y
This version of the function will look for something like
ChartTypes.type name<TX, TY>(IEnumerable<TX> xvalues, IEnumerable<TY> yvalues) where TX: IConvertible where TY: IConvertible
in FSharpType and then attempt to create it:
let (|FindChartOfType|_|) numGenericArgs chartType = let mi = (typeof<FSharpChart>.GetMethods() |> Array.filter( fun v -> v.Name = chartType && v.GetParameters().Length = 1 && v.GetGenericArguments().Length = numGenericArgs)).[0] match mi with | x when x = Unchecked.defaultof<MethodInfo> -> None |_ -> Some mi let findAndCreateChart chartType (genericArgs : Type []) name y = let mi = match chartType with | FindChartOfType genericArgs.Length mi -> mi | _ -> invalidArg "chartType " ("Chart of type " + chartType + " not found") let chart = mi.GetGenericMethodDefinition().MakeGenericMethod(genericArgs).Invoke(null, [|y|]) :?> ChartTypes.GenericChart chart.Name <- name chart
Another function of interest is the one that puts it all together and displays the chart:
let createForm (chart : ChartTypes.CombinedChart) = let chartForm = new ChartForm<ChartData.DataSourceCombined>(chart) chartForm.Text <- "Chart" chartForm.ClientSize <- new Size(600, 600) Application.EnableVisualStyles() Application.Run(chartForm :> Form)
MSDN.FSharp.Charting namespace ChartExtensions module provides the ChartForm class that is used to host the chart control and display the chart.
My test data represents a hypothetical portfolio, taken from a Frank Fabozzi’s book:
[TestMethod] public void ChartWeights() { Vector expected = new DenseVector (new double [] { 0.079, 0.079, 0.09, 0.071 }); Matrix correlations = new DenseMatrix(new double[,] { { 1.0, 0F, 0F, 0F }, { 0.24, 1.0, 0F, 0F }, { 0.25, 0.47, 1.0, 0F }, { 0.22, 0.14, 0.25, 1.0 } }); Vector stdDeviations = new DenseVector(new double[] { 0.195, 0.182, 0.183, 0.165 }); RiskMinimizationFormulation.RiskMinimization model = new RiskMinimizationFormulation.RiskMinimization(expected, correlations, stdDeviations); var range = ListModule.OfSeq(Enumerable.Range(50, 120).Where(e => e % 5 == 0).Select(e => (double)e / 1000D)); model.ChartOptimalWeights(range, new string [] {"Australia", "Austria", "Belgium", "Canada"}); }
If it is possible, could you update this sample to use the new Fsharp.Charting (the next evolution of FSharpChart)? http://fsharp.github.io/FSharp.Charting/
Thanks
Don and Tomas