diff --git a/package-lock.json b/package-lock.json
index 8928217..f6632b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-hot-toast": "^2.4.1",
"react-router-dom": "^7.1.0",
"recharts": "^2.15.0",
"use-debounce": "^10.0.4"
@@ -2861,6 +2862,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/goober": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+ "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -3735,6 +3745,22 @@
"react": "^19.0.0"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+ "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "goober": "^2.1.10"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/package.json b/package.json
index e1905e7..c35ea0b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "investment-portfolio-tracker",
"private": true,
- "version": "1.0.0",
+ "version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,6 +16,7 @@
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-hot-toast": "^2.4.1",
"react-router-dom": "^7.1.0",
"recharts": "^2.15.0",
"use-debounce": "^10.0.4"
diff --git a/src/App.tsx b/src/App.tsx
index 1886c6f..2c124ce 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,5 @@
import { lazy, Suspense, useState } from "react";
+import { Toaster } from "react-hot-toast";
import { AppShell } from "./components/Landing/AppShell";
import { LoadingPlaceholder } from "./components/utils/LoadingPlaceholder";
@@ -19,6 +20,7 @@ export default function App() {
/>
+
);
}
diff --git a/src/components/InvestmentForm.tsx b/src/components/InvestmentForm.tsx
index ebcfb84..d3c503a 100644
--- a/src/components/InvestmentForm.tsx
+++ b/src/components/InvestmentForm.tsx
@@ -1,5 +1,6 @@
import { Loader2 } from "lucide-react";
import React, { useState } from "react";
+import toast from "react-hot-toast";
import { usePortfolioSelector } from "../hooks/usePortfolio";
import { generatePeriodicInvestments } from "../utils/calculations/assetValue";
@@ -81,9 +82,16 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+ e.stopPropagation();
+
+ console.log("submitting")
+ console.time('generatePeriodicInvestments');
+ console.timeLog('generatePeriodicInvestments', "1");
+
setIsSubmitting(true);
setTimeout(async () => {
+ console.log("timeout")
try {
if (type === "single") {
const investment = {
@@ -94,6 +102,7 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
date
};
addInvestment(assetId, investment);
+ toast.success('Investment added successfully');
} else {
const periodicSettings = {
startDate: date,
@@ -108,18 +117,23 @@ const InvestmentForm = ({ assetId, clearSelectedAsset }: { assetId: string, clea
},
} : undefined),
};
+ console.timeLog('generatePeriodicInvestments', "2");
const investments = generatePeriodicInvestments(
periodicSettings,
dateRange.endDate,
assetId
);
+ console.timeLog('generatePeriodicInvestments', "3");
+ addInvestment(assetId, investments);
- for (const investment of investments) {
- addInvestment(assetId, investment);
- }
+ toast.success('Periodic investment plan created successfully');
}
+ } catch (error:any) {
+ toast.error('Failed to add investment. Please try again.' + String(error?.message || error));
} finally {
+ console.timeLog('generatePeriodicInvestments', "4");
+ console.timeEnd('generatePeriodicInvestments');
setIsSubmitting(false);
setAmount('');
clearSelectedAsset();
diff --git a/src/components/Modals/AddAssetModal.tsx b/src/components/Modals/AddAssetModal.tsx
index 6b2eaa3..53140c7 100644
--- a/src/components/Modals/AddAssetModal.tsx
+++ b/src/components/Modals/AddAssetModal.tsx
@@ -1,5 +1,6 @@
import { Loader2, Search, X } from "lucide-react";
import { useState } from "react";
+import toast from "react-hot-toast";
import { useDebouncedCallback } from "use-debounce";
import { usePortfolioSelector } from "../../hooks/usePortfolio";
@@ -43,17 +44,23 @@ export default function AddAssetModal({ onClose }: { onClose: () => void }) {
dateRange.endDate
);
+ if (historicalData.length === 0) {
+ toast.error(`No historical data available for ${asset.name}`);
+ return;
+ }
+
const assetWithHistory = {
...asset,
- // override name with the fetched long Name if available
name: longName || asset.name,
historicalData,
};
addAsset(assetWithHistory);
+ toast.success(`Successfully added ${assetWithHistory.name}`);
onClose();
} catch (error) {
console.error('Error fetching historical data:', error);
+ toast.error(`Failed to add ${asset.name}. Please try again.`);
} finally {
setLoading(null);
}
diff --git a/src/components/Modals/EditInvestmentModal.tsx b/src/components/Modals/EditInvestmentModal.tsx
index 1727a6b..a871f99 100644
--- a/src/components/Modals/EditInvestmentModal.tsx
+++ b/src/components/Modals/EditInvestmentModal.tsx
@@ -1,5 +1,6 @@
import { X } from "lucide-react";
import { useState } from "react";
+import toast from "react-hot-toast";
import { usePortfolioSelector } from "../../hooks/usePortfolio";
import { Investment } from "../../types";
@@ -18,11 +19,16 @@ export const EditInvestmentModal = ({ investment, assetId, onClose }: EditInvest
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
- updateInvestment(assetId, investment.id, {
- ...investment,
- amount: parseFloat(amount),
- });
- onClose();
+ try {
+ updateInvestment(assetId, investment.id, {
+ ...investment,
+ amount: parseFloat(amount),
+ });
+ toast.success('Investment updated successfully');
+ onClose();
+ } catch (error:any) {
+ toast.error('Failed to update investment' + String(error?.message || error));
+ }
};
return (
diff --git a/src/components/Modals/EditSavingsPlanModal.tsx b/src/components/Modals/EditSavingsPlanModal.tsx
new file mode 100644
index 0000000..3816f41
--- /dev/null
+++ b/src/components/Modals/EditSavingsPlanModal.tsx
@@ -0,0 +1,236 @@
+import { Loader2, X } from "lucide-react";
+import { useState } from "react";
+import toast from "react-hot-toast";
+
+import { usePortfolioSelector } from "../../hooks/usePortfolio";
+import { generatePeriodicInvestments } from "../../utils/calculations/assetValue";
+
+interface EditSavingsPlanModalProps {
+ assetId: string;
+ groupId: string;
+ amount: number;
+ dayOfMonth: number;
+ interval: number;
+ dynamic?: {
+ type: 'percentage' | 'fixed';
+ value: number;
+ yearInterval: number;
+ };
+ onClose: () => void;
+}
+
+export const EditSavingsPlanModal = ({
+ assetId,
+ groupId,
+ amount: initialAmount,
+ dayOfMonth: initialDayOfMonth,
+ interval: initialInterval,
+ dynamic: initialDynamic,
+ onClose
+}: EditSavingsPlanModalProps) => {
+ const [amount, setAmount] = useState(initialAmount.toString());
+ const [dayOfMonth, setDayOfMonth] = useState(initialDayOfMonth.toString());
+ const [interval, setInterval] = useState(initialInterval.toString());
+ const [isDynamic, setIsDynamic] = useState(!!initialDynamic);
+ const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>(initialDynamic?.type || 'percentage');
+ const [dynamicValue, setDynamicValue] = useState(initialDynamic?.value.toString() || '');
+ const [yearInterval, setYearInterval] = useState(initialDynamic?.yearInterval.toString() || '1');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { dateRange, addInvestment, removeInvestment, assets } = usePortfolioSelector((state) => ({
+ dateRange: state.dateRange,
+ addInvestment: state.addInvestment,
+ removeInvestment: state.removeInvestment,
+ assets: state.assets,
+ }));
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsSubmitting(true);
+
+ setTimeout(async () => {
+ try {
+ // First, remove all existing investments for this savings plan
+ const asset = assets.find(a => a.id === assetId)!;
+ const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
+ const startDate = investments[0].date!; // Keep original start date
+
+ investments.forEach(inv => {
+ removeInvestment(assetId, inv.id);
+ });
+
+ // Generate and add new investments
+ const periodicSettings = {
+ startDate,
+ dayOfMonth: parseInt(dayOfMonth),
+ interval: parseInt(interval),
+ amount: parseFloat(amount),
+ ...(isDynamic ? {
+ dynamic: {
+ type: dynamicType,
+ value: parseFloat(dynamicValue),
+ yearInterval: parseInt(yearInterval),
+ },
+ } : undefined),
+ };
+
+ const newInvestments = generatePeriodicInvestments(
+ periodicSettings,
+ dateRange.endDate,
+ assetId
+ );
+
+ addInvestment(assetId, newInvestments);
+ toast.success('Savings plan updated successfully');
+ onClose();
+ } catch (error:any) {
+ toast.error('Failed to update savings plan: ' + String(error?.message || error));
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, 10);
+ };
+
+ return (
+
+
+
+
Edit Savings Plan
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/PortfolioTable.tsx b/src/components/PortfolioTable.tsx
index e2e312a..9d01de6 100644
--- a/src/components/PortfolioTable.tsx
+++ b/src/components/PortfolioTable.tsx
@@ -3,12 +3,14 @@ import {
Download, FileDown, LineChart, Loader2, Pencil, RefreshCw, ShoppingBag, Trash2
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
+import toast from "react-hot-toast";
import { usePortfolioSelector } from "../hooks/usePortfolio";
import { Investment } from "../types";
import { calculateInvestmentPerformance } from "../utils/calculations/performance";
import { downloadTableAsCSV, generatePortfolioPDF } from "../utils/export";
import { EditInvestmentModal } from "./Modals/EditInvestmentModal";
+import { EditSavingsPlanModal } from "./Modals/EditSavingsPlanModal";
import { FutureProjectionModal } from "./Modals/FutureProjectionModal";
import { Tooltip } from "./utils/ToolTip";
@@ -23,8 +25,20 @@ export default function PortfolioTable() {
investment: Investment;
assetId: string;
} | null>(null);
- const [showSavingsPlans, setShowSavingsPlans] = useState(true);
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
+ const [isUpdatingSavingsPlan, setIsUpdatingSavingsPlan] = useState(false);
+ const [editingSavingsPlan, setEditingSavingsPlan] = useState<{
+ assetId: string;
+ groupId: string;
+ amount: number;
+ dayOfMonth: number;
+ interval: number;
+ dynamic?: {
+ type: 'percentage' | 'fixed';
+ value: number;
+ yearInterval: number;
+ };
+ } | null>(null);
const performance = useMemo(() => calculateInvestmentPerformance(assets), [assets]);
@@ -34,13 +48,23 @@ export default function PortfolioTable() {
const handleDelete = useCallback((investmentId: string, assetId: string) => {
if (window.confirm("Are you sure you want to delete this investment?")) {
- removeInvestment(assetId, investmentId);
+ try {
+ removeInvestment(assetId, investmentId);
+ toast.success('Investment deleted successfully');
+ } catch (error:any) {
+ toast.error('Failed to delete investment' + String(error?.message || error));
+ }
}
}, [removeInvestment]);
const handleClearAll = useCallback(() => {
if (window.confirm("Are you sure you want to clear all investments?")) {
- clearInvestments();
+ try {
+ clearInvestments();
+ toast.success('All investments cleared successfully');
+ } catch (error:any) {
+ toast.error('Failed to clear investments' + String(error?.message || error));
+ }
}
}, [clearInvestments]);
@@ -113,11 +137,39 @@ export default function PortfolioTable() {
savingsPlansPerformance,
performance.summary.performancePerAnnoPerformance
);
+ toast.success('PDF generated successfully');
+ } catch (error:any) {
+ toast.error('Failed to generate PDF' + String(error?.message || error));
} finally {
setIsGeneratingPDF(false);
}
};
+ const handleDeleteSavingsPlan = useCallback((assetId: string, groupId: string) => {
+ if (window.confirm("Are you sure you want to delete this savings plan? All related investments will be removed.")) {
+ try {
+ setIsUpdatingSavingsPlan(true);
+ setTimeout(() => {
+ try {
+ const asset = assets.find(a => a.id === assetId);
+ if (!asset) throw new Error('Asset not found');
+ const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
+ investments.forEach(inv => {
+ removeInvestment(assetId, inv.id);
+ });
+ toast.success('Savings plan deleted successfully');
+ } catch (error:any) {
+ toast.error('Failed to delete savings plan: ' + String(error?.message || error));
+ } finally {
+ setIsUpdatingSavingsPlan(false);
+ }
+ }, 10);
+ } catch (error:any) {
+ toast.error('Failed to delete savings plan: ' + String(error?.message || error));
+ }
+ }
+ }, [assets, removeInvestment]);
+
return (
@@ -140,15 +192,6 @@ export default function PortfolioTable() {
Future Projection
-
-
- {!isSavingsPlanOverviewDisabled && showSavingsPlans && savingsPlansPerformance.length > 0 && (
+ {!isSavingsPlanOverviewDisabled && savingsPlansPerformance.length > 0 && (
Savings Plans Performance
@@ -184,20 +227,57 @@ export default function PortfolioTable() {
Total Invested |
Current Value |
Performance (%) |
-
Performance (p.a.) |
+
Performance (p.a.) |
+
Actions |
- {savingsPlansPerformance.map((plan) => (
-
- {plan.assetName} |
- {plan.amount} |
- €{plan.totalInvested.toFixed(2)} |
- €{plan.currentValue.toFixed(2)} |
- {plan.performancePercentage.toFixed(2)}% |
- {plan.performancePerAnnoPerformance.toFixed(2)}% |
-
- ))}
+ {savingsPlansPerformance.map((plan) => {
+ const asset = assets.find(a => a.name === plan.assetName)!;
+ const firstInvestment = asset.investments.find(inv => inv.type === 'periodic')!;
+ const groupId = firstInvestment.periodicGroupId!;
+
+ return (
+
+ {plan.assetName} |
+ {plan.amount} |
+ €{plan.totalInvested.toFixed(2)} |
+ €{plan.currentValue.toFixed(2)} |
+ {plan.performancePercentage.toFixed(2)}% |
+ {plan.performancePerAnnoPerformance.toFixed(2)}% |
+
+
+
+
+
+ |
+
+ );
+ })}
@@ -276,10 +356,10 @@ export default function PortfolioTable() {
{performance.summary.performancePercentage.toFixed(2)}%
- - (avg. acc. {averagePerformance}%)
- - (avg. p.a. {performance.summary.performancePerAnnoPerformance.toFixed(2)}%)
- - (best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
- - (worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
+ - (avg. acc. {averagePerformance}%)
+ - (avg. p.a. {performance.summary.performancePerAnnoPerformance.toFixed(2)}%)
+ - (best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
+ - (worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
|
|
@@ -366,6 +446,12 @@ export default function PortfolioTable() {
onClose={() => setShowProjection(false)}
/>
)}
+ {editingSavingsPlan && (
+
setEditingSavingsPlan(null)}
+ />
+ )}
);
};
diff --git a/src/providers/PortfolioProvider.tsx b/src/providers/PortfolioProvider.tsx
index db88119..9bb69a3 100644
--- a/src/providers/PortfolioProvider.tsx
+++ b/src/providers/PortfolioProvider.tsx
@@ -16,7 +16,7 @@ type PortfolioAction =
| { type: 'ADD_ASSET'; payload: Asset }
| { type: 'REMOVE_ASSET'; payload: string }
| { type: 'CLEAR_ASSETS' }
- | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment } }
+ | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment | Investment[] } }
| { type: 'REMOVE_INVESTMENT'; payload: { assetId: string; investmentId: string } }
| { type: 'UPDATE_DATE_RANGE'; payload: DateRange }
| { type: 'UPDATE_ASSET_HISTORICAL_DATA'; payload: { assetId: string; historicalData: HistoricalData[]; longName?: string } }
@@ -57,7 +57,7 @@ const portfolioReducer = (state: PortfolioState, action: PortfolioAction): Portf
...state,
assets: state.assets.map(asset =>
asset.id === action.payload.assetId
- ? { ...asset, investments: [...asset.investments, action.payload.investment] }
+ ? { ...asset, investments: [...asset.investments, ...(Array.isArray(action.payload.investment) ? action.payload.investment : [action.payload.investment])] }
: asset
)
};
@@ -127,7 +127,7 @@ export interface PortfolioContextType extends PortfolioState {
addAsset: (asset: Asset) => void;
removeAsset: (assetId: string) => void;
clearAssets: () => void;
- addInvestment: (assetId: string, investment: Investment) => void;
+ addInvestment: (assetId: string, investment: Investment | Investment[]) => void;
removeInvestment: (assetId: string, investmentId: string) => void;
updateDateRange: (dateRange: DateRange) => void;
updateAssetHistoricalData: (assetId: string, historicalData: HistoricalData[], longName?: string) => void;
@@ -148,7 +148,7 @@ export const PortfolioProvider = ({ children }: { children: React.ReactNode }) =
addAsset: (asset: Asset) => dispatch({ type: 'ADD_ASSET', payload: asset }),
removeAsset: (assetId: string) => dispatch({ type: 'REMOVE_ASSET', payload: assetId }),
clearAssets: () => dispatch({ type: 'CLEAR_ASSETS' }),
- addInvestment: (assetId: string, investment: Investment) =>
+ addInvestment: (assetId: string, investment: Investment | Investment[]) =>
dispatch({ type: 'ADD_INVESTMENT', payload: { assetId, investment } }),
removeInvestment: (assetId: string, investmentId: string) =>
dispatch({ type: 'REMOVE_INVESTMENT', payload: { assetId, investmentId } }),
diff --git a/src/services/yahooFinanceService.ts b/src/services/yahooFinanceService.ts
index 515e3f7..a0b107f 100644
--- a/src/services/yahooFinanceService.ts
+++ b/src/services/yahooFinanceService.ts
@@ -1,4 +1,5 @@
import type { Asset, YahooSearchResponse, YahooChartResult } from "../types";
+import toast from "react-hot-toast";
// this is only needed when hosted staticly without a proxy server or smt
// TODO change it to use the proxy server
@@ -49,6 +50,7 @@ export const searchAssets = async (query: string): Promise
=> {
}));
} catch (error) {
console.error('Error searching assets:', error);
+ toast.error('Failed to search assets. Please try again later.');
return [];
}
};
@@ -81,6 +83,7 @@ export const getHistoricalData = async (symbol: string, startDate: string, endDa
}
} catch (error) {
console.error('Error fetching historical data:', error);
+ toast.error(`Failed to fetch historical data for ${symbol}. Please try again later.`);
return { historicalData: [], longName: '' };
}
};
diff --git a/src/utils/calculations/performance.ts b/src/utils/calculations/performance.ts
index f436c90..cd2e04b 100644
--- a/src/utils/calculations/performance.ts
+++ b/src/utils/calculations/performance.ts
@@ -52,7 +52,7 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor
const yearStart = new Date(year, 0, 1); // 1. Januar
const yearEnd = year === endYear ? new Date(year, now.getMonth(), now.getDate()) : new Date(year, 11, 31); // Aktuelles Datum oder 31. Dez.
- const investmentsPerformances:number[] = [];
+ const yearInvestments: { percent: number; weight: number }[] = [];
for (const asset of assets) {
// Get prices for the start and end of the year
@@ -67,7 +67,7 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor
if (startPrice === 0 || endPrice === 0) {
console.warn(`Skipping asset for year ${year} due to missing start or end price`);
- continue; // Überspringe, wenn keine Daten vorhanden
+ continue;
}
// Get all investments made before or during this year
@@ -88,19 +88,23 @@ export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerfor
(data) => isAfter(new Date(data.date), new Date(investment.date!))
).find((v) => v.price !== 0)?.price || 0;
-
if (buyInPrice > 0) {
- const shares = investment.amount / buyInPrice; // Berechne Anzahl der Shares
+ const shares = investment.amount / buyInPrice;
const endValue = shares * endPrice;
const startValue = shares * startPrice;
- investmentsPerformances.push((endValue - startValue) / startValue * 100);
+ yearInvestments.push({
+ percent: ((endValue - startValue) / startValue) * 100,
+ weight: startValue
+ });
}
}
}
- // Calculate performance for the year
- if (investmentsPerformances.length > 0) {
- const percentage = investmentsPerformances.reduce((acc, curr) => acc + curr, 0) / investmentsPerformances.length;
+ // Calculate weighted average performance for the year
+ if (yearInvestments.length > 0) {
+ const totalWeight = yearInvestments.reduce((sum, inv) => sum + inv.weight, 0);
+ const percentage = yearInvestments.reduce((sum, inv) =>
+ sum + (inv.percent * (inv.weight / totalWeight)), 0);
if (!isNaN(percentage)) {
annualPerformances.push({ year, percentage });
diff --git a/src/utils/calculations/portfolioValue.ts b/src/utils/calculations/portfolioValue.ts
index 29fbf08..b7f0f52 100644
--- a/src/utils/calculations/portfolioValue.ts
+++ b/src/utils/calculations/portfolioValue.ts
@@ -21,8 +21,12 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) =
percentageChange: 0,
assets: {},
};
- // this should contain the percentage gain of all investments till now
- const pPercents: number[] = [];
+
+ interface WeightedPercent {
+ percent: number;
+ weight: number;
+ }
+ const weightedPercents: WeightedPercent[] = [];
for (const asset of assets) {
// calculate the invested kapital
@@ -49,14 +53,20 @@ export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) =
dayData.assets[asset.id] = currentValueOfAsset;
const percent = ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100;
- if (!Number.isNaN(percent)) pPercents.push(percent);
+ if (!Number.isNaN(percent) && investedValue && investedValue > 0) {
+ weightedPercents.push({
+ percent,
+ weight: investedValue
+ });
+ }
}
}
-
- // Calculate average percentage change if percentages array is not empty
- if (pPercents.length > 0) {
- dayData.percentageChange = pPercents.reduce((a, b) => a + b, 0) / pPercents.length;
+ // Calculate weighted average percentage change
+ if (weightedPercents.length > 0) {
+ const totalWeight = weightedPercents.reduce((sum, wp) => sum + wp.weight, 0);
+ dayData.percentageChange = weightedPercents.reduce((sum, wp) =>
+ sum + (wp.percent * (wp.weight / totalWeight)), 0);
} else {
dayData.percentageChange = 0;
}