|
1087 | 1087 | }
|
1088 | 1088 | private async Task UpdateInvestmentsPrice(string ticker, List<Investment> investments)
|
1089 | 1089 | {
|
1090 |
| - if (!string.IsNullOrEmpty(appData.EODHistoricalDataApiKey)) { |
| 1090 | + if (!string.IsNullOrEmpty(appData.EODHistoricalDataApiKey) && !string.IsNullOrEmpty(ticker)) { |
1091 | 1091 | var quoteDataJson = await Http.GetStreamAsync($"https://api.bogle.tools/api/getquotes?ticker={ticker}&apikey={appData.EODHistoricalDataApiKey}");
|
1092 | 1092 | var quoteData = await JsonSerializer.DeserializeAsync<QuoteData>(quoteDataJson);
|
1093 | 1093 | if (quoteData?.Close != null) {
|
|
1279 | 1279 | fetchQuote = investment.LastUpdated == null || investment.LastUpdated?.Date != previousMarketClose.Date;
|
1280 | 1280 | }
|
1281 | 1281 |
|
| 1282 | + if (investment.AssetType == AssetType.IBond) |
| 1283 | + { |
| 1284 | + await CalculateIBondValue(investment); |
| 1285 | + } |
| 1286 | + |
1282 | 1287 | if (fetchQuote && investment.Ticker != null) {
|
1283 | 1288 | if (!quotes.ContainsKey(investment.Ticker)) {
|
1284 | 1289 | quotes.Add(investment.Ticker, new List<Investment> () { investment });
|
|
1322 | 1327 | refreshButtonText = "🔃 Quotes";
|
1323 | 1328 | }
|
1324 | 1329 |
|
| 1330 | + public static Dictionary<string,List<double>>? IBondRates { get; set; } |
| 1331 | + |
| 1332 | + async Task LoadIBondRates() { |
| 1333 | + IBondRates = new(); |
| 1334 | + var ibondsUri = new Uri("https://raw.githubusercontent.com/bogle-tools/financial-variables/main/data/usa/treasury-direct/i-bond-rate-chart.csv"); |
| 1335 | + var httpClient = new HttpClient(); |
| 1336 | + var ibondsCsv = await httpClient.GetAsync(ibondsUri.AbsoluteUri); |
| 1337 | + var stream = await ibondsCsv.Content.ReadAsStreamAsync(); |
| 1338 | + using var reader = new CsvReader(stream); |
| 1339 | + var RowEnumerator = reader.GetRowEnumerator().GetAsyncEnumerator(); |
| 1340 | + await RowEnumerator.MoveNextAsync(); |
| 1341 | + await RowEnumerator.MoveNextAsync(); |
| 1342 | + while (await RowEnumerator.MoveNextAsync()) |
| 1343 | + { |
| 1344 | + string[] chunks = RowEnumerator.Current; |
| 1345 | + int chunkNum = 0; |
| 1346 | + string? date = null; |
| 1347 | + List<double> rates = new(); |
| 1348 | + foreach (var chunk in chunks) |
| 1349 | + { |
| 1350 | + switch (chunkNum) |
| 1351 | + { |
| 1352 | + case 0: |
| 1353 | + date = chunk[..5]; |
| 1354 | + if (!char.IsDigit(date[0])) |
| 1355 | + { |
| 1356 | + // lines at bottom of the csv file that don't start with a dates should be skipped. |
| 1357 | + return; |
| 1358 | + } |
| 1359 | + break; |
| 1360 | + case 1: |
| 1361 | + break; |
| 1362 | + default: |
| 1363 | + if (string.IsNullOrEmpty(chunk)) |
| 1364 | + { |
| 1365 | + continue; |
| 1366 | + } |
| 1367 | + else |
| 1368 | + { |
| 1369 | + var rate = DoubleFromPercentageString(chunk); |
| 1370 | + rates.Add(rate); |
| 1371 | + } |
| 1372 | + break; |
| 1373 | + } |
| 1374 | + |
| 1375 | + chunkNum++; |
| 1376 | + } |
| 1377 | + |
| 1378 | + IBondRates[date!] = rates; |
| 1379 | + } |
| 1380 | + } |
| 1381 | + |
| 1382 | + string GetRateDate(int month, int year) |
| 1383 | + { |
| 1384 | + if (month < 5) { |
| 1385 | + return "11/" + (year-1).ToString().Substring(2); |
| 1386 | + } |
| 1387 | + else if (month < 11) { |
| 1388 | + return "05/" + (year).ToString().Substring(2); |
| 1389 | + } else { |
| 1390 | + return "11/" + (year).ToString().Substring(2); |
| 1391 | + } |
| 1392 | + } |
| 1393 | + |
| 1394 | + async Task CalculateIBondValue(Investment investment) |
| 1395 | + { |
| 1396 | + if (IBondRates == null) { |
| 1397 | + await LoadIBondRates(); |
| 1398 | + } |
| 1399 | + |
| 1400 | + if (IBondRates != null) |
| 1401 | + { |
| 1402 | + if (investment.PurchaseDate.HasValue) |
| 1403 | + { |
| 1404 | + var month = investment.PurchaseDate.Value.Month; |
| 1405 | + var year = investment.PurchaseDate.Value.Year; |
| 1406 | + var date = GetRateDate(month, year); |
| 1407 | + var rates = Portfolio.IBondRates[date]; |
| 1408 | + |
| 1409 | + var nowMonth = DateTime.Now.Month; |
| 1410 | + var nowYear = DateTime.Now.Year; |
| 1411 | + double value = investment.CostBasis ?? 0.0; |
| 1412 | + double bondQuantity = (value / 25.0); |
| 1413 | + for (int i = rates.Count - 1; i >= 0; i--) |
| 1414 | + { |
| 1415 | + var monthCount = i > 0 ? 6 : GetMonthsLeft(investment.PurchaseDate.Value, DateTime.Now); |
| 1416 | + value = (bondQuantity*Math.Round(value/bondQuantity*Math.Pow((1.0+rates[i]/2.0),((double)monthCount/6.0)),2)); |
| 1417 | + } |
| 1418 | + |
| 1419 | + investment.ValuePIN = (int)value; |
| 1420 | + } |
| 1421 | + } |
| 1422 | + } |
| 1423 | + |
| 1424 | + private int GetMonthsLeft(DateOnly purchaseDate, DateTime now) |
| 1425 | + { |
| 1426 | + var months = ((now.Year - purchaseDate.Year) * 12 + now.Month - purchaseDate.Month) % 6; |
| 1427 | + return months; |
| 1428 | + } |
| 1429 | + |
| 1430 | + private static double DoubleFromPercentageString(string value) |
| 1431 | + { |
| 1432 | + return double.Parse(value.Replace("%","")) / 100; |
| 1433 | + } |
| 1434 | + |
1325 | 1435 | enum MarketDay {
|
1326 | 1436 | MarketDay,
|
1327 | 1437 | Holiday,
|
|
0 commit comments