Let's create a stock tracking app in .NET MAUI

Categories: .NET C#, .NET .NET MAUI
I have a keen interest in personal finance and investing. An excel file or third party apps don't really provide the tools that I'm looking for to track my investments. So as a developer, what better way to explore a new technology (.NET MAUI) than to create such a app yourself? This article will provide a step-by-step guide to do this. I will assume that the reader understands the fundamentals of C#, XAML and MVVM.

Creating the solution

Let's go over the structure of the solution quickly to ensure you understand the namespaces in the code snippets that will follow. I have decided to name the project IFi (I Finance). 

XAML was the recommended approach to define the user interface in desktop applications for most Microsoft technologies (WPF, UWP etc.), and this is still the case with MAUI.NET. Data binding and the MVVM (Model-View-ViewModel) pattern come naturally with XAML, so immediately we already know a few of the project types that we will need. There are variations to this structure of course, depending on your preference.

1. IFi.Presentation.Maui: A MAUI.NET app project that will contain all the pages

2. IFi.Presentation.VM.Maui: A MAUI.NET class library that will contain all the view models. You can choose to include the view models in the presentation layer.

3. IFi.Domain: A class library that will contain all the models and DTO's

4. IFi.Utilities: A class library that will contain extension methods and classes that are not part of the domain

5. IFi.Utilities.Maui: A MAUI.NET class library that will contain extension methods and classes that are not part of the domain, and use the MAUI.NET framework.

 

Setting up the main page

When the MAUI app project has been created we are presented with a main page with some XAML code that serves as a demo. Let's get rid of all the additional code within <ContentPage>. The code-behind also needs to be removed.

XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="IFiDemo.Presentation.Maui.MainPage">

</ContentPage>
C#
namespace IFiDemo.Presentation.Maui
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

We can also remove the title from the ShellContent so that it doesn't take up space in the window.

XAML
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="IFiDemo.Presentation.Maui.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:IFiDemo.Presentation.Maui"
    Shell.FlyoutBehavior="Disabled">

    <ShellContent
        ContentTemplate="{DataTemplate local:MainPage}"
        Route="MainPage" />
</Shell>

Creating the base layout

Let's create the basic structure of the main page. It should contain the most important elements that we plan to implement later on. In the code snippets I have already included some data bindings. Those will be implemented later on.

The list of stocks that we want to track can be presented in a CollectionView. Initially I thought to use a DataGrid for this. However, officially there is no support for this and when you really think about it, it may not be the most-user friendly solution. Especially not for mobile apps.

The information that I would like to include for each stock ticker is:

XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="IFiDemo.Presentation.Maui.MainPage">
    <Grid RowDefinitions="auto, auto, *">
        <Grid Grid.Row="1" RowDefinitions="auto" ColumnDefinitions="2*,*,*,*,*,*,*,*,*,*">
            <Grid.Resources>
                <Style TargetType="Button">
                    <Setter Property="Padding" Value="5"></Setter>
                    <Setter Property="CornerRadius" Value="0"></Setter>
                </Style>
            </Grid.Resources>
            <Button Grid.Column="0" Text="Symbol" />
            <Button Grid.Column="1" Text="Position" />
            <Button Grid.Column="2" Text="Close" />
            <Button Grid.Column="3" Text="Value" />
            <Button Grid.Column="4" Text="Target value" />
            <Button Grid.Column="5" Text="1D change" />
            <Button Grid.Column="6" Text="7D change" />
            <Button Grid.Column="7" Text="1M change" />
            <Button Grid.Column="8" Text="3M change" />
            <Button Grid.Column="9" Text="1Y change" />
        </Grid>
        <CollectionView Grid.Row="2" ItemsSource="{Binding StockPositions}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Border Stroke="Gray" StrokeThickness="2" HeightRequest="80">
                        <Grid RowDefinitions="auto" ColumnDefinitions="2*,*,*,*,*,*,*,*,*,*" Padding="10">
                            <Grid.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="Stroke" Value="Black" />
                                    <Setter Property="StrokeThickness" Value="1" />
                                    <Setter Property="StrokeDashArray" Value="5,2" />
                                    <Setter Property="StrokeDashOffset" Value="2"/>
                                    <Setter Property="Padding" Value="2"/>
                                    <Setter Property="Background" Value="White" />
                                </Style>
                            </Grid.Resources>
                            <VerticalStackLayout Grid.Column="0">
                                <Label HorizontalTextAlignment="Center" Text="{Binding Stock.Symbol, StringFormat='{0}'}"/>
                                <Label HorizontalTextAlignment="Center" Text="{Binding Ticker.Name}" MaxLines="2"/>
                            </VerticalStackLayout>
                            <Border Grid.Column="1">
                                <Label HorizontalOptions="Center" Text="{Binding Position, StringFormat='{0}'}" />
                            </Border>
                            <Border Grid.Column="2">
                                <Label HorizontalOptions="Center" Text="{Binding Stock.Close, StringFormat='{0:F2}'}"/>
                            </Border>
                            <Border Grid.Column="3">
                                <VerticalStackLayout>
                                    <Label HorizontalTextAlignment="Center" Text="{Binding Value, StringFormat='{0:F2}'}"/>
                                    <Label HorizontalTextAlignment="Center" Text="{Binding CurrentHoldingPct, StringFormat='{0:P2}'}"/>
                                </VerticalStackLayout>
                            </Border>
                            <Border Grid.Column="4">
                                <VerticalStackLayout>
                                    <Label HorizontalTextAlignment="Center" Text="{Binding TargetValue, StringFormat='{0:F2}'}"/>
                                    <Label HorizontalTextAlignment="Center" Text="{Binding TargetHoldingPct, StringFormat='{0:P2}'}"/>
                                </VerticalStackLayout>
                            </Border>
                            <Border Grid.Column="5">
                                <Label  HorizontalOptions="Center" Text="{Binding Change1Day, StringFormat='{0:P2}'}" />
                            </Border>
                            <!--Other periodicities omitted for brevity-->
                        </Grid>
                    </Border>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
</ContentPage>

When we run the app this is what it looks like:

Preparing to read stock data from an API

The application doesn't do much at the moment. We would like to load our tracked stocks from somewhere and show it on screen. There are already a few bindings in the view, but the view model has not been implemented yet. 
However, before we start writing code it would be a good idea to already start thinking about how we will load stock market data from an API. Ideally we should be able to reuse the DTO's that are used for (de)serialization as models in the domain.

I have considered several API's and eventually chose https://marketstack.com/. In terms of price and ease of use I believe it is one of the best for a hobby project. The free plan covers 1000 requests per month. This might be enough for very basic functionalities but once you start loading historical quotes you will have to pay. The basic plan is $ 8.99 (€ 8.23) per month and covers 10 000 requests per month. In my experience this has always been enough. If you implement smart caching mechanisms later on you can reduce the request rate.

It doesn't provide intraday data for exchanges outside the US, but that is not the purpose of this application. It is meant for long term investments. I believe it would become a costly business to implement real-time updates.

The website has clear documentation and all endpoints return one or more JSON objects that have the same properties. Knowing this, we can check the endpoints and create the DTO's. The example above is for Stock.cs, but StockExchange, Ticker and Timezone are also in scope of this project to search for ticker symbols. You can find the full code here.

C#
using System.Text.Json.Serialization;

namespace IFiDemo.Domain.ApiResponse
{
    public class Stock
    {
        [JsonPropertyName("open")]
        public decimal? Open { get; set; }
        [JsonPropertyName("high")]
        public decimal? High { get; set; }
        [JsonPropertyName("low")]
        public decimal? Low { get; set; }
        [JsonPropertyName("close")]
        public decimal? Close { get; set; }
        [JsonPropertyName("volume")]
        public decimal? Volume { get; set; }
        [JsonPropertyName("adj_high")]
        public decimal? AdjHigh { get; set; }
        [JsonPropertyName("adj_low")]
        public decimal? AdjLow { get; set; }
        [JsonPropertyName("adj_close")]
        public decimal? AdjClose { get; set; }
        [JsonPropertyName("adj_open")]
        public decimal? AdjOpen { get; set; }
        [JsonPropertyName("adj_volume")]
        public decimal? AdjVolume { get; set; }
        [JsonPropertyName("split_factor")]
        public decimal? SplitFactor { get; set; }
        [JsonPropertyName("dividend")]
        public decimal? Dividend { get; set; }
        [JsonPropertyName("symbol")]
        public string Symbol { get; set; }
        [JsonPropertyName("exchange")]
        public string Exchange { get; set; }
        [JsonPropertyName("date")]
        public DateTimeOffset Date { get; set; }
    }
}

Implementing the view models

Now it's time to load stock data and show it on screen. The view has already been implemented so we only need to focus on the view models and services.

All view models need to implement INotifyPropertyChanged and all properties need to notify the view(s) about changes. Doing this manually is a lot of work and somewhat ugly. There are packages that do this automatically for you.
I have added the CommunityToolkit.Mvvm package. Now all we have to do is make it a partial class (so that the package can add the auto-generated code), inherit from ObservableObject and apply the [ObservableProperty] attribute to all fields that need so send notifications. The properties are created automatically during development.

C#
namespace IFiDemo.Presentation.VM.Maui.ViewModels
{
    public partial class StockPosition : ObservableObject
    {
        public Stock Stock { get; set; }
        private Stock[] _historicalData;
        [JsonIgnore]
        public Stock[] HistoricalData //order from old to new
        {
            get => _historicalData;
            set => _historicalData = value.OrderBy(x => x.Date).ToArray();
        }
        public decimal Value => (Stock.Close ?? 0) * Position;
        [ObservableProperty]
        private float _currentHoldingPct;
        [ObservableProperty]
        public decimal _targetValue;
        [ObservableProperty]
        private float _targetHoldingPct;
        [ObservableProperty]
        private int _position;

        public Ticker Ticker { get; set; }
        public StockPosition() //for json deserialization
        {
        }
        public StockPosition(Stock stock, Ticker ticker)
        {
            Stock = stock;
            Ticker = ticker;
            Position = 0;
            TargetHoldingPct = 0;
        }

        internal void Refresh(IEnumerable<StockPosition> stockPositions)
        {
            if (stockPositions.Sum(x => x.Value) > 0)
            {
                CurrentHoldingPct = (float)(Value / stockPositions.Sum(x => x.Value));
                TargetValue = (decimal)TargetHoldingPct * stockPositions.Sum(x => x.Value);
            }
        }
    }
}
C#
namespace IFiDemo.Presentation.VM.Maui.ViewModels
{
    public partial class MainVM : ObservableObject
    {
        private readonly StockMarketDataFileService _fileService;
        public ObservableCollection<StockPosition> StockPositions { get; } = new ObservableCollection<StockPosition>();
        public MainVM()
        {
            _fileService = new(StockPositions);
        }

        public async Task InitializeAsync()
        {
            foreach (var stockPosition in (await _fileService.GetStockPositionsAsync()).OrderBy(x => x.Stock.Symbol))
            {
                StockPositions.Add(stockPosition);
            }
        }
    }
}
C#
namespace IFiDemo.Presentation.Maui
{
    public partial class MainPage : ContentPage
    {
        private readonly MainVM _vm;
        public MainPage()
        {
            _vm = new MainVM();
            BindingContext = _vm;
            InitializeComponent();
            Task.Run(_vm.InitializeAsync);
        }
    }
}

Reading stock data

Lastly, we need to read stock data. To optimize the request rate, as much data as possible should be cached on disk.
Reading stock data consists of the following steps:

  1. Read the current stock positions from a file
  2. For all symbols, read all historical data from a file if it's up-to-date, else read it from the API
  3. Save the historical data to a file
  4. If no historical data was found then we can assume that the stock doesn't exist OR it's the first trading days since the IPO (initial public offering) OR Marketstack is not providing the correct data. In either case, we will still try to fetch the stock data with the "eod/latest" endpoint instead of all historical data with the date_from and date_to arguments
  5. Save the stock positions to a file

There is a lot going on here, so let's go over it one step at a time.

Reading stock positions from a file

Reading stock positions should be fairly straightforward. We check if the file exists. If it does then we read the contents and deserialize everything to a list of stock positions. Otherwise we can assume this is the first time that the application is running.
The difficulty here is that Marketstack reads and writes and dates in ISO-8601 format. And even though according to the System.Text.Json documentation the JsonSerializer is fully ISO 8601-1:2019 compliant, in my experience this is not the case. Trying to deserialize dates in format "yyyy-MM-dd'T'HH:mm:sszzz" to a DateTimeOffset fails. The workaround for when you are having issues with (de)serialization is to implement a custom converter.

C#
private async Task<List<StockPosition>> ReadStockPositionsAsync()
{
    FileInfo fi = new(_stockPositionsFileName);
    if (fi.Exists)
    {
        string json = await File.ReadAllTextAsync(_stockPositionsFileName);
        var stockPositions = JsonSerializer.Deserialize<List<StockPosition>>(json, DateTimeOffsetConverter_ISO8601.DefaultJsonSerializerOptions);
        return stockPositions;
    }
    return new List<StockPosition>();
}
C#
namespace IFiDemo.Utilities.JsonConverters
{
    public class DateTimeOffsetConverter_ISO8601 : JsonConverter<DateTimeOffset>
    {
        public static JsonSerializerOptions DefaultJsonSerializerOptions { get; }
        static DateTimeOffsetConverter_ISO8601()
        {
            JsonSerializerOptions options = new JsonSerializerOptions();
            options.Converters.Add(new DateTimeOffsetConverter_ISO8601());
            DefaultJsonSerializerOptions = options;
        }
        public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (!reader.TryGetDateTimeOffset(out DateTimeOffset value))
            {
                value = DateTimeOffset.ParseExact(reader.GetString(), "yyyy-MM-dd'T'HH:mm:sszzz", null);
            }

            return value;
        }
        public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString("yyyy-MM-dd'T'HH:mm:sszzz"));
        }
    }
}

Reading historical data from a file

 This is the most complex part.

First we try to read the historical data from a file. If we have data then we have to determine if the data is up-to-date. In other words, we need to know if it's possible that there was new data after the last time that the file was modified.
How do we know that? Well, so far I have not found a perfect solution. To know this, we need to know the trading days and trading hours of an exchange and Marketstack doesn't provide that information. So the best idea I have is to check if the modification date is the current date, or a Friday if it's weekend. This covers most of the cases and most likely data will be fetched a bit more frequently than needed. The code is a bit verbose, but that's what it comes down to.
Finally, we fetch the historical data for all symbols that need updates. This is fetched all at once (in chunks) from the API to optimize the request rate.

C#
//guaranteed to have all symbols as keys
public async Task<Dictionary<string, Stock[]>> GetHistoricalDataAsync(DateTime from, DateTime to, params string[] symbols)
{
    Dictionary<string, Stock[]> dataBySymbol = new Dictionary<string, Stock[]>();
    foreach (string symbol in symbols)
    {
        Stock[] data = null;
        if (Currency.IsCurrency(symbol))
            data = new Stock[0];
        else
        {
            DateTime lastWriteTime;
            (data, lastWriteTime) = await ReadHistoricalDataAsync(symbol);
            if (data == null || (lastWriteTime.Date != DateTime.Today && !IsHistoricalDataComplete(data, from, to)))
                continue;
        }
        dataBySymbol.Add(symbol, data);
    }
    string[] symbolsToFetch = symbols.Where(x => !dataBySymbol.ContainsKey(x)).ToArray();
    var historicalData = await _repo.GetHistoricalDataAsync(symbolsToFetch, from, to);
    foreach (string symbol in symbolsToFetch)
    {
        if (historicalData.TryGetValue(symbol, out var value))
        {
            dataBySymbol.Add(symbol, value);
            await SaveHistoricalDataAsync(value, symbol);
        }
        else
            dataBySymbol.Add(symbol, new Stock[0]);
    }
    return dataBySymbol;
}
private async Task<(Stock[] stocks, DateTime lastWriteTime)> ReadHistoricalDataAsync(string symbol)
{
    string path = Path.Combine(FileSystem.Current.AppDataDirectory, _historicalDataDirectory, $"{symbol}.txt");
    Stock[] data = null;
    var fi = new FileInfo(path);
    if (fi.Exists)
    {
        string json = await File.ReadAllTextAsync(path);
        data = JsonSerializer.Deserialize<Stock[]>(json, DateTimeOffsetConverter_ISO8601.DefaultJsonSerializerOptions);
    }
    return (data, fi.LastWriteTime);
}
internal static bool IsHistoricalDataComplete(IEnumerable<Stock> historicalData, DateTime from, DateTime to)
{
    from = from.GetClosestWeekDay(false);
    to = to.GetClosestWeekDay(true);
    var ordered = historicalData.OrderBy(x => x.Date);
    return ordered.First().Date.Date <= from && ordered.Last().Date.Date >= to;
    //does not account for gaps -> TODO, but how?
}

Reading stock data from the API

The API calls are luckily very straighforward. All thanks to the great documentation. There are two very important things to mention here.

Firstly, the historical data is fetched in chunks. The API allows us to provide a limit and offset, and the response contains the total amount of results and the amount of returned results. This gives us full control. The maximum allowed limit is 1000, so that is what we will be using.

Secondly, in my repository I am using my own (privately accessible) API that uses Marketstack. The arguments to the API are exactly the same as those of Marketstack, but I don't have to provide an access token as that is being taken care of by the API. The API is used for further optimization, but that is outside the scope of this article. What matters here is that, if you are using Marketstack, you have to provide an access token in the URL.

C#
namespace IFiDemo.Domain
{
    public class StockMarketDataRepository
    {
        private readonly static HttpClient _httpClient = new HttpClient();
        static StockMarketDataRepository()
        {
            _httpClient.BaseAddress = new Uri("<redacted>");
        }
        public Task<IReadOnlyList<Stock>> GetStocksAsync(string[] tickers) => GetStocksAsync(tickers, null);
        public async Task<IReadOnlyList<Stock>> GetStocksAsync(string[] tickers, string exchange)
        {
            if(! tickers.Any()) return new List<Stock>() { };
            try
            {
                string today = DateTime.Today.ToString("yyyy-MM-dd");

                string requestUri = $"eod/latest?symbols={string.Join(',', tickers)}";
                if (!string.IsNullOrWhiteSpace(exchange))
                    requestUri += "&exchange={exchange}";
                var response = await _httpClient.GetAsync(requestUri);
                if (response.IsSuccessStatusCode)
                {
                    string json = await response.Content.ReadAsStringAsync();

                    var eod = JsonSerializer.Deserialize<Eod>(json, DateTimeOffsetConverter_ISO8601.DefaultJsonSerializerOptions);
                    return eod.Data.ToList();
                }
                return new List<Stock>() { };
            }
            catch
            {
                return new List<Stock>() { };
            }
        }
        private const int limit = 1000;
        public async Task<Dictionary<string, Stock[]>> GetHistoricalDataAsync(string[] tickers, DateTime from, DateTime to)
        {
            if (!tickers.Any()) return new Dictionary<string, Stock[]>();
            List<Stock> data = new List<Stock>();
            int offset = 0;
            int total = -1;
            do
            {
                try
                {
                    string requestUri = $"eod?symbols={string.Join(',', tickers)}&date_from={from.ToString("yyyy-MM-dd")}&date_to={to.ToString("yyyy-MM-dd")}&limit={limit}&offset={offset}";
                    var response = await _httpClient.GetAsync(requestUri);
                    if (response.IsSuccessStatusCode)
                    {
                        string json = await response.Content.ReadAsStringAsync();

                        var eod = JsonSerializer.Deserialize<Eod>(json, DateTimeOffsetConverter_ISO8601.DefaultJsonSerializerOptions);

                        data.AddRange(eod.Data);

                        total = eod.Pagination.Total;
                        offset += eod.Pagination.Count;
                    }
                    else break;
                }
                catch
                {
                    break;
                }
            }
            while (offset < total);

            return data.GroupBy(x => x.Symbol).ToDictionary(x => x.Key, x => x.ToArray());
        }
    }
}

Saving historical data and stock positions

C#
private Task SaveHistoricalDataAsync(Stock[] data, string symbol)
{
    if (!Directory.Exists(Path.Combine(FileSystem.Current.AppDataDirectory, _historicalDataDirectory)))
        Directory.CreateDirectory(Path.Combine(FileSystem.Current.AppDataDirectory, _historicalDataDirectory));
    string path = Path.Combine(FileSystem.Current.AppDataDirectory, _historicalDataDirectory, $"{symbol}.txt");
    string json = JsonSerializer.Serialize(data, DateTimeOffsetConverter_ISO8601.DefaultJsonSerializerOptions);
    return File.WriteAllTextAsync(path, json);
}
public Task SaveStockPositionsAsync(IReadOnlyList<StockPosition> stockPositions)
{
    string json = JsonSerializer.Serialize(stockPositions, DateTimeOffsetConverter_ISO8601.DefaultJsonSerializerOptions);
    return File.WriteAllTextAsync(_stockPositionsFileName, json);
}

Bringing it all together

All that remains now is using the methods that we just implemented in the stock data initialization.

C#
namespace IFiDemo.Presentation.VM.Maui
{
    public class StockMarketDataFileService
    {
        private readonly string _historicalDataDirectory = "historicalData";
        private readonly string _stockPositionsFileName = Path.Combine(FileSystem.Current.AppDataDirectory, "stockPositions.txt");

        private readonly StockMarketDataRepository _repo = new();
        private DateTime HistoricalDataFrom => DateTime.Today.AddYears(-1);
        private DateTime HistoricalDataTo => DateTime.Today;

        private readonly ObservableCollection<StockPosition> _stockPositions;

        public StockMarketDataFileService(ObservableCollection<StockPosition> stockPositions)
        {
            _stockPositions = stockPositions;
        }

        public async Task<IReadOnlyList<StockPosition>> GetStockPositionsAsync()
        {
            var stockPositions = await ReadStockPositionsAsync();
            var symbols = stockPositions.Select(x => x.Stock?.Symbol ?? x.Ticker.Symbol);
            var historicalData = await GetHistoricalDataAsync(HistoricalDataFrom, HistoricalDataTo, symbols.ToArray());
            bool needsUpdate = false;
            foreach (var stockPosition in stockPositions)
            {
                Stock stock = null;
                if (historicalData.TryGetValue(stockPosition.Stock?.Symbol ?? stockPosition.Ticker.Symbol, out var data))
                    stock = data.OrderByDescending(x => x.Date).FirstOrDefault();
                stockPosition.HistoricalData = data;

                if (stock == null)
                {
                    if (Currency.IsCurrency(stockPosition.Ticker))
                        stock = Currency.AsStock(stockPosition.Ticker);
                    else
                    {
                        stock = (await _repo.GetStocksAsync(new[] { stockPosition.Stock?.Symbol ?? stockPosition.Ticker.Symbol })).FirstOrDefault();
                    }
                    needsUpdate = true;
                }
                else
                {
                    if (stockPosition.Stock == null || stockPosition.Stock.Date != stock.Date)
                        needsUpdate = true;
                }
                stockPosition.Stock = stock;
            }
            foreach (var stockPosition in stockPositions)
            {
                stockPosition.Refresh(stockPositions);
            }
            if (needsUpdate)
                await SaveStockPositionsAsync(stockPositions);
            return stockPositions;
        }
        //other code omitted
    }
}

Running the app

Now we have a stock tracking app that that has the basic functionalities. It's missing important functionalities such as adding and removing stocks, sorting, and some bindings in the main page have not been implemented yet.
For testing purposes, let's say we want to track 3 shares of Apple from the exchange Borsa Italiana. We can then add the following json to a new file stockPositions.txt in the AppData directory of the current application (FileSystem.Current.AppDataDirectory). This will be unique for you situation, but should be similar to C:\Users\username\AppData\Local\Packages\package id\LocalState\stockPositions.txt. The application will load the missing data.

json
[{"Ticker":{"name":"APPLE","symbol":"AAPL.XMIL","stock_exchange":{"name":"Borsa Italiana","acronym":"MIL","mic":"XMIL","country":"Italy","country_code":"IT","city":"Milano","website":"www.borsaitaliana.it","timezone":null}}, "Position":3}]