diff --git a/Pages/About.razor b/Pages/About.razor index 1f78e7a..5bd43f1 100644 --- a/Pages/About.razor +++ b/Pages/About.razor @@ -9,6 +9,7 @@ This site aims to provide useful tools to people interested in optimizing their It provides tools to help you: @@ -18,6 +19,7 @@ It provides tools to help you:

Feedback Welcome: via email to suggestions@bogle.tools or via Private Message to Rob Relyea on bogleheads.org


-Privacy - None of the answers you fill out on this site get sent to a server. All data and calculations are done on your device (PC/Phone/Tablet).
+Privacy - None of your data leaves this device (PC/Phone/Tablet). If you save it, it will be saved to this browser's local storage only.
+(if you set up stock price updating, requests for quotes contain ticker symbols only.)

Programmers - This site is open source. See github.com/bogle-tools/site ā†—ļø for code and an issues list.
diff --git a/Pages/Home.razor b/Pages/Home.razor index b0e43c9..74b02ff 100644 --- a/Pages/Home.razor +++ b/Pages/Home.razor @@ -5,6 +5,7 @@ bogle.tools

šŸ“ˆ Portfolio - organize your portfolio

+

šŸ¤” Portfolio Review - gather background info and questions

šŸ’° Saving Plan - plan your annual savings


diff --git a/Pages/Portfolio-Review.razor b/Pages/Portfolio-Review.razor new file mode 100644 index 0000000..95e8ee6 --- /dev/null +++ b/Pages/Portfolio-Review.razor @@ -0,0 +1,1251 @@ +@page "/portfolio-review" +@page "/portfolio-review/reload" +@page "/portfolio-review/topic/{TopicValue}" +@page "/portfolio-review/{stepPath}" +@inject HttpClient Http +@inject IAppData appData +@inject IRSData irsData +@inject IJSRuntime JS +@inject NavigationManager Navigation +@inject IList Funds +@using System.Text.Json.Serialization + + +Portfolio Review@(stepPath==null?"":": "+stepPath.Replace('-',' ')) - bogle.tools + + + + @if (new Uri(Navigation.Uri).PathAndQuery == "/portfolio-review/reload") + { + // reload is a workaround where the correct data seemed to be deleted, but i needed to navigate away and then back to retirement-assets + Navigation.NavigateTo("/portfolio-review/retirement-assets"); + } + @if (steps == null || familyData == null) + { +

Loading...

+ } + else + { + @if (stepPath == null) { + var prevStep = steps[steps.Length - 1]; + var nextStep = steps[0]; + string prevPage = folderName + prevStep?.step; + string nextPage = folderName + nextStep?.step; + familyData.UpdatePercentages(); + +

+ Portfolio Review
+
+

+

Bogleheads.org forum enables you to Ask Portfolio Questions ā†—ļø. This tool makes it easy to collect your information and format it properly.

+
+

Press edit (āœļø) to complete each step:

+ + + @foreach (var step in steps) { + var href = folderName + @step.step; + + + @switch (step.step) { + case "age": + + break; + case "emergency-fund": + + break; + case "debts": + + break; + case "tax-status": + + break; + case "asset-allocation": + + break; + case "retirement-assets": + + break; + case "available-funds": + + break; + case "additional-background": + @((MarkupString)boldUnderline("Additional Background:"))
+ @familyData.AdditionalBackground +
+ break; + case "questions": + + break; + case "contributions": + runningTotal = 0; + pastTotal = false; + + + break; + default: + + break; + } + + } +
+ + āœļø + + + @((MarkupString)bold("Age:")) + @if(familyData.PersonCount > 0 && familyData.People[0].Age != null) { +  @familyData.People[0].Age @getPronoun(familyData.People[0]) + } + @if(familyData.PersonCount > 1 && familyData.People[1].Age != null) { +  and @familyData.People[1].Age @getPronoun(familyData.People[1]) + } +

+
+ @((MarkupString)bold("Emergency funds:")) + @if (familyData.EmergencyFund.CurrentMonths != null) { +  @familyData.EmergencyFund.CurrentMonthsString months + } else if (familyData.EmergencyFund.ShowDollars) { +  @formatMoney(familyData.EmergencyFund.CurrentBalance) + } +

+
+ @((MarkupString)bold("Debt:")) + @if (familyData.Debts.Count == 0 && familyData.DebtsComplete) {  no debts } + else if (familyData.Debts.Count >= 1) { +
+ @foreach (var debt in familyData.Debts) + { +   @debt.Name: @formatMoney(debt.Total) @@ @(debt.Rate == null ? "unknown" : debt.Rate)%
+ } + } +
+
+ + @((MarkupString)bold("Tax Filing Status:")) + @if (familyData.TaxFilingStatus != TaxFilingStatus.ChoiceNeeded) {  @familyData.TaxFilingStatus } +

+ @((MarkupString)bold("Tax Rate:")) + @if (familyData.FederalMarginalTaxBracket != null) {  @familyData.FederalMarginalTaxBracket Federal, @familyData.StateMarginalTaxBracket% State } +

+ @((MarkupString)bold("State of Residence:")) + @if (familyData.StateMarginalTaxBracket != null) {  @familyData.StateOfResidence } +
+

+
+ @((MarkupString)bold("Desired Asset allocation:")) + @if (familyData.Stocks != null && familyData.Bonds != null) { +  @familyData.Stocks% stocks / @familyData.Bonds% bonds
+ } else { +
+ } +   Desired International allocation: + @if (familyData.International != null) { + @familyData.International% + } +

+
+ @((MarkupString)bold("Portfolio Size:")) @(familyData.ValueStyle==0?formatMoneyThousands(familyData.Value):(familyData.ValueStyle==1?estimatePortfolio(familyData):formatMoney(familyData.Value)))
+
+ @if (familyData.Accounts.Count > 0) { + + + + + +
+ + @if (showPortfolioAnalysis) { + @if (!double.IsNaN(familyData.ActualStockAllocation) || !double.IsNaN(familyData.ActualBondAllocation) || !double.IsNaN(familyData.ActualInternationalStockAllocation)) { + @((MarkupString)bold("Actual Asset allocation: ")) +  @formatPercent(familyData.ActualStockAllocation*100.0) stocks / @formatPercent(familyData.ActualBondAllocation*100.0) bonds
+   Actual International allocation: + @formatPercent(familyData.ActualInternationalStockAllocation*100.0)
+   Actual Cash allocation: + @formatPercent(familyData.ActualCashAllocation*100.0)
+ @if(familyData.ActualOtherAllocation > 0.0) { +   Actual Unclassified allocation: + @formatPercent(familyData.ActualOtherAllocation*100.0)
+ } +
+ } + + @((MarkupString)bold("Weighted Expense Ratio: ")) +  @formatPercent3(familyData.OverallER) or @formatMoney(familyData.ExpensesTotal) per year + @if (familyData.InvestmentsMissingER > 0){ (@familyData.InvestmentsMissingER investment(s) missing ER)
} + } +
+
+ } + + @((MarkupString)boldUnderline("Current Retirement Assets:"))
+ + @foreach (var account in familyData.Accounts) { +
+ @((MarkupString)bold((account.Identifier != null ? ""+account.Identifier+" " : "") + account.AccountType + (account.Custodian != null ? " at " + account.Custodian : "") + " (" + formatPercent(account.Percentage) + ")" )) +
+ @foreach (var investment in account.Investments) { +     @formatPercent(investment.Percentage) @investment.Name (@investment.Ticker) (@(investment.ExpenseRatio!=null?investment.ExpenseRatio:"??")%)
+ } + } +
+
+ @((MarkupString)boldUnderline("Available funds:"))
+ @foreach (var account in familyData.Accounts) { + @if (account.AvailableFunds.Count > 0) { +
+ @((MarkupString)bold("Funds available in " + account.FullName))
+
+ @foreach (var investment in account.AvailableFunds) { +     @investment.Name (@investment.Ticker) (@investment.ExpenseRatio%)
+ } + } + } +
+
+ @((MarkupString)boldUnderline("Questions:"))
+ @for (int i = 0; i < familyData.Questions.Count; i++) { + var question = familyData.Questions[i]; + @(i+1 + ".") @question +

+ } +
+ + @((MarkupString)boldUnderline("Contributions:")) +
+ @if (familyData.PlannedSavings > 0) { + Planned savings: @formatMoney(familyData.PlannedSavings)

+ } + + Prioritized Investments:
+ + @if (familyData.EmergencyFund.AmountToSave > 0) { + @formatMoney(familyData.EmergencyFund.AmountToSave) in Emergency Fund
+ @((MarkupString)checkTotal(familyData.EmergencyFund.AmountToSave)) + } + @if (familyData.PersonCount > 0 && familyData.People[0].EmployerPlan.AmountToSaveForMatch > 0) { + @formatMoney(familyData.People[0].EmployerPlan.AmountToSaveForMatch) in @familyData.People[0].PossessiveID 401k (Match: @formatMoney(familyData.People[0].EmployerPlan.MatchAmount))
+ @((MarkupString)checkTotal(familyData.People[0].EmployerPlan.AmountToSaveForMatch)) + } + @if (familyData.PersonCount > 1 && familyData.People[1].EmployerPlan.AmountToSaveForMatch > 0) { + @formatMoney(familyData.People[1].EmployerPlan.AmountToSaveForMatch) in @familyData.People[1].PossessiveID 401k (Match: @formatMoney(familyData.People[1].EmployerPlan.MatchAmount))
+ @((MarkupString)checkTotal(familyData.People[1].EmployerPlan.AmountToSaveForMatch)) + } + @if (familyData.HighDebts > 0) { + @familyData.HighDebts in High Debts
+ @((MarkupString)checkTotal(familyData.HighDebts)) + } + @if (familyData.PersonCount > 0 && (familyData.People[0].EmployerBenefits.Complete || familyData.People[0].EmployerPlan.AnnualSalary == 0 || familyData.People[0].HealthSavingsAccount.EligibleForHSA || familyData.People[0].HealthSavingsAccount.EligibleForHSACatchUpOnly) && familyData.People[0].HealthSavingsAccount.AmountToSave > 0) { + @formatMoney(familyData.People[0].HealthSavingsAccount.AmountToSave) in @familyData.People[0].PossessiveID HSA (Employer: @formatMoney(familyData.People[0].HealthSavingsAccount.EmployerContribution))
+ @((MarkupString)checkTotal(familyData.People[0].HealthSavingsAccount.AmountToSave)) + } + @if (familyData.PersonCount > 1 && (familyData.People[1].EmployerBenefits.Complete || familyData.People[1].EmployerPlan.AnnualSalary == 0 || familyData.People[1].HealthSavingsAccount.EligibleForHSA || familyData.People[1].HealthSavingsAccount.EligibleForHSACatchUpOnly) && familyData.People[1].HealthSavingsAccount.AmountToSave > 0) { + @formatMoney(familyData.People[1].HealthSavingsAccount.AmountToSave) in @familyData.People[1].PossessiveID HSA (Employer: @formatMoney(familyData.People[1].HealthSavingsAccount.EmployerContribution))
+ @((MarkupString)checkTotal(familyData.People[1].HealthSavingsAccount.AmountToSave)) + } + @if (familyData.AdjustedGrossIncome != null) { + if (familyData.PersonCount > 0) { + @((MarkupString)GetRecommendedIRAMarkup(familyData.People[0]))
+ @((MarkupString)checkTotal(GetRecommdedIRAAmount(familyData.People[0]))) + } + if(familyData.PersonCount > 1) { + @((MarkupString)GetRecommendedIRAMarkup(familyData.People[1]))
+ @((MarkupString)checkTotal(GetRecommdedIRAAmount(familyData.People[1]))) + } + } + @if (familyData.PersonCount > 0 && familyData.People[0].EmployerPlan.AmountToSaveForNonMatched > 0) { + @formatMoney(familyData.People[0].EmployerPlan.AmountToSaveForNonMatched) in @familyData.People[0].PossessiveID 401k
+ @((MarkupString)checkTotal(familyData.People[0].EmployerPlan.AmountToSaveForNonMatched)) + } + @if (familyData.PersonCount > 1 && familyData.People[1].EmployerPlan.AmountToSaveForNonMatched > 0) { + @formatMoney(familyData.People[1].EmployerPlan.AmountToSaveForNonMatched) in @familyData.People[1].PossessiveID 401k
+ @((MarkupString)checkTotal(familyData.People[1].EmployerPlan.AmountToSaveForNonMatched)) + } + @if (familyData.PersonCount > 0 && familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit > 0) { + @formatMoney(familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit) in @familyData.People[0].PossessiveID mega backdoor roth
+ @((MarkupString)checkTotal(familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit)) + } + @if (familyData.PersonCount > 1 && familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit > 0) { + @formatMoney(familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit) in @familyData.People[1].PossessiveID mega backdoor roth
+ @((MarkupString)checkTotal(familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit)) + } + @if (familyData.MediumDebts > 0) { + @formatMoney(familyData.MediumDebts) in Medium Debts
+ @((MarkupString)checkTotal(familyData.MediumDebts)) + } + @if (familyData.PlannedSavings > runningTotal) { + @formatMoney(familyData.PlannedSavings-runningTotal) in Taxable
+ @((MarkupString)checkTotal(familyData.PlannedSavings-runningTotal)) + } else { + @formatMoney(0) in Taxable
+ } + @if (familyData.LowDebts > 0) { + @formatMoney(familyData.LowDebts) in Low Debts
+ @((MarkupString)checkTotal(familyData.LowDebts)) + } +
+
NYI
+
+ +

Copy the correct format for your post:

+
+
+
+
+ +
+ +
+ } + else + { + @foreach (var step in steps) { + @if (step.step == stepPath) { + var firstStepIndex = 0; + var lastStepIndex = steps.Length-1; + var prevStep = step.number - 2 >= firstStepIndex ? steps[step.number - 2] : null; + var nextStep = step.number <= lastStepIndex ? steps[step.number] : null; + string prevPage = folderName + prevStep?.step; + string nextPage = folderName + nextStep?.step; + +

+ Portfolio Review > @step.title (@step.number)
+ +
+

+
+ +

@((MarkupString)markupize(step.priority))

+ +

@((MarkupString)markupize(step.summary))

+ + @switch (step.step) { + case "emergency-fund": +

Worksheet:

+ + + $

+ $

+
+
@familyData.EmergencyFund.CurrentMonthsString months
+ @if(familyData.EmergencyFund.CurrentMonths == null && familyData.EmergencyFund.CurrentBalance != null) { +

+
+ (Months is MORE useful than a dollar amount.) + } +

+ break; + case "debts": +

Worksheet:

+
+ list of debts (all interest rates):

+ + @if (familyData.DebtFree == TriState.ChoiceNeeded || familyData.DebtFree == TriState.True) { + + @foreach (var option in Enum.GetValues()) { + + } +
+
+ } + + + + + + + + + + + + @for (var i=0;i + + + + + + + } +
rate
@debt.Category
+
+ + +

+

+ High Interest: @formatMoney(familyData.HighDebts)
+ Medium Interest: @formatMoney(familyData.MediumDebts)
+ Low Interest: @formatMoney(familyData.LowDebts)
+ Unknown Interest: @formatMoney(familyData.UnknownDebts)
+
+
+
+ + @code{ + void RemoveDebt(MouseEventArgs e, int debtIndex) { + familyData.Debts.RemoveAt(debtIndex); + } + void addDebt() + { + familyData?.Debts.Add(new Debt()); + } + } + break; + case "tax-status": +

Worksheet:

+ + +
+
+ +
+ + @foreach (var taxFiler in familyData.IRSData.TaxRateData.TaxData.TaxFilers) + { + bool isMatch = false; + switch (familyData.TaxFilingStatus.ToString()) + { + case "MarriedFilingSeperatelyAndLivingApart": + isMatch = taxFiler.TaxFilingStatus == "MarriedFilingSeperately"; + break; + default: + isMatch = taxFiler.TaxFilingStatus == familyData.TaxFilingStatus.ToString(); + break; + } + if (isMatch) + { + string? lastBracket = null; + int? lastStartAmount = null; + +
+ +
+ + + @foreach (var taxBracket in taxFiler.TaxBrackets) + { + if (lastBracket != null) { + var elName = "bracket"+lastBracket.Substring(0,lastBracket.Length-2); + + } + lastBracket = taxBracket.Rate; + lastStartAmount = taxBracket.StartAmount + 1; + } + +
 @(lastStartAmount==1?"Up":formatMoney(lastStartAmount)) to @formatMoney(taxBracket.StartAmount)
 @lastBracketOver @formatMoney(lastStartAmount-1)
+
+ } + } + +
+ +

+ + %

+

+
+
+ break; + case "age": +

Prerequisities:

+ +

+ +

Worksheet:

+ @for (int i = 0; i < familyData.PersonCount; i++) { + var person = familyData.People[i]; + int personIndex = i + 1; +

Person @personIndex:

+ + +
+ + @if(person?.FamilyData?.PersonCount>1){
} + +
+

+ } + break; + case "asset-allocation": +

Worksheet:

+ + +
+ %
+ %
+
+
+ %
+

+ break; + case "available-funds": + + + +
+ + @if(currentAccountIndex != null && currentAccountIndex != "Choose Account...") { + int index = int.Parse(currentAccountIndex); + var currentAccount = familyData.Accounts[index]; + for (int i=0;i < currentAccount.AvailableFunds.Count; i++) { + var investment = currentAccount.AvailableFunds[i]; + int invBuffer = i; +
+ + +
+
+ +  ( + + %) +
+ } +
+ + } + + break; + case "additional-background": + + + + break; + case "questions": + + @for (int i = 0; i < familyData.Questions.Count; i++) { + int iBuffer = i; + + +
+
+ } +
+ +
+ @code { + void addQuestion() { + familyData.Questions.Add(""); + } + } + + break; + case "retirement-assets": + @if(ImportResult == null) { +

Prerequisites:

+ + +
+
+ + Current Retirement Assets:
+ + accountIndex = 0; + @foreach (var account in familyData.Accounts) { + int accBuffer = accountIndex; + accountIndex++; + var accountId = "account"+accBuffer; +
+
+ + @if (familyData.PersonCount == 2) { + + } + + at
+ +
+ + investmentIndex = 0; + @foreach (var investment in account.Investments) { + int invBuffer = investmentIndex; + investmentIndex++; +
+ + + Balance šŸŸ° $ +
+
+ +  ( + + %) +
+ } + +
+
Account Total: @formatMoney(account.Value)
+ } +
+
+
Portfolio Size: @formatMoney(familyData.Value)
+
+
+ to manually enter another Account's info. +
+
+
+ to import Account info from a CSV/XLSX file. + @if(showError){

ERROR: must be a CSV file from Ameriprise, eTrade, Fidelity, Merrill Edge, Schwab, or Vanguard or a XLSX file from Morgan Stanley.
} + @if(showImport){ +
+
+ Download portfolio details from custodian website, and then "choose file", to import:
+
+
    +
  • Currently supports importing Ameriprise, eTrade, Fidelity, Merrill Edge, Morgan Stanley, Schwab, or Vanguard data files
  • +
  • If your custodian's format isn't supported yet, we're happy to try to add support. If you can share your custodian's downloaded CSV file format (feel free to anonymize/change the data), please email it to suggestions@bogle.tools with details on what custodian it is from.
  • +
  • CAUTION: only share personal info (like these files) to apps worthy of your trust!
  • +
+
+ } +
+
+ + @if(familyData.Value > 0) { +
+
Preferred technique to represent the approximate size of your total portfolio (in portfolio review):
+ +
+
+
+
+ } + } else { + +
Data Files Contains These Accounts/Investments - Choose Which to Import:
+
+ + +
+
+ + @foreach (var error in ImportResult.Errors) + { +
Error: @error.Exception.Message
+ @if(error.Exception.InnerException!=null){
Failure location:
@error.Exception.InnerException.StackTrace

}; + } + +
UPDATE these EXISTING accounts in your data file:
+ @foreach (var account in ImportResult.UpdatedAccounts) + { +
@account.Custodian @account.Note => @formatMoney(account.Value)
+ @foreach (var investment in account.Investments) + { +
@investment.Name (@investment.Ticker) @(investment.Shares) shares => @formatMoney(investment.Value)
+ } + } + +
+
ADD these NEW accounts in your data file:
+ @foreach (var account in ImportResult.NewAccounts) + { +
@account.Custodian @account.Note => @formatMoney(account.Value)
+ @foreach (var investment in account.Investments) + { +
@investment.Name (@investment.Ticker) @(investment.Shares) shares => @formatMoney(investment.Value)
+ } + } +
+ } + break; + case "contributions": +
+ Edit Savings Contribution at: /saving

+ + + @((MarkupString)boldUnderline("Contributions:")) +
+ @if (familyData.PlannedSavings > 0) { + Planned savings: @formatMoney(familyData.PlannedSavings)

+ } + + Prioritized Investments:
+ + @if (familyData.EmergencyFund.AmountToSave > 0) { + @formatMoney(familyData.EmergencyFund.AmountToSave) in Emergency Fund
+ @((MarkupString)checkTotal(familyData.EmergencyFund.AmountToSave)) + } + @if (familyData.PersonCount > 0 && familyData.People[0].EmployerPlan.AmountToSaveForMatch > 0) { + @formatMoney(familyData.People[0].EmployerPlan.AmountToSaveForMatch) in @familyData.People[0].PossessiveID 401k (Match: @formatMoney(familyData.People[0].EmployerPlan.MatchAmount))
+ @((MarkupString)checkTotal(familyData.People[0].EmployerPlan.AmountToSaveForMatch)) + } + @if (familyData.PersonCount > 1 && familyData.People[1].EmployerPlan.AmountToSaveForMatch > 0) { + @formatMoney(familyData.People[1].EmployerPlan.AmountToSaveForMatch) in @familyData.People[1].PossessiveID 401k (Match: @formatMoney(familyData.People[1].EmployerPlan.MatchAmount))
+ @((MarkupString)checkTotal(familyData.People[1].EmployerPlan.AmountToSaveForMatch)) + } + @if (familyData.HighDebts > 0) { + @familyData.HighDebts in High Debts
+ @((MarkupString)checkTotal(familyData.HighDebts)) + } + @if (familyData.PersonCount > 0 && (familyData.People[0].EmployerBenefits.Complete || familyData.People[0].EmployerPlan.AnnualSalary == 0 || familyData.People[0].HealthSavingsAccount.EligibleForHSA || familyData.People[0].HealthSavingsAccount.EligibleForHSACatchUpOnly) && familyData.People[0].HealthSavingsAccount.AmountToSave > 0) { + @formatMoney(familyData.People[0].HealthSavingsAccount.AmountToSave) in @familyData.People[0].PossessiveID HSA (Employer: @formatMoney(familyData.People[0].HealthSavingsAccount.EmployerContribution))
+ @((MarkupString)checkTotal(familyData.People[0].HealthSavingsAccount.AmountToSave)) + } + @if (familyData.PersonCount > 1 && (familyData.People[1].EmployerBenefits.Complete || familyData.People[1].EmployerPlan.AnnualSalary == 0 || familyData.People[1].HealthSavingsAccount.EligibleForHSA || familyData.People[1].HealthSavingsAccount.EligibleForHSACatchUpOnly) && familyData.People[1].HealthSavingsAccount.AmountToSave > 0) { + @formatMoney(familyData.People[1].HealthSavingsAccount.AmountToSave) in @familyData.People[1].PossessiveID HSA (Employer: @formatMoney(familyData.People[1].HealthSavingsAccount.EmployerContribution))
+ @((MarkupString)checkTotal(familyData.People[1].HealthSavingsAccount.AmountToSave)) + } + @if (familyData.AdjustedGrossIncome != null) { + if (familyData.PersonCount > 0) { + @((MarkupString)GetRecommendedIRAMarkup(familyData.People[0]))
+ @((MarkupString)checkTotal(GetRecommdedIRAAmount(familyData.People[0]))) + } + if(familyData.PersonCount > 1) { + @((MarkupString)GetRecommendedIRAMarkup(familyData.People[1]))
+ @((MarkupString)checkTotal(GetRecommdedIRAAmount(familyData.People[1]))) + } + } + @if (familyData.PersonCount > 0 && familyData.People[0].EmployerPlan.AmountToSaveForNonMatched > 0) { + @formatMoney(familyData.People[0].EmployerPlan.AmountToSaveForNonMatched) in @familyData.People[0].PossessiveID 401k
+ @((MarkupString)checkTotal(familyData.People[0].EmployerPlan.AmountToSaveForNonMatched)) + } + @if (familyData.PersonCount > 1 && familyData.People[1].EmployerPlan.AmountToSaveForNonMatched > 0) { + @formatMoney(familyData.People[1].EmployerPlan.AmountToSaveForNonMatched) in @familyData.People[1].PossessiveID 401k
+ @((MarkupString)checkTotal(familyData.People[1].EmployerPlan.AmountToSaveForNonMatched)) + } + @if (familyData.PersonCount > 0 && familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit > 0) { + @formatMoney(familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit) in @familyData.People[0].PossessiveID mega backdoor roth
+ @((MarkupString)checkTotal(familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit)) + } + @if (familyData.PersonCount > 1 && familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit > 0) { + @formatMoney(familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit) in @familyData.People[1].PossessiveID mega backdoor roth
+ @((MarkupString)checkTotal(familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit)) + } + @if (familyData.MediumDebts > 0) { + @formatMoney(familyData.MediumDebts) in Medium Debts
+ @((MarkupString)checkTotal(familyData.MediumDebts)) + } + @if (familyData.PlannedSavings > runningTotal) { + @formatMoney(familyData.PlannedSavings-runningTotal) in Taxable
+ @((MarkupString)checkTotal(familyData.PlannedSavings-runningTotal)) + } else { + @formatMoney(0) in Taxable
+ } + @if (familyData.LowDebts > 0) { + @formatMoney(familyData.LowDebts) in Low Debts
+ @((MarkupString)checkTotal(familyData.LowDebts)) + } +
+
+ break; + } +

+ @if(step.description != "") { + Details: @((MarkupString)markupize(step.description)) + } +

+ +
+ } + } + } + } + +@code { + public enum Mode { + normal = 0, + href, + text, + } + public bool pastTotal = false; + public int runningTotal = 0; + + public string checkTotal(int? itemTotal) { + runningTotal += itemTotal ?? 0; + if (!pastTotal) { + pastTotal = familyData.PlannedSavings <= runningTotal; + + if (pastTotal) { + return "
-------
Planned savings " + (familyData.PlannedSavings==runningTotal?"met":"exceeded") + " after " + formatMoney(itemTotal+(familyData.PlannedSavings-runningTotal)) + " towards previous step
-------
"; + } + } + + return ""; + } + + public int? GetRecommdedIRAAmount(Person person) { + switch(person.IRATypeRecommendation) { + case IRAType.DeductibleIRAThenBackdoorRoth: + case IRAType.DeductibleIRA: + return person.IRA.AmountToSave; + case IRAType.Roth: + return person.RothIRA.AmountToSave; + case IRAType.NondeductibleIRAThenBackdoorRoth: + case IRAType.NondeductibleIRA: + return person.IRA.AmountToSave; + default: + return null; + } + } + + public string GetRecommendedIRAMarkup(Person person) { + switch(person.IRATypeRecommendation) { + case IRAType.DeductibleIRAThenBackdoorRoth: + return "" + formatMoney(person.IRA.AmountToSave) + " in " + person.PossessiveID + " IRA (" + formatMoney(person.IRA.DeductionAllowed) + " Deductible), then backdoor roth"; + case IRAType.DeductibleIRA: + return "" + formatMoney(person.IRA.AmountToSave) + " in " + person.PossessiveID + " IRA (" + formatMoney(person.IRA.DeductionAllowed) + " Deductible)"; + case IRAType.Roth: + return "" + formatMoney(person.RothIRA.AmountToSave) + " in " + person.PossessiveID + " Roth IRA"; + case IRAType.NondeductibleIRAThenBackdoorRoth: + return "" + formatMoney(person.IRA.AmountToSave) + " (nondeductible), then backdoor roth"; + case IRAType.NondeductibleIRA: + return "$" + formatMoney(person.IRA.AmountToSave) + " (nondeductible)"; + default: + return "

do nothing

"; + } + } + + public string getPronoun(Person person) + { + if (person?.FamilyData != null && person.FamilyData.PersonCount > 1 && person.Identifier != null && person.Identifier != "None") { + return "(" + person.Identifier + ")"; + } + else + { + return ""; + } + } + + public string formatMoney(int? amount) + { + return String.Format("${0:#,0.##}", amount); + } + public string formatMoney(double? amount) + { + return String.Format("${0:#,0.##}", amount); + } + public string formatMoneyThousands(double? amount) + { + if (amount == null) return ""; + + if (amount >= 1000000) { + return String.Format("${0:#,0.##M}", Math.Round((double)amount / 10000.0)/100.0); + } else if (amount >= 1000) { + return String.Format("${0:#,0.##K}", Math.Round((double)amount / 1000.0)); + } else { + return String.Format("${0:#,0.##}", amount); + } + } + public string formatPercent(double? amount) + { + return String.Format("{0:#,0.#}%", amount); + } + public string formatPercent3(double? amount) + { + return String.Format("{0:#,0.###}%", amount); + } + public string estimatePortfolio(IFamilyData familyData) + { + if (familyData.Value >= 10000000) { + return "8-figures"; + } else if (familyData.Value >= 6666666) { + return "high 7-figures"; + } else if (familyData.Value >= 3333333) { + return "mid 7-figures"; + } else if (familyData.Value >= 1000000) { + return "low 7-figures"; + } else if (familyData.Value >= 666666) { + return "high 6-figures"; + } else if (familyData.Value >= 333333) { + return "mid 6-figures"; + } else if (familyData.Value >= 100000) { + return "low 6-figures"; + } else if (familyData.Value >= 66666) { + return "high 5-figures"; + } else if (familyData.Value >= 33333) { + return "mid 5-figures"; + } else if (familyData.Value >= 10000) { + return "low 5-figures"; + } else if (familyData.Value >= 1000) { + return "4-figures"; + } else if (familyData.Value == 0) { + return "-"; + } else { + return "less than $1,000"; + } + } + + public string bold(string text) { + if (ShowMarkup) { + return "[b]" + text + "[/b]"; + } else { + return "" + text + ""; + } + } + + public string boldUnderline(string text) { + if (ShowMarkup) { + return "[b][u]" + text + "[/u][/b]"; + } else { + return "" + text + ""; + } + } + + public string markupize(string? wikiText) { + if (wikiText == null) return ""; + string returnVal = ""; + char lastChar = 'x'; + var mode = Mode.normal; + string href = ""; + string text = ""; + foreach (var c in wikiText) { + switch (c) { + case '[': + if (lastChar == '[') { + mode = Mode.href; + } + break; + case ']': + if (lastChar == ']') { + if (text == "") { text = href; } + if (href.StartsWith("https://bogle.tools/")) + { + returnVal += "" + text + ""; + } + else if (href.StartsWith("https://")) + { + returnVal += "" + text + ""; + } + else + { + returnVal += "" + text + ""; + } + href = ""; + text = ""; + mode = Mode.normal; + } + break; + case '|': + if (mode == Mode.href) { + mode = Mode.text; + } + break; + } + + switch (mode) { + case Mode.href: + switch (c) { + case '[': + case ']': + break; + default: + href += c; + break; + } + break; + case Mode.text: + switch (c) { + case '[': + case ']': + case '|': + break; + default: + text += c; + break; + } + break; + default: + switch (c) { + case '[': + case ']': + break; + case '\n': + returnVal += "
"; + break; + default: + returnVal += c; + break; + } + break; + } + + lastChar = c; + } + returnVal += "
"; + + return returnVal; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (focusTicker) + { + await tickerN.FocusAsync(); + focusTicker = false; + } else if (focusAccount) { + await accountN.FocusAsync(); + focusAccount = false; + } + } + + [Parameter] + public string? stepPath { get; set; } + + [Parameter] + public string? TopicValue { get; set; } + + public bool ShowMarkup { get; set; } + public string? currentAccountIndex { get; set; } + private Step[]? steps; + public int accountIndex = 0; + public int investmentIndex = 0; + private const string folderName = "/portfolio-review/"; + private bool focusTicker = false; + private bool focusAccount = false; + private bool showPortfolioAnalysis = true; + + private FamilyData familyData { + get { + return appData.FamilyData; + } + set { + appData.FamilyData = value; + } + } + + protected override async Task OnInitializedAsync() + { + steps = await Http.GetFromJsonAsync("data/portfolio-review-steps.json"); + + if (!string.IsNullOrEmpty(TopicValue)) { + await LoadPortfolioForTopic(TopicValue); + } + } + + private ElementReference tickerN; + private ElementReference accountN; + private bool showImport = false; + private bool showError = false; + + private void ShowImport() { + showImport = !showImport; + if (!showImport) { + showError = false; + } + } + + private async Task UpdatePrice(MouseEventArgs e, Investment investment) { + if (!string.IsNullOrEmpty(familyData.EODHistoricalDataApiKey)) { + var quoteDataJson = await Http.GetStreamAsync($"https://api.bogle.tools/api/getquotes?ticker={investment.Ticker}&apikey={familyData.EODHistoricalDataApiKey}"); + var quoteData = await JsonSerializer.DeserializeAsync(quoteDataJson); + if (quoteData?.Close != null) { + investment.Price = quoteData.Close; + investment.Value = investment.Price * investment.Shares; + } + } + } + + private ImportResult? ImportResult = null; + + private async Task OnDataFilesImport(InputFileChangeEventArgs e) + { + var files = e.GetMultipleFiles(); + ImportResult = await Importer.ImportDataFiles(files, Funds, familyData.Accounts); + foreach (var account in ImportResult.ImportedAccounts) + { + account.Import = true; + } + + Console.WriteLine($"{ImportResult.DataFilesImported} files imported with {ImportResult.ImportedAccounts.Count} accounts"); + showImport = false; + + Navigation.NavigateTo("/portfolio-review/reload"); // WORKAROUND + } + + private void finishImport() + { + if (ImportResult == null) { return; } + + foreach (var updatedAccount in ImportResult.UpdatedAccounts) + { + if (updatedAccount.Import && updatedAccount.ReplaceAccount != null) + { + updatedAccount.ReplaceAccount.Investments.Clear(); + updatedAccount.ReplaceAccount.Investments.AddRange(updatedAccount.Investments); + } + } + + foreach (var newAccount in ImportResult.NewAccounts) + { + if (newAccount.Import) + { + familyData.Accounts.Add(newAccount); + } + } + + ImportResult = null; + } + + private void cancelImport() + { + ImportResult = null; + } + + private void addInvestment(MouseEventArgs e, int accountIndex) + { + var newInvestment = new Investment() { funds = Funds }; + familyData.Accounts[accountIndex].Investments.Add(newInvestment); + focusTicker = true; + } + + void deleteInvestment(MouseEventArgs e, int accountIndex, int investmentIndex) + { + familyData.Accounts[accountIndex].Investments.RemoveAt(investmentIndex); + Navigation.NavigateTo("/portfolio-review/reload"); + familyData.UpdatePercentages(); + } + void addAvailableFund(MouseEventArgs e, Account account) + { + var newFund = new Investment() { funds = Funds }; + account.AvailableFunds.Add(newFund); + focusTicker = true; + } + void deleteAvailableFund(MouseEventArgs e, Account account, int investmentIndex) + { + account.AvailableFunds.RemoveAt(investmentIndex); + } + void addAccount() + { + var newAccount = new Account(); + var newInvestment = new Investment() { funds = Funds }; + newAccount.Investments.Add(newInvestment); + familyData?.Accounts.Add(newAccount); + focusAccount = true; + } + void deleteAccount(MouseEventArgs e, int accountIndex) + { + familyData.Accounts.RemoveAt(accountIndex); + Navigation.NavigateTo("/portfolio-review/reload"); + familyData.UpdatePercentages(); + } + + void editAccount(MouseEventArgs e, int index) + { + familyData.Accounts[index].Edit = true; + } + private async Task LoadPortfolioForTopic(string topicStr) { + var options = new JsonSerializerOptions() + { + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + var topicJsonUri = "https://raw.githubusercontent.com/bogle-tools/financial-variables/main/data/portfolios/" + topicStr + ".json"; + var stream = await Http.GetStreamAsync(topicJsonUri); + familyData = await FamilyData.LoadFromJsonStream(familyData, stream, options); + } + + private async Task CopyTextToClipboard() + { + var text = await JS.InvokeAsync("getTableInnerText"); + text = text.Replace("\nāœļø\t",""); + text = text.Replace("āœļø\t",""); + text = text.Replace("Show Portfolio Analysis\n", ""); + await JS.InvokeVoidAsync("navigator.clipboard.writeText", text); + } +} diff --git a/Pages/Portfolio.razor b/Pages/Portfolio.razor index 697f73e..1ca0230 100644 --- a/Pages/Portfolio.razor +++ b/Pages/Portfolio.razor @@ -67,9 +67,11 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too
-

Organizing information about you and your portfolio will help you optimize your finances.

+

Organizing information about your portfolio will help you optimize your finances.
+ Press Save button to save your data. Enable updates to stock/fund prices.

+
-

Press edit (āœļø) to complete each step:

+

Press each āœļø button to enter info:

@foreach (var step in steps) { @@ -83,6 +85,9 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too @switch (step.step) { case "age": break; - case "emergency-fund": - - break; - case "debts": - - break; - case "tax-status": - - break; case "asset-allocation": - break; - case "available-funds": - - break; + break; default: - break; } @@ -317,10 +182,7 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too
+ @((MarkupString)bold("Tax Filing Status:")) + @if (familyData.TaxFilingStatus != TaxFilingStatus.ChoiceNeeded) {  @familyData.TaxFilingStatus } +
@((MarkupString)bold("Age:")) @if(familyData.PersonCount > 0 && familyData.People[0].Age != null) {  @familyData.People[0].Age @getPronoun(familyData.People[0]) @@ -93,46 +98,6 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too

- @((MarkupString)bold("Emergency funds:")) - @if (familyData.EmergencyFund.CurrentMonths != null) { -  @familyData.EmergencyFund.CurrentMonthsString months - } else if (familyData.EmergencyFund.ShowDollars) { -  @formatMoney(familyData.EmergencyFund.CurrentBalance) - } -

-
- @((MarkupString)bold("Debt:")) - @if (familyData.Debts.Count == 0 && familyData.DebtsComplete) {  no debts } - else if (familyData.Debts.Count >= 1) { -
- @foreach (var debt in familyData.Debts) - { -   @debt.Name: @formatMoney(debt.Total) @@ @(debt.Rate == null ? "unknown" : debt.Rate)%
- } - } -
-
- - @((MarkupString)bold("Tax Filing Status:")) - @if (familyData.TaxFilingStatus != TaxFilingStatus.ChoiceNeeded) {  @familyData.TaxFilingStatus } -

- @((MarkupString)bold("Tax Rate:")) - @if (familyData.FederalMarginalTaxBracket != null) {  @familyData.FederalMarginalTaxBracket Federal, @familyData.StateMarginalTaxBracket% State } -

- @((MarkupString)bold("State of Residence:")) - @if (familyData.StateMarginalTaxBracket != null) {  @familyData.StateOfResidence } -
-

-
@((MarkupString)bold("Desired Asset allocation:")) @@ -183,133 +148,33 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too
} - @((MarkupString)boldUnderline("Current Retirement Assets:"))
+ @((MarkupString)boldUnderline("Current Retirement Assets:"))  +

- @foreach (var account in familyData.Accounts) { -
- @((MarkupString)bold((account.Identifier != null ? ""+account.Identifier+" " : "") + account.AccountType + (account.Custodian != null ? " at " + account.Custodian : "") + " (" + formatPercent(account.Percentage) + ")" )) -
- @foreach (var investment in account.Investments) { -     @formatPercent(investment.Percentage) @investment.Name (@investment.Ticker) (@(investment.ExpenseRatio!=null?investment.ExpenseRatio:"??")%)
- } - } -
-
- @((MarkupString)boldUnderline("Available funds:"))
- @foreach (var account in familyData.Accounts) { - @if (account.AvailableFunds.Count > 0) { -
- @((MarkupString)bold("Funds available in " + account.FullName))
-
- @foreach (var investment in account.AvailableFunds) { -     @investment.Name (@investment.Ticker) (@investment.ExpenseRatio%)
- } + + + + + + + + + @foreach (var account in familyData.Accounts) { + @foreach (var investment in account.Investments) { + + + + + + + } } -
- - break; - case "additional-background": - @((MarkupString)boldUnderline("Additional Background:"))
- @familyData.AdditionalBackground -
- break; - case "questions": - - break; - case "contributions": - runningTotal = 0; - pastTotal = false; - -
AccountTicker$ / shareSharesValue
@((MarkupString)((account.Identifier != null ? ""+account.Identifier+" " : "") + account.AccountType + (account.Custodian != null ? " at " + account.Custodian : "")))@investment.Ticker@investment.Price@investment.Shares@investment.Value
- @((MarkupString)boldUnderline("Questions:"))
- @for (int i = 0; i < familyData.Questions.Count; i++) { - var question = familyData.Questions[i]; - @(i+1 + ".") @question -

- } -
- - @((MarkupString)boldUnderline("Contributions:")) -
- @if (familyData.PlannedSavings > 0) { - Planned savings: @formatMoney(familyData.PlannedSavings)

- } - - Prioritized Investments:
- - @if (familyData.EmergencyFund.AmountToSave > 0) { - @formatMoney(familyData.EmergencyFund.AmountToSave) in Emergency Fund
- @((MarkupString)checkTotal(familyData.EmergencyFund.AmountToSave)) - } - @if (familyData.PersonCount > 0 && familyData.People[0].EmployerPlan.AmountToSaveForMatch > 0) { - @formatMoney(familyData.People[0].EmployerPlan.AmountToSaveForMatch) in @familyData.People[0].PossessiveID 401k (Match: @formatMoney(familyData.People[0].EmployerPlan.MatchAmount))
- @((MarkupString)checkTotal(familyData.People[0].EmployerPlan.AmountToSaveForMatch)) - } - @if (familyData.PersonCount > 1 && familyData.People[1].EmployerPlan.AmountToSaveForMatch > 0) { - @formatMoney(familyData.People[1].EmployerPlan.AmountToSaveForMatch) in @familyData.People[1].PossessiveID 401k (Match: @formatMoney(familyData.People[1].EmployerPlan.MatchAmount))
- @((MarkupString)checkTotal(familyData.People[1].EmployerPlan.AmountToSaveForMatch)) - } - @if (familyData.HighDebts > 0) { - @familyData.HighDebts in High Debts
- @((MarkupString)checkTotal(familyData.HighDebts)) - } - @if (familyData.PersonCount > 0 && (familyData.People[0].EmployerBenefits.Complete || familyData.People[0].EmployerPlan.AnnualSalary == 0 || familyData.People[0].HealthSavingsAccount.EligibleForHSA || familyData.People[0].HealthSavingsAccount.EligibleForHSACatchUpOnly) && familyData.People[0].HealthSavingsAccount.AmountToSave > 0) { - @formatMoney(familyData.People[0].HealthSavingsAccount.AmountToSave) in @familyData.People[0].PossessiveID HSA (Employer: @formatMoney(familyData.People[0].HealthSavingsAccount.EmployerContribution))
- @((MarkupString)checkTotal(familyData.People[0].HealthSavingsAccount.AmountToSave)) - } - @if (familyData.PersonCount > 1 && (familyData.People[1].EmployerBenefits.Complete || familyData.People[1].EmployerPlan.AnnualSalary == 0 || familyData.People[1].HealthSavingsAccount.EligibleForHSA || familyData.People[1].HealthSavingsAccount.EligibleForHSACatchUpOnly) && familyData.People[1].HealthSavingsAccount.AmountToSave > 0) { - @formatMoney(familyData.People[1].HealthSavingsAccount.AmountToSave) in @familyData.People[1].PossessiveID HSA (Employer: @formatMoney(familyData.People[1].HealthSavingsAccount.EmployerContribution))
- @((MarkupString)checkTotal(familyData.People[1].HealthSavingsAccount.AmountToSave)) - } - @if (familyData.AdjustedGrossIncome != null) { - if (familyData.PersonCount > 0) { - @((MarkupString)GetRecommendedIRAMarkup(familyData.People[0]))
- @((MarkupString)checkTotal(GetRecommdedIRAAmount(familyData.People[0]))) - } - if(familyData.PersonCount > 1) { - @((MarkupString)GetRecommendedIRAMarkup(familyData.People[1]))
- @((MarkupString)checkTotal(GetRecommdedIRAAmount(familyData.People[1]))) - } - } - @if (familyData.PersonCount > 0 && familyData.People[0].EmployerPlan.AmountToSaveForNonMatched > 0) { - @formatMoney(familyData.People[0].EmployerPlan.AmountToSaveForNonMatched) in @familyData.People[0].PossessiveID 401k
- @((MarkupString)checkTotal(familyData.People[0].EmployerPlan.AmountToSaveForNonMatched)) - } - @if (familyData.PersonCount > 1 && familyData.People[1].EmployerPlan.AmountToSaveForNonMatched > 0) { - @formatMoney(familyData.People[1].EmployerPlan.AmountToSaveForNonMatched) in @familyData.People[1].PossessiveID 401k
- @((MarkupString)checkTotal(familyData.People[1].EmployerPlan.AmountToSaveForNonMatched)) - } - @if (familyData.PersonCount > 0 && familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit > 0) { - @formatMoney(familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit) in @familyData.People[0].PossessiveID mega backdoor roth
- @((MarkupString)checkTotal(familyData.People[0].EmployerBenefits.MegaBackdoorRoth.ContributionLimit)) - } - @if (familyData.PersonCount > 1 && familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit > 0) { - @formatMoney(familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit) in @familyData.People[1].PossessiveID mega backdoor roth
- @((MarkupString)checkTotal(familyData.People[1].EmployerBenefits.MegaBackdoorRoth.ContributionLimit)) - } - @if (familyData.MediumDebts > 0) { - @formatMoney(familyData.MediumDebts) in Medium Debts
- @((MarkupString)checkTotal(familyData.MediumDebts)) - } - @if (familyData.PlannedSavings > runningTotal) { - @formatMoney(familyData.PlannedSavings-runningTotal) in Taxable
- @((MarkupString)checkTotal(familyData.PlannedSavings-runningTotal)) - } else { - @formatMoney(0) in Taxable
- } - @if (familyData.LowDebts > 0) { - @formatMoney(familyData.LowDebts) in Low Debts
- @((MarkupString)checkTotal(familyData.LowDebts)) - } +

NYI

-

Bogleheads.org forum enables you to Ask Portfolio Questions ā†—ļø. This tool makes it easy to copy the correct format for your post:

-
-
-
+

See the Portfolio Review page for help authoring questions to post. Bogleheads.org forum is a great place to Ask Portfolio Questions ā†—ļø.


@@ -489,7 +351,7 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too
break; case "age": -

Prerequisities:

+

Worksheet:



-

Worksheet:

@for (int i = 0; i < familyData.PersonCount; i++) { var person = familyData.People[i]; int personIndex = i + 1; @@ -600,19 +461,6 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too break; case "retirement-assets": @if(ImportResult == null) { -

Prerequisites:

- - -
-
- Current Retirement Assets:
accountIndex = 0; @@ -664,19 +512,11 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too
- Balance šŸŸ° $ -
-
- shares: - price: + $ + āœ–ļø shares + šŸŸ° $
-
- -  ( - - %) -
}
@@ -1115,7 +955,7 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too protected override async Task OnInitializedAsync() { - steps = await Http.GetFromJsonAsync("data/portfolio-steps.json"); + steps = await Http.GetFromJsonAsync("data/portfolio-assets-steps.json"); if (!string.IsNullOrEmpty(TopicValue)) { await LoadPortfolioForTopic(TopicValue); @@ -1134,17 +974,39 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too } } - private async Task UpdatePrice(MouseEventArgs e, Investment investment) { + private async Task UpdateInvestmentPrice(Investment investment) + { + var quoteDataJson = await Http.GetStreamAsync($"https://api.bogle.tools/api/getquotes?ticker={investment.Ticker}&apikey={familyData.EODHistoricalDataApiKey}"); + var quoteData = await JsonSerializer.DeserializeAsync(quoteDataJson); + if (quoteData?.Close != null) { + investment.Price = quoteData.Close; + investment.Value = investment.Price * investment.Shares; + } + } + + private async Task UpdateInvestmentsPrice(Dictionary> quotes) + { if (!string.IsNullOrEmpty(familyData.EODHistoricalDataApiKey)) { - var quoteDataJson = await Http.GetStreamAsync($"https://api.bogle.tools/api/getquotes?ticker={investment.Ticker}&apikey={familyData.EODHistoricalDataApiKey}"); - var quoteData = await JsonSerializer.DeserializeAsync(quoteDataJson); - if (quoteData?.Close != null) { - investment.Price = quoteData.Close; - investment.Value = investment.Price * investment.Shares; + foreach (var quote in quotes) + { + var quoteDataJson = await Http.GetStreamAsync($"https://api.bogle.tools/api/getquotes?ticker={quote.Key}&apikey={familyData.EODHistoricalDataApiKey}"); + var quoteData = await JsonSerializer.DeserializeAsync(quoteDataJson); + if (quoteData?.Close != null) { + foreach (var investment in quote.Value) { + investment.Price = quoteData.Close; + investment.Value = investment.Price * investment.Shares; + } + } } } } + private async Task UpdatePrice(MouseEventArgs e, Investment investment) { + if (!string.IsNullOrEmpty(familyData.EODHistoricalDataApiKey)) { + UpdateInvestmentPrice(investment); + } + } + private ImportResult? ImportResult = null; private async Task OnDataFilesImport(InputFileChangeEventArgs e) @@ -1254,4 +1116,27 @@ Portfolio@(stepPath==null?": review":": "+stepPath.Replace('-',' ')) - bogle.too text = text.Replace("Show Portfolio Analysis\n", ""); await JS.InvokeVoidAsync("navigator.clipboard.writeText", text); } + + private async Task refreshPrices() { + Dictionary> quotes = new(); + foreach (var account in familyData.Accounts) + { + foreach (var investment in account.Investments) + { + if (investment.AssetType != AssetType.Cash && investment.Ticker != "IBOND" && !string.IsNullOrEmpty(investment.Ticker)) { + if (!quotes.ContainsKey(investment.Ticker)) + { + quotes.Add(investment.Ticker, new List () { investment }); + } + else + { + var investments = quotes[investment.Ticker]; + investments.Add(investment); + } + } + } + } + + UpdateInvestmentsPrice(quotes); + } } diff --git a/Pages/Updated-quotes.razor b/Pages/Updated-quotes.razor new file mode 100644 index 0000000..91627c4 --- /dev/null +++ b/Pages/Updated-quotes.razor @@ -0,0 +1,30 @@ +ļ»æ@page "/updated-quotes" +@inject IAppData appData + + +Updated Quotes + +

Updated Stock/Fund Quotes

+ +The portfolio tool can have prices of stocks/funds updated, but you need a free account at EODHistoricalData.com

+ +Steps: +
    +
  1. Register for free account for updated quotes at EODHistoricalData.com. They will email a free API key. You'll get 20 free quotes a day. (And can buy another 100,000 quotes for $5 from that site, which is 200 quotes per penny.)
  2. +
  3. Enter key into this tool. Then press Save button at top-right of page.
  4. + + + +
+

+ +@code{ + private FamilyData familyData { + get { + return appData.FamilyData; + } + set { + appData.FamilyData = value; + } + } +} \ No newline at end of file diff --git a/Shared/Models/FamilyData/Importer.cs b/Shared/Models/FamilyData/Importer.cs index 3b68833..83ce812 100644 --- a/Shared/Models/FamilyData/Importer.cs +++ b/Shared/Models/FamilyData/Importer.cs @@ -175,6 +175,7 @@ public static async Task> ImportCSV(string[] lines, IList fu string? investmentName; double value; + double? price = null; double? shares = null; double? costBasis = null; @@ -193,6 +194,7 @@ public static async Task> ImportCSV(string[] lines, IList fu { investmentName = chunks[3]; value = ParseDouble(chunks[7], allowCurrency:true); + price = ParseDouble(chunks[5], allowCurrency:true); shares = ParseDouble(chunks[4]); costBasis = ParseDouble(chunks[13], allowCurrency:true); } @@ -209,7 +211,7 @@ public static async Task> ImportCSV(string[] lines, IList fu lastAccountNumber = accountNumber; } - Investment newInvestment = new () { funds = funds, Ticker = symbol, Name = investmentName, Value = value, Shares = shares, CostBasis = costBasis }; + Investment newInvestment = new () { funds = funds, Ticker = symbol, Name = investmentName, Price = price, Value = value, Shares = shares, CostBasis = costBasis }; newAccount?.Investments.Add(newInvestment); } @@ -235,6 +237,7 @@ public static async Task> ImportCSV(string[] lines, IList fu var symbol = chunks[2]; var investmentName = chunks[1]; double shares = ParseDouble(chunks[3]); + double price = ParseDouble(chunks[4], allowCurrency:true); double value = ParseDouble(chunks[5], allowCurrency:true); // costBasis not available in CSV @@ -247,7 +250,7 @@ public static async Task> ImportCSV(string[] lines, IList fu importedAccounts.Add(newAccount); } - Investment newInvestment = new () { funds = funds, Ticker = symbol, Name = investmentName, Value = value, Shares = shares }; + Investment newInvestment = new () { funds = funds, Ticker = symbol, Name = investmentName, Price = price, Value = value, Shares = shares }; newAccount?.Investments.Add(newInvestment); } diff --git a/Shared/NavMenu.razor b/Shared/NavMenu.razor index 7732390..17535bc 100644 --- a/Shared/NavMenu.razor +++ b/Shared/NavMenu.razor @@ -18,6 +18,11 @@ šŸ“ˆ Portfolio +