Skip to content

Commit b987698

Browse files
committed
Changed SummaryService.cs in such way that it does now correctly handle zero totalCost values.
Implemented fetching market entries on the portfolio detail page and calculating the portfolio summary. Added CryptoNameResolver singleton into the DI graph. Added Back button to the Portfolio Entry Detail page Improved the sorting method of cryptocurrencies on the New Portfolio page (items are now ordered based on whether they are present within the portfolio and only after that they are ordered based on the length of their symbol).
1 parent 2038c09 commit b987698

File tree

6 files changed

+202
-4
lines changed

6 files changed

+202
-4
lines changed

Services/Services/Services.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<ProjectReference Include="..\..\CryptoStatsSource\CryptoStatsSource.csproj" />
89
<ProjectReference Include="..\..\Model\Model.csproj" />
910
<ProjectReference Include="..\..\Repository\Repository.csproj" />
1011
</ItemGroup>

Services/Services/SummaryService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public ISummaryService.Summary GetAverageOfSummaries(IEnumerable<ISummaryService
4141
totalCost += summary.Cost;
4242
totalAbsoluteChange += summary.AbsoluteChange;
4343
}
44+
45+
if (totalCost == 0)
46+
{
47+
return new ISummaryService.Summary(0,0,0,0);
48+
}
4449

4550
decimal totalRelativeChange = (totalMarketValue / totalCost) -1m;
4651

@@ -70,6 +75,11 @@ public ISummaryService.Summary GetPortfolioEntrySummary(List<MarketOrder> portfo
7075
totalFee += order.Fee;
7176
});
7277

78+
if (totalCost == 0)
79+
{
80+
return new ISummaryService.Summary(0,0,0,0);
81+
}
82+
7383
decimal currentTotalHoldingValue = totalHoldingSize * assetPrice;
7484

7585
decimal totalAbsoluteChange = currentTotalHoldingValue + totalSellValue - totalCost - totalFee;

WebFrontend/Pages/NewPortfolioEntry.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<div class="mat-layout-grid">
3232
<div class="mat-layout-grid-inner">
3333
<div class="mat-layout-grid-cell-span-6">
34-
<MatH5><MatButton Outlined="true" Icon="keyboard_arrow_left" Style="margin-right: 1rem;" OnClick='() => { NavigationManager.NavigateTo($"/portfolio/{Portfolio.Id}"); }'>Back</MatButton>Manage entries of <b>@Portfolio.Name</b></MatH5>
34+
<MatH5><MatButton Outlined="true" Icon="keyboard_arrow_left" Style="margin-right: 1rem;" OnClick='() => { NavigationManager.NavigateTo($"/portfolios/{Portfolio.Id}"); }'>Back</MatButton>Manage entries of <b>@Portfolio.Name</b></MatH5>
3535
<MatTextField Label="Filter by symbol" Style="margin-bottom: 2rem;" Icon="filter_list" FullWidth="true" @bind-Value="@CryptocurrencyFilter"></MatTextField>
3636
@if (AvailableCryptocurrenciesWithUsage == null)
3737
{
@@ -128,7 +128,7 @@
128128
var entriesSymbols = PortfolioEntries.Select(e => e.Symbol.ToLower());
129129
AvailableCryptocurrenciesWithUsage = availableCryptocurrencies.Select(
130130
c => new Tuple<Cryptocurrency, bool>(c, !entriesSymbols.Contains(c.Symbol.ToLower()))
131-
).OrderBy(c => c.Item1.Symbol.Length).ToList();
131+
).OrderBy(c => c.Item2).ThenBy(c => c.Item1.Symbol.Length).ToList();
132132
}
133133

134134
private void OnAddCurrencyClicked(Cryptocurrency cryptocurrency)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
@page "/portfolios/{portfolioId:int}"
2+
@using Model
3+
@using Services
4+
@using Utils
5+
@using CryptoStatsSource
6+
@using CryptoStatsSource.model
7+
@using System.Diagnostics
8+
@inject Microsoft.AspNetCore.Components.NavigationManager NavigationManager
9+
@inject IPortfolioService PortfolioService
10+
@inject IPortfolioEntryService PortfolioEntryService
11+
@inject IMarketOrderService MarketOrderService;
12+
@inject ICryptoStatsSource CryptoStatsSource;
13+
@inject ISummaryService SummaryService;
14+
@inject ICryptoNameResolver CryptoNameResolver;
15+
16+
<style>
17+
.demo-mat-card {
18+
margin-bottom: 2em;
19+
}
20+
21+
.demo-mat-card-content {
22+
padding: 1rem;
23+
}
24+
25+
.clear-margin {
26+
margin: 0px;
27+
}
28+
29+
.app-fab--absolute {
30+
position: fixed;
31+
bottom: 1rem;
32+
right: 1rem;
33+
}
34+
35+
</style>
36+
<div class="mat-layout-grid mat-layout-grid-align-center">
37+
<div class="mat-layout-grid-inner center">
38+
39+
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-3"></div>
40+
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-6">
41+
@if (activePortfolio != null)
42+
{
43+
<MatCard class="demo-mat-card">
44+
<MatCardContent>
45+
<div class="demo-mat-card-content">
46+
<MatHeadline6 class="clear-margin">
47+
<MatChipSet Style="align-items: center">
48+
<MatH5 Class="clear-margin">@activePortfolio.Name</MatH5>
49+
<MatChip Style="vertical-align: center" Label="@CurrencyUtils.GetCurrencyLabel(activePortfolio.Currency)"/>
50+
</MatChipSet>
51+
</MatHeadline6>
52+
</div>
53+
54+
<MatBody2 class="demo-mat-card-content clear-margin">
55+
<div class="mat-layout-grid">
56+
<div class="mat-layout-grid-inner" style="align-items: center">
57+
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-6">
58+
<MatH4 Class="clear-margin">@(CurrencyUtils.Format(portfolioSummary.MarketValue, activePortfolio.Currency))</MatH4>
59+
</div>
60+
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-6" style="text-align: end">
61+
@(portfolioSummary.RelativeChange * 100m) %
62+
</div>
63+
</div>
64+
</div>
65+
</MatBody2>
66+
</MatCardContent>
67+
</MatCard>
68+
<MatTable Items="@portfolioEntryRows" Striped="true" AllowSelection="true" RowClass="tester" class="mat-elevation-z5" ShowPaging="false" PageSize="9999" SelectionChanged="SelectionChangedEvent">
69+
<MatTableHeader>
70+
<th>Coin</th>
71+
<th>Price</th>
72+
<th>Change (1h)</th>
73+
<th>Holdings</th>
74+
</MatTableHeader>
75+
<MatTableRow>
76+
<td>@context.symbol.ToUpper()</td>
77+
<td>@(CurrencyUtils.Format(context.currentPrice, activePortfolio.Currency))</td>
78+
<td style='color: @(context.relativeChange >= 0 ? "#17a104" : "#FF0000")'>@context.relativeChange%</td>
79+
<td>@context.percentage%</td>
80+
</MatTableRow>
81+
</MatTable>
82+
}
83+
else
84+
{
85+
<MatProgressBar Indeterminate="true"></MatProgressBar>
86+
}
87+
</div>
88+
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-3"></div>
89+
</div>
90+
</div>
91+
<MatFAB Class="app-fab--absolute" Icon="@MatIconNames.Add" Label="Add a new entry" OnClick='() => { NavigationManager.NavigateTo($"newportfolioentry/{activePortfolio.Id}"); }'></MatFAB>
92+
93+
94+
@code
95+
{
96+
[Parameter]
97+
public int PortfolioId { get; set; }
98+
99+
protected Portfolio activePortfolio;
100+
101+
protected ISummaryService.Summary portfolioSummary = new(1341m, 1.8m, 9982.489m, 1000m);
102+
103+
protected List<PortfolioEntry> activePortfolioEntries = new List<PortfolioEntry>()
104+
{
105+
new("btc", 1, 1),
106+
new("ada", 1, 2),
107+
new("eth", 1, 3),
108+
new("ltc", 1, 4),
109+
new("link", 1, 5),
110+
};
111+
112+
protected List<decimal> portfolioHoldings = new()
113+
{
114+
44.8886m,
115+
28.18m,
116+
10.116m,
117+
9.38m,
118+
2.70m,
119+
};
120+
121+
protected List<PortfolioEntryRow> portfolioEntryRows = new()
122+
{
123+
new("btc", 57644.42m, 1.35m, 44.76m),
124+
new("ada", 1.36m, 0.58m, 28.18m),
125+
new("eth", 3279.64m, 10.95m, 27.11m),
126+
new("ltc", 291.55m, 7.20m, 9.38m),
127+
new("link", 42.20m, -5.19m, 2.70m)
128+
};
129+
130+
protected record PortfolioEntryRow(string symbol, decimal currentPrice, decimal relativeChange, decimal percentage);
131+
132+
protected override void OnInitialized()
133+
{
134+
activePortfolio = PortfolioService.GetPortfolio(PortfolioId);
135+
activePortfolioEntries = PortfolioEntryService.GetPortfolioEntries(PortfolioId);
136+
}
137+
138+
protected override async Task OnInitializedAsync()
139+
{
140+
var marketEntries = (await CryptoStatsSource.GetMarketEntries(
141+
CurrencyUtils.GetCurrencyLabel(activePortfolio.Currency).ToLower(),
142+
await Task.WhenAll(activePortfolioEntries.Select(async entry => (await CryptoNameResolver.Resolve(entry.Symbol)).ToLower()))
143+
)).ToDictionary(entry => entry.Symbol, entry => entry);
144+
145+
146+
List<ISummaryService.Summary> summaries = new List<ISummaryService.Summary>();
147+
foreach (var portfolioEntry in activePortfolioEntries)
148+
{
149+
var marketEntry = marketEntries.GetValueOrDefault(portfolioEntry.Symbol);
150+
151+
if (marketEntry == null)
152+
{
153+
// TODO market entry is null
154+
}
155+
var entryOrders = MarketOrderService.GetPortfolioEntryOrders(portfolioEntry.Id);
156+
summaries.Add(SummaryService.GetPortfolioEntrySummary(entryOrders, marketEntry.CurrentPrice));
157+
}
158+
portfolioSummary = SummaryService.GetPortfolioSummary(summaries);
159+
160+
if (portfolioSummary.Cost == 0)
161+
{
162+
portfolioSummary = portfolioSummary with{
163+
RelativeChange = 0
164+
};
165+
}
166+
167+
var totalMarketValue = summaries.Sum(summary => summary.MarketValue);
168+
169+
170+
portfolioEntryRows = summaries.Zip(activePortfolioEntries).Select((summary) => new PortfolioEntryRow(summary.Second.Symbol, marketEntries[summary.Second.Symbol].CurrentPrice, new decimal(marketEntries[summary.Second.Symbol].PriceChangePercentage24H), totalMarketValue > 0 ? (summary.First.MarketValue / totalMarketValue) * 100 : 0)).ToList();
171+
}
172+
173+
public void SelectionChangedEvent(object row)
174+
{
175+
if (row == null)
176+
{
177+
}
178+
else
179+
{
180+
NavigationManager.NavigateTo($"entrydetail");
181+
}
182+
}
183+
184+
}

WebFrontend/Pages/PortfolioEntryDetail.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
</style>
3232
<div class="mat-layout-grid mat-layout-grid-align-center">
3333
<div class="mat-layout-grid-inner center">
34-
3534
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-2"></div>
3635
<div class="mat-layout-grid-cell mat-layout-grid-cell-span-8">
36+
<MatButton Outlined="true" Icon="keyboard_arrow_left" Style="margin-bottom: 1rem;" OnClick='() => { NavigationManager.NavigateTo($"/portfolios/{activePortfolio.Id}"); }'>Back</MatButton>
3737
<MatCard class="demo-mat-card">
3838
<MatCardContent>
3939
<div class="demo-mat-card-content">

WebFrontend/Startup.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ public void ConfigureServices(IServiceCollection services)
4646
config.VisibleStateDuration = 3000;
4747
});
4848

49-
services.AddScoped<ICryptoStatsSource, CoingeckoSource>();
49+
services.AddSingleton<ICryptoStatsSource, CoingeckoSource>();
5050

5151
// TODO ensure that SqlKataDatabase gets disposed
5252
var dbConnection = new SqliteConnection("Data Source=data.db");
5353
var db = new SqlKataDatabase(dbConnection, new SqliteCompiler());
5454
services.AddSingleton(ctx => db);
55+
56+
services.AddSingleton<ICryptoNameResolver, CryptoNameResolverImpl>();
57+
services.AddSingleton<ISummaryService, SummaryServiceImpl>();
5558

5659
services.AddSingleton<IPortfolioRepository, SqlKataPortfolioRepository>();
5760
services.AddSingleton<IMarketOrderRepository, SqlKataMarketOrderRepository>();

0 commit comments

Comments
 (0)