Skip to content

Commit fa67c22

Browse files
committed
calculate IBOND prices
1 parent 3a8d761 commit fa67c22

File tree

7 files changed

+118
-4
lines changed

7 files changed

+118
-4
lines changed

Pages/Portfolio.razor

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,7 @@
10871087
}
10881088
private async Task UpdateInvestmentsPrice(string ticker, List<Investment> investments)
10891089
{
1090-
if (!string.IsNullOrEmpty(appData.EODHistoricalDataApiKey)) {
1090+
if (!string.IsNullOrEmpty(appData.EODHistoricalDataApiKey) && !string.IsNullOrEmpty(ticker)) {
10911091
var quoteDataJson = await Http.GetStreamAsync($"https://api.bogle.tools/api/getquotes?ticker={ticker}&apikey={appData.EODHistoricalDataApiKey}");
10921092
var quoteData = await JsonSerializer.DeserializeAsync<QuoteData>(quoteDataJson);
10931093
if (quoteData?.Close != null) {
@@ -1279,6 +1279,11 @@
12791279
fetchQuote = investment.LastUpdated == null || investment.LastUpdated?.Date != previousMarketClose.Date;
12801280
}
12811281

1282+
if (investment.AssetType == AssetType.IBond)
1283+
{
1284+
await CalculateIBondValue(investment);
1285+
}
1286+
12821287
if (fetchQuote && investment.Ticker != null) {
12831288
if (!quotes.ContainsKey(investment.Ticker)) {
12841289
quotes.Add(investment.Ticker, new List<Investment> () { investment });
@@ -1322,6 +1327,111 @@
13221327
refreshButtonText = "🔃 Quotes";
13231328
}
13241329

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+
13251435
enum MarketDay {
13261436
MarketDay,
13271437
Holiday,

Shared/Models/FamilyData/Account.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ public void UpdateInvestmentCategoryTotals(Investment investment, FamilyData fam
212212
case AssetType.Bond:
213213
case AssetType.Bond_ETF:
214214
case AssetType.Bond_Fund:
215+
case AssetType.IBond:
215216
case AssetType.InternationalBond:
216217
case AssetType.InternationalBond_ETF:
217218
case AssetType.InternationalBond_Fund:

Shared/Models/FamilyData/Advisor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public static List<string> Advise(Investment investment, Account account, IAppDa
3737
adviceItems.Add("2nd: international->taxable");
3838
}
3939
break;
40+
case AssetType.IBond:
4041
case AssetType.Bond:
4142
case AssetType.Bond_ETF:
4243
case AssetType.Bond_Fund:

Shared/Models/FamilyData/AssetType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public enum AssetType
77
Cash,
88
Cash_BankAccount,
99
Cash_MoneyMarket,
10+
IBond,
1011
InternationalStock,
1112
InternationalStock_ETF,
1213
InternationalStock_Fund,

Shared/Models/FamilyData/Investment.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public int InvestmentOrder {
217217
{
218218
global::AssetType.USStock or global::AssetType.USStock_ETF or global::AssetType.USStock_Fund or global::AssetType.Stock=> 1,
219219
global::AssetType.InternationalStock or global::AssetType.InternationalStock_ETF or global::AssetType.InternationalStock_Fund => 2,
220-
global::AssetType.Bond or global::AssetType.Bond_ETF or global::AssetType.Bond_Fund or global::AssetType.InternationalBond or global::AssetType.InternationalBond_ETF or global::AssetType.InternationalBond_Fund => 3,
220+
global::AssetType.Bond or global::AssetType.IBond or global::AssetType.Bond_ETF or global::AssetType.Bond_Fund or global::AssetType.InternationalBond or global::AssetType.InternationalBond_ETF or global::AssetType.InternationalBond_Fund => 3,
221221
global::AssetType.StocksAndBonds_ETF or global::AssetType.StocksAndBonds_Fund => 4,
222222
global::AssetType.Cash or global::AssetType.Cash_BankAccount or global::AssetType.Cash_MoneyMarket => 5,
223223
_ => 6,
@@ -334,6 +334,7 @@ public void UpdateValue() {
334334
public double? PreviousClose { get; set; }
335335
public double? PercentChange { get; set; }
336336
public DateTime? LastUpdated { get; set; }
337+
337338
[JsonIgnore]
338339
public double Percentage { get; set; }
339340
}

wwwroot/cache.manifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
CACHE MANIFEST
22

3-
# Version 1.0107
3+
# Version 1.0108
44

55
NETWORK:
66
*

wwwroot/data/funds.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)