Building a Trading App (F# & Xamarin.Forms)

Intro

In the previous post, I discussed my continued journey of learning by building a Xamarin.Forms app using F#.

In that post, I discussed the overall architecture:

SolutionExplorer.png

The architecture above is my attempt at practicing Robert C. Martin’s Clean Architecture. Specifically I make the features of the app the focus of the architecture and I treat everything else as details that are abstracted away via folders.

I treat the integration points of an application as details:

Integration.png

The Integration folder above harbors the modules that integrate with my domain. Such modules would be the application, services, and runtime dependencies.

Types

I began code construction of the application by writing functions to support trade transactions.

The following functions were implemented:

module ManageTradeImpl

open Services
open Core.Entities
open TestAPI

let buyShares (service:IBroker) context balance =
    (context , balance) ||> service.TryPurchase

let sellShares (service:IBroker) context =
    context |> service.TrySell

Amongst, the various types that were flushed out as I wrote functions, I wrote a Broker interface that will eventually rely on IO.

The broker interface was added to my Services library:

services

The Broker Interface is the following:

namespace Services

open Core.Entities

type IBroker =

    abstract member GetInfo       : string -> StockInfo option
    abstract member TryPurchase   : Shares -> decimal -> PurchaseResult
    abstract member TrySell       : Shares -> SellResult
    abstract member InvestmentsOf : string -> SharesWithPrice seq

Here’s how the buyShares function can get executed:

(*Test*)
let context = { AccountId= "Bizmonger"
                Symbol=    "ROK"
                Qty=        100 }

let result = buyShares (MockBroker()) context 5000m

Here’s the result:

val result : PurchaseResult =
  InsufficientFunds {PurchaseAttempt = {AccountId = "Bizmonger";
                                        Symbol = "ROK";
                                        Quantity = 100;};
                     Balance = 5000M;
                     StockPrice = 300M;}

Here’s another example for the buyShares function:

(*Test*)
let context = { AccountId= "Bizmonger"
                Symbol=    "ROK"
                Qty=        5 }

let result = buyShares (MockBroker()) context 5000m

Here’s the result:

val result : PurchaseResult = PurchaseRequested {AccountId = "Bizmonger";
                                                 Symbol = "ROK";
                                                 Quantity = 5;}

The following types have been defined so far for making trades:

module Core.Entities

type StockInfo = {
    Symbol  : string
    Price   : decimal
    DayLow  : decimal
    DayHigh : decimal
}

type Shares = {
    AccountId : string
    Symbol    : string
    Qty       : int
}

type SharesWithPrice ={
    Shares        : Shares
    PricePerShare : decimal
    Total         : decimal
}

type PurchaseInfo = {
    AccountId : string
    Symbol    : string
    Quantity  : int
}

type Owner = { AccountId:string ; Symbol:string }

type SellInfo = {
    AccountId : string
    Symbol    : string
    Quantity  : int
}

type InsufficientFunds = {
    PurchaseAttempt : PurchaseInfo
    Balance         : decimal
    StockPrice      : decimal
}    

type PurchaseResult =
    | PurchaseRequested of PurchaseInfo
    | UnknownSymbol     of PurchaseInfo
    | InvalidQuantity   of PurchaseInfo
    | InsufficientFunds of InsufficientFunds

type SellResult =
    | SellRequested        of SellInfo
    | InsufficientQuantity of SellInfo

User Interface

A domain is useless if there’s no interaction with it.

Here’s a sample UI that was built in XAML:

Emuator.png

Here’s the XAML that resulted in the above UI:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"              xmlns:converters="clr-namespace:FnTrade.Converters;assembly=FnTrade"              xmlns:home="clr-namespace:Home.ViewModels;assembly=Home"              x:Class="FnTrade.HomePage">

    <ContentPage.BindingContext>
        <home:HomeViewModel />
    </ContentPage.BindingContext>

    <ContentPage.Resources>
        <ResourceDictionary>
            <converters:InstanceToBoolConverter x:Key="InstanceToBoolConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <ListView x:Name="SharesListView" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"                    ItemsSource="{Binding Investments}"                   HorizontalOptions="Center" VerticalOptions="Center">
            <ListView.Header>
                <Grid >
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>

                    <Label Grid.Row="0" Grid.Column="0" Text="Symbol" FontAttributes="Bold" HorizontalTextAlignment="Center" />
                    <Label Grid.Row="0" Grid.Column="1" Text="Qty"    FontAttributes="Bold" HorizontalTextAlignment="Center" />
                    <Label Grid.Row="0" Grid.Column="2" Text="Price"  FontAttributes="Bold" HorizontalTextAlignment="Center" />
                    <Label Grid.Row="0" Grid.Column="3" Text="Total"  FontAttributes="Bold" HorizontalTextAlignment="Center" />

                    <BoxView Grid.Row="1" Grid.ColumnSpan="4" Color="Gray" Margin="5,0,5,5"                              HorizontalOptions="FillAndExpand" HeightRequest="1" />
                </Grid>
            </ListView.Header>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>

                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition />
                                <ColumnDefinition />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>

                            <Label Grid.Column="0" Text="{Binding Shares.Symbol}" HorizontalTextAlignment="Center" />
                            <Label Grid.Column="1" Text="{Binding Shares.Qty}"    HorizontalTextAlignment="Center" />
                            <Label Grid.Column="2" Text="{Binding PricePerShare,  StringFormat='{}{0:c}'}" HorizontalTextAlignment="Center" />
                            <Label Grid.Column="3" Text="{Binding Total,          StringFormat='{}{0:c}'}" HorizontalTextAlignment="Center" />
                        </Grid>

                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Button Grid.Row="1" Grid.Column="0" Text="Buy More"                   Command="{Binding Buy}"                  CommandParameter="{Binding Source={x:Reference SharesListView}, Path=SelectedItem.Shares.Symbol}"                 IsEnabled="{Binding Source={x:Reference SharesListView},                                     Path=SelectedItem, Converter={StaticResource InstanceToBoolConverter}}" />

        <Button Grid.Row="1" Grid.Column="1" Text="Sell"                  Command="{Binding Sell}"                 CommandParameter="{Binding Source={x:Reference SharesListView}, Path=SelectedItem.Shares.Symbol}"                 IsEnabled="{Binding Source={x:Reference SharesListView},                                     Path=SelectedItem, Converter={StaticResource InstanceToBoolConverter}}" />
    </Grid>

</ContentPage>

ViewModel

In order for the UI to present dynamic content, I had to implement a viewmodel.
The following viewmodel was implemented:

namespace Home.ViewModels

open System.Windows.Input
open Core.IntegrationLogic
open Core.Entities
open Integration.Factories
open Services
open Search
open System.Collections.ObjectModel

type HomeViewModel() as this =

    inherit ViewModelBase()

    let dispatcher = getDispatcher()
    let accountId =  getAccountId()
    let broker =     getBroker() :> IBroker

    let searchCommand =
        DelegateCommand( (fun _ -> this.StockInfo <- getQuote broker this.Symbol) ,                           fun _ -> true) :> ICommand
    let sellCommand =
        DelegateCommand( (fun o -> dispatcher.Sell { Owner.AccountId=accountId ; Symbol=o :?> string} ) ,
                          fun _ -> true) :> ICommand
    let buyCommand =
        DelegateCommand( (fun o -> dispatcher.Buy  { Owner.AccountId=accountId ; Symbol=o :?> string} ) ,
                          fun _ -> true) :> ICommand

    let mutable symbol = ""
    let mutable stockInfo = None
    let mutable investments = ObservableCollection<SharesWithPrice>()

    member this.Load() =
        let result = broker.InvestmentsOf accountId
        this.Investments <- ObservableCollection<SharesWithPrice>(result)

    member this.Symbol
        with get() =      symbol
        and  set(value) = symbol <- value
                          base.NotifyPropertyChanged(<@ this.Symbol @>)
    member this.StockInfo
        with get() =      stockInfo
        and  set(value) = stockInfo <- value
                          base.NotifyPropertyChanged(<@ this.StockInfo @>)
    member this.Investments
        with get() =      investments
        and  set(value) = investments <- value
                          base.NotifyPropertyChanged(<@ this.Investments @>)

    member this.Search = searchCommand
    member this.Sell =   sellCommand
    member this.Buy =    buyCommand

Conclusion

This solution, which is a Work In Process, can be found here on GitHub.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: